Compare commits

..

39 commits

Author SHA1 Message Date
Romain Vimont
f01231dff8 Update links to 3.3.1 2025-06-20 20:14:42 +02:00
Romain Vimont
5b18ce0d2e Bump version to 3.3.1 2025-06-20 19:54:16 +02:00
Romain Vimont
4841fdd1ef Add horizontal scrolling support for HID mouse
PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 19:54:16 +02:00
Romain Vimont
fc75319bb2 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 <https://github.com/Genymobile/scrcpy/issues/6156>
PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 19:54:16 +02:00
Romain Vimont
7c8bdccbdc 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 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 19:54:16 +02:00
Romain Vimont
9787fe5d26 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 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 18:30:50 +02:00
Romain Vimont
98d30288f7 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 283326b2f6
Fixes #6165 <https://github.com/Genymobile/scrcpy/issues/6165>
2025-06-17 21:08:54 +02:00
Romain Vimont
d74cfd5711 Silence DiscouragedPrivateApi lint warning 2025-06-13 09:40:45 +02:00
Romain Vimont
cd3a5d50b6 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 8a02e3c2f5
Fixes #6151 <https://github.com/Genymobile/scrcpy/issues/6151>
2025-06-13 09:37:32 +02:00
Romain Vimont
772f42134a Use Context.CLIPBOARD_SERVICE directly
The constant is defined in Context, not FakeContext.
2025-06-13 09:37:32 +02:00
berk ziya
38256d8ff9 Fix deprecated brew command
`brew cask` is an outdated command, replaced by `brew install --cask`.

Refs #5398 <https://github.com/Genymobile/scrcpy/pull/5398>
PR #6149 <https://github.com/Genymobile/scrcpy/pull/6149>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-12 15:30:04 +02:00
Romain Vimont
4e1cf13a50 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 283326b2f6
Fixes #6146 <https://github.com/Genymobile/scrcpy/issues/6146>
2025-06-12 09:10:11 +02:00
Romain Vimont
696402c68c Update links to 3.3 2025-06-11 22:15:30 +02:00
Romain Vimont
dc169e425e Bump version to 3.3 2025-06-11 19:39:48 +02:00
Romain Vimont
13fc75902a Merge branch 'master' into release 2025-06-11 19:39:09 +02:00
Romain Vimont
454beaa757 Upgrade libusb (1.0.29) 2025-06-11 19:39:02 +02:00
Romain Vimont
1a9ffb3814 Upgrade SDL (2.32.8) 2025-06-11 19:38:29 +02:00
Romain Vimont
ac16be54c8 Upgrade platform-tools (36.0.0) 2025-06-11 19:36:22 +02:00
Romain Vimont
8a02e3c2f5 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 <https://github.com/Genymobile/scrcpy/pull/6009>
PR #6129 <https://github.com/Genymobile/scrcpy/pull/6129>

Suggested by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2025-06-11 17:47:37 +02:00
Romain Vimont
283326b2f6 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 <https://github.com/Genymobile/scrcpy/pull/6009#issuecomment-2940810736>
PR #6129 <https://github.com/Genymobile/scrcpy/pull/6129>
2025-06-11 17:47:07 +02:00
Simon Chan
ca4f50c5ef 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 <https://github.com/Genymobile/scrcpy/issues/5547>
PR #6009 <https://github.com/Genymobile/scrcpy/pull/6009>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-05 20:34:11 +02:00
Romain Vimont
7a3fe830d4 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 <https://github.com/Genymobile/scrcpy/pull/6009>
2025-06-05 20:34:11 +02:00
Romain Vimont
ee414231ed Cache getDisplayInfo method
Do not use reflection to retrieve the method for every call.

PR #6009 <https://github.com/Genymobile/scrcpy/pull/6009>
2025-06-05 20:34:11 +02:00
Simon Chan
41ed40f5f9 Simplify InputManager wrapper
Use the public InputManager API.

PR #6009 <https://github.com/Genymobile/scrcpy/pull/6009>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-05 20:34:11 +02:00
Colin Kinloch
d2cc930975 Add app name SDL hint
This allows pulseaudio to label the audio stream "scrcpy" rather than
"SDL Application".

