From c6ff78f4147ad5505b45cce8f1d6f44c8585a9b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Dec 2023 12:39:05 +0100 Subject: [PATCH 001/518] 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 002/518] 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 003/518] 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 004/518] 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 005/518] 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 006/518] 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 007/518] 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 008/518] 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 009/518] 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 010/518] 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 011/518] 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 012/518] 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 013/518] 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 014/518] 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 015/518] 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 016/518] 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 017/518] 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 018/518] 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 019/518] 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 020/518] 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 021/518] 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 022/518] 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 023/518] 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 024/518] 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 025/518] 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 026/518] 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 027/518] 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 028/518] 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 029/518] 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 030/518] 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 031/518] 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 032/518] 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 033/518] 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 034/518] 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 035/518] 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 036/518] 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 037/518] 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 038/518] 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 039/518] 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 040/518] 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 041/518] 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 042/518] 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 043/518] 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 044/518] 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 045/518] 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 046/518] 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 047/518] 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 048/518] 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 049/518] 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 050/518] 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 051/518] 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 052/518] 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 053/518] 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 054/518] 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 055/518] 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 056/518] 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 057/518] 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 058/518] 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 059/518] 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 060/518] 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 061/518] 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 062/518] 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 063/518] 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 064/518] 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 065/518] 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 066/518] 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 067/518] 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 068/518] 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 069/518] 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 070/518] 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 071/518] 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 072/518] 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 073/518] 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 074/518] 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 075/518] 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 076/518] 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 077/518] 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 078/518] 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 079/518] 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 080/518] 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 081/518] 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 082/518] 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 083/518] 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 084/518] 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 085/518] 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 086/518] 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 087/518] 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 088/518] 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 089/518] 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 090/518] 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 091/518] 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 092/518] 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 093/518] 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 094/518] 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 095/518] 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 096/518] 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 097/518] 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 098/518] 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 099/518] 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 100/518] 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 101/518] 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 102/518] 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 103/518] 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 104/518] 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 105/518] 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 106/518] 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 107/518] 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 108/518] 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 109/518] 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 110/518] 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 111/518] 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 112/518] 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 113/518] 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 114/518] 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 115/518] 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 116/518] 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 117/518] 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 118/518] 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 119/518] 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 120/518] 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 121/518] 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 122/518] 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 123/518] 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 124/518] 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 125/518] 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 126/518] 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 127/518] 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 128/518] 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 129/518] 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 130/518] 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 131/518] 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 132/518] 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 133/518] 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 134/518] 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 135/518] 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 136/518] 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 137/518] 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 138/518] 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 139/518] 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 140/518] 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 141/518] 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 142/518] 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 143/518] 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 144/518] 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 145/518] 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 146/518] 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 147/518] 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 148/518] 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 149/518] 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 150/518] 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 151/518] 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 152/518] 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 153/518] 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 154/518] 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 155/518] 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 156/518] 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 157/518] 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 158/518] 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 159/518] 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 160/518] 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 161/518] 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 162/518] 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 163/518] 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 164/518] 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 165/518] 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 166/518] 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 167/518] 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 168/518] 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 169/518] 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 170/518] 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 171/518] 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 172/518] 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 173/518] 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 174/518] 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 175/518] 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 176/518] 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 177/518] 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 178/518] 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 179/518] 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 180/518] 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 181/518] 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 182/518] 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 183/518] 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 184/518] 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 185/518] 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 186/518] 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 187/518] 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 188/518] 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 189/518] 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 190/518] 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 191/518] 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 192/518] 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 193/518] 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 194/518] 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 195/518] 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 196/518] 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 197/518] 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 198/518] 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 199/518] 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 200/518] 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 201/518] 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 202/518] 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 203/518] 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 204/518] 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 205/518] 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 206/518] 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 207/518] 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 208/518] 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 209/518] 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 210/518] 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 211/518] 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 212/518] 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 213/518] 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 214/518] 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 215/518] 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 216/518] 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 217/518] 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 218/518] 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 219/518] 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 220/518] 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 221/518] 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 222/518] 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 223/518] 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 224/518] 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 225/518] 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 226/518] 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 227/518] 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 228/518] 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 229/518] 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 230/518] 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 231/518] 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 232/518] 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 233/518] 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 234/518] 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 235/518] 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 236/518] 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 237/518] 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 238/518] 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 239/518] 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 240/518] 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 241/518] 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 242/518] 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 243/518] 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 244/518] 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 245/518] 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 246/518] 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 247/518] 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 248/518] 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 249/518] 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 250/518] 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 251/518] 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 252/518] 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 253/518] 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 254/518] 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 255/518] 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 256/518] 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 257/518] 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 258/518] 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 259/518] 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 260/518] 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 261/518] 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 262/518] 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 263/518] 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 264/518] 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 265/518] 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 266/518] 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 267/518] 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 268/518] 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 269/518] 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 270/518] 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 271/518] 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 272/518] 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 273/518] 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 274/518] 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 275/518] 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 276/518] 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 277/518] 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 278/518] 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 279/518] 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 280/518] 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 281/518] 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 282/518] 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 283/518] 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 284/518] 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 285/518] 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 286/518] 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 287/518] 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 288/518] 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 289/518] 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 290/518] 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 291/518] 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 292/518] 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 293/518] 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 294/518] 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 295/518] 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 296/518] 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 297/518] 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 298/518] 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 299/518] 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 300/518] 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 301/518] 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 302/518] 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 303/518] 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 304/518] 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 305/518] 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 306/518] 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 307/518] 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 308/518] 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 309/518] 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 310/518] 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 311/518] 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 312/518] 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 313/518] 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 314/518] 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 315/518] 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 316/518] 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 317/518] 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 318/518] 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 319/518] 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 320/518] 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 321/518] 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 322/518] 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 323/518] 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 324/518] 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 325/518] 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 326/518] 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 327/518] 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 328/518] 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 329/518] 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 330/518] 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 331/518] 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 332/518] 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 333/518] 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 334/518] 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 335/518] 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 336/518] 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 337/518] 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 338/518] 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 339/518] 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 340/518] 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 341/518] 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 342/518] 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 343/518] 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 344/518] 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 345/518] 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 346/518] 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 347/518] 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 348/518] 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 349/518] 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 350/518] 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 351/518] 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 352/518] 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 353/518] 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 354/518] 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 355/518] 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 356/518] 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 357/518] 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 358/518] 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 359/518] 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 360/518] 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 361/518] 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 362/518] 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 363/518] 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 364/518] 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 365/518] 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 366/518] 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 367/518] 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 368/518] 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 369/518] 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 370/518] 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 371/518] 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 372/518] 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 373/518] 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 374/518] 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 375/518] 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 376/518] 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 377/518] 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 378/518] 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 379/518] 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 380/518] 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 381/518] 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 382/518] 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 383/518] 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 384/518] 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 385/518] 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 386/518] 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 387/518] 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 388/518] 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 389/518] 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 390/518] 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 391/518] 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 392/518] 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 393/518] 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 394/518] 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 395/518] 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 396/518] 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 397/518] 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 398/518] 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 399/518] 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 400/518] 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 401/518] 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 402/518] 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 403/518] 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 404/518] 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 405/518] 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 406/518] 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 407/518] 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 408/518] 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 409/518] 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 410/518] 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 411/518] 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 412/518] 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 413/518] 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 414/518] 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 415/518] 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 416/518] 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 417/518] 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 418/518] 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 419/518] 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 420/518] 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 421/518] 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 422/518] 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 423/518] 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 424/518] 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 425/518] 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 426/518] 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 427/518] 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 428/518] 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 429/518] 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 430/518] 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 431/518] 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 432/518] 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 433/518] 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 434/518] 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 435/518] 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 436/518] 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 437/518] 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 438/518] 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 439/518] 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 440/518] 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 441/518] 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 442/518] 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 443/518] 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 444/518] 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 445/518] 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 446/518] 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 447/518] 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 448/518] 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 449/518] 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 450/518] 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 451/518] 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 452/518] 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 453/518] 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 454/518] 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 455/518] 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 456/518] 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 457/518] 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 458/518] 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 459/518] 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 460/518] 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 461/518] 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 462/518] 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 463/518] 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 464/518] 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 465/518] 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 466/518] 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 467/518] 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 468/518] 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 469/518] 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 470/518] 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 471/518] 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 472/518] 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 473/518] 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 474/518] 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 475/518] 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 476/518] 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 477/518] 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 478/518] 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 479/518] 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 480/518] 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 481/518] 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 482/518] 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 483/518] 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 484/518] 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 485/518] 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 486/518] 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 487/518] 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 488/518] 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 489/518] 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 490/518] 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 491/518] 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 492/518] 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 493/518] 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 494/518] 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 495/518] 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 496/518] 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 497/518] 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 498/518] 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 499/518] 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 500/518] 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 501/518] 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 502/518] 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 503/518] 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 504/518] 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 505/518] 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 506/518] 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 507/518] 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 508/518] 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 509/518] 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 510/518] 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 511/518] 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 512/518] 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 513/518] 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 514/518] 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 515/518] 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 516/518] 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 517/518] 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 518/518] 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