PR #6107 <https://github.com/Genymobile/scrcpy/pull/6107>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-05 19:59:59 +02:00
Romain Vimont
52f5d08d1f Avoid calling wait(0)
Calling wait(0) results in waiting without a timeout, which is
unintended.

Refs #6009 comment <https://github.com/Genymobile/scrcpy/pull/6009#issuecomment-2935930294>
2025-06-03 21:15:11 +02:00
Romain Vimont
70bfa2cf39 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 <https://zsh.sourceforge.io/Doc/Release/Completion-System.html>
2025-05-22 20:00:58 +02:00
hltdev8642
38f779d9d3 Escape parentheses in zsh completion script
PR #6079 <https://github.com/Genymobile/scrcpy/pull/6079>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-05-22 20:00:53 +02:00
Romain Vimont
8cd63cb63e 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 <https://github.com/Genymobile/scrcpy/pull/6080>
2025-05-15 19:52:52 +02:00
Romain Vimont
cc309a2b34 Build static linux binary on Ubuntu 22.04
Ubuntu 20.04 is no longer available on GitHub Actions.

Refs <https://github.com/actions/runner-images/issues/11101>
Refs #6050 <https://github.com/Genymobile/scrcpy/pull/6050>

This reverts commit 69858c6f43.
2025-05-02 11:39:47 +02:00
Romain Vimont
91a4a74641 Move regex pattern initialization
If text == null, then the Pattern is not used.
2025-04-25 10:24:07 +02:00
Romain Vimont
48f38c4bb6 Fix default locked capture orientation
The default landscape locked orientation was reversed.

Fixes #6010 <https://github.com/Genymobile/scrcpy/issues/6010>
2025-04-24 16:12:28 +02:00
Romain Vimont
6875e9aa88 Revert "Fix AudioRecord package name for Android 16"
This reverts commit c27d116a66.

This commit breaks audio on Android 16 beta 4.

Refs #5960 comment <https://github.com/Genymobile/scrcpy/issues/5960#issuecomment-2816608015>
Fixes #6021 <https://github.com/Genymobile/scrcpy/issues/6021>
2025-04-24 16:05:13 +02:00
Nicholas Wilson
c5ed2cfc28 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 <https://github.com/Genymobile/scrcpy/pull/6017>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-04-19 10:30:29 +02:00
Romain Vimont
1a0d300786 Add missing --screen-off-timeout doc in manpage
Refs eff5b4b219
2025-04-14 18:07:37 +02:00
Romain Vimont
d2447b5c19 Fix --screen-off-timeout bash completion
Only the option must be auto-completed, not its value.
2025-04-14 18:05:08 +02:00
Romain Vimont
5900e9e39c Remove irrelevant link in FAQ 2025-04-07 10:30:56 +02:00
Romain Vimont
882003f314 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 <https://github.com/Genymobile/scrcpy/pull/5804>
Fixes #5970 <https://github.com/Genymobile/scrcpy/issues/5970>
2025-04-03 08:15:55 +02:00
Romain Vimont
db9dc6ae83 Make the snap version as obsolete
The version of scrcpy packaged in snap is currently 1.25.

Refs <https://snapcraft.io/scrcpy>
2025-04-01 11:04:34 +02:00
47 changed files with 410 additions and 440 deletions

View file

@ -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: |

5
FAQ.md
View file

@ -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

View file

@ -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.1)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -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
@ -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

View file

@ -205,6 +205,7 @@ _scrcpy() {
|-p|--port \
|--push-target \
|--rotation \
|--screen-off-timeout \
|--tunnel-host \
|--tunnel-port \
|--v4l2-buffer \

View file

@ -1,4 +1,4 @@
#compdef -N scrcpy -N scrcpy.exe
#compdef scrcpy scrcpy.exe
#
# name: scrcpy
# auth: hltdev [hltdev8642@gmail.com]
@ -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]'

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -1,33 +0,0 @@
From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001
From: Neal Gompa <neal@gompa.dev>
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

View file

@ -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"

View file

@ -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.1"
END
END
BLOCK "VarFileInfo"

View file

@ -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".

View file

@ -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"},

View file

@ -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

View file

@ -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);

View file

@ -3,8 +3,8 @@
#include <stdint.h>
// 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,22 +188,27 @@ 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
}
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 && !event->hscroll_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);
// Horizontal scrolling ignored
data[3] = CLAMP(event->vscroll_int, -127, 127);
data[4] = CLAMP(event->hscroll_int, -127, 127);
return true;
}
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {

View file

@ -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);

View file

@ -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
};

View file

@ -897,12 +897,14 @@ 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
.hscroll_int = event->x,
.vscroll_int = event->y,
.buttons_state = im->mouse_buttons_state,
};

View file

@ -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");
@ -165,7 +176,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 +208,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 +944,7 @@ aoa_complete:
}
}
ret = event_loop(s);
ret = event_loop(s, options->window);
terminate_event_loop();
LOGD("quit...");

View file

@ -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");
}

View file

@ -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)");

View file

@ -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),
};

View file

@ -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)));

View file

@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v3.2`][direct-scrcpy-server]
<sub>SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0`</sub>
- [`scrcpy-server-v3.3.1`][direct-scrcpy-server]
<sub>SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8`</sub>
[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.1/scrcpy-server-v3.3.1
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View file

@ -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)
<sub>SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be`</sub>
- [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422`</sub>
[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.1/scrcpy-linux-x86_64-v3.3.1.tar.gz
and extract it.
@ -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))

View file

@ -6,15 +6,15 @@
Download a static build of the [latest release]:
- [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b`</sub>
- [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139`</sub>
- [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e`</sub>
- [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d`</sub>
[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.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.

View file

@ -6,14 +6,14 @@
Download the [latest release]:
- [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit)
<sub>SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0`</sub>
- [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit)
<sub>SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357`</sub>
- [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit)
<sub>SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0`</sub>
- [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit)
<sub>SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6`</sub>
[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.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.

View file

@ -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.1/scrcpy-server-v3.3.1
PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View file

@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '3.2',
version: '3.3.1',
meson_version: '>= 0.49',
default_options: [
'c_std=c11',

View file

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 35
versionCode 30200
versionName "3.2"
versionCode 30301
versionName "3.3.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View file

@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.2
SCRCPY_VERSION_NAME=3.3.1
PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}

View file

@ -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]);

View file

@ -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";
@ -72,7 +76,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();
}
@ -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;
}
}

View file

@ -24,10 +24,13 @@ 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;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
@ -55,17 +58,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();
}
}
}
@ -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,21 @@ public final class Server {
}
}
private static void prepareMainLooper() {
// Like Looper.prepareMainLooper(), but with quitAllowed set to true
Looper.prepare();
synchronized (Looper.class) {
try {
@SuppressLint("DiscouragedPrivateApi")
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 {
@ -221,6 +229,8 @@ public final class Server {
Ln.e("Exception on thread " + t, e);
});
prepareMainLooper();
Options options = Options.parse(args);
Ln.disableSystemStreams();

View file

@ -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()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// 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"
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();
}
private static void fillAppInfo() {
try {
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();

View file

@ -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);
}

View file

@ -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;
@ -17,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;
@ -114,22 +114,20 @@ 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(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 {
@ -156,8 +154,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;
}
@ -699,7 +723,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (timeout < 0) {
return null;
}
displayDataAvailable.wait(timeout);
if (timeout > 0) {
displayDataAvailable.wait(timeout);
}
data = displayData.get();
}

View file

@ -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<Integer, FileDescriptor> 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);
}
}
}

View file

@ -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;
}
}

View file

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

View file

@ -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;
import android.content.Context;
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(Context.CLIPBOARD_SERVICE);
if (manager == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
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);
}
}

View file

@ -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;
@ -81,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) {
@ -95,12 +96,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();
@ -114,9 +115,18 @@ public final class DisplayManager {
return flags;
}
// 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);
}
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);
@ -129,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);
}

View file

@ -1,11 +1,15 @@
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;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
@ -15,39 +19,28 @@ 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;
private static Method addUniqueIdAssociationByPortMethod;
private static Method removeUniqueIdAssociationByPortMethod;
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;
}
@ -57,6 +50,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;
}
@ -97,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);
}
}
}

View file

@ -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();
}

View file

@ -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