Compare commits

...

26 Commits

Author SHA1 Message Date
Yu-Chen Lin
f24d488ca2 Check client and server mismatch
Send client version as first parameter and check it at server start.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-11 14:43:16 +01:00
Romain Vimont
6996cbf5d3 Log device disconnection
If scrcpy closes due to socket disconnection, log a warning.
2019-11-09 21:17:03 +01:00
olbb
e282100d0b Call Looper.prepareMainLooper() to avoid exception
Some devices internally create a Handler when creating an input Surface,
causing an exception:

> Surface: java.lang.RuntimeException: Can't create handler inside
> thread that has not called Looper.prepare()

As a workaround, call Looper.prepareMainLooper() beforehand.

Fixes:
 - <https://github.com/Genymobile/scrcpy/issues/240>
 - <https://github.com/Genymobile/scrcpy/issues/921>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-09 20:31:57 +01:00
Romain Vimont
b08a98324d Fix segfault on empty file recorded
Write the file trailer only if the file header have been written, to
avoid a segfault in libav.

Fixes <https://github.com/Genymobile/scrcpy/issues/918>.
2019-11-07 21:58:57 +01:00
Romain Vimont
c916af0984 Add --prefer-text option
Expose an option to configure how key/text events are forwarded to the
Android device.

Enabling the option avoids issues when combining multiple keys to enter
special characters, but breaks the expected behavior of alpha keys in
games (typically WASD).

Fixes <https://github.com/Genymobile/scrcpy/issues/650>
2019-11-07 19:01:35 +01:00
Romain Vimont
ff061b4f30 Deprecate short options for advanced features
The short options will be removed in the future (and may be reused for
other features).
2019-11-07 10:01:59 +01:00
Romain Vimont
157c60feb4 Fix indentation 2019-11-07 09:48:48 +01:00
Romain Vimont
2d90e1befd Fix include recorder.h 2019-11-06 22:22:46 +01:00
Romain Vimont
0e301ddf19 Factorize scrcpy options and command-line args
Do not duplicate all scrcpy options fields in the structure storing the
parsed command-line arguments.
2019-11-06 22:06:54 +01:00
Romain Vimont
c42ff75b74 Pass screen to mouse event converters
Mouse events coordinates depend on the screen size and location, so the
converter need to access the screen.

The fact that it needs the position or the size is an internal detail,
so pass a pointer to the whole screen structure.
2019-11-06 21:24:36 +01:00
Romain Vimont
b0db1178d1 Move event conversion to input_manager
Only keep helper functions separated.

This will help to convert coordinates internally when necessary.
2019-11-06 21:24:36 +01:00
Romain Vimont
8d601d3210 Rename "input_manager" variables to "im"
It is used a lot, a short name improves readability.
2019-11-06 21:24:36 +01:00
Romain Vimont
683f7ca848 Document how to attach a debugger to the server 2019-11-03 19:42:37 +01:00
Romain Vimont
120f08ee96 Fix manpage option parameter format
The parameter for --window-title was not underlined the same way as
others.
2019-11-03 16:51:47 +01:00
Romain Vimont
0415672a75 Merge branch 'master' into dev 2019-11-03 15:56:10 +01:00
Romain Vimont
3ea4742321 Call ninja without changing directory
In build instructions, use:

    ninja -Cx ...

instead of:

    cd x
    ninja ...
2019-10-31 21:05:04 +01:00
Romain Vimont
95fd64b5de Add scrcpy version in recorded video metadata
It might help to understand problems in recorded videos.
2019-10-31 20:57:57 +01:00
Romain Vimont
ab205084dc Merge pull request #896 from yangfl/upstream
Add manpage for scrcpy
2019-10-31 13:44:01 +01:00
yangfl
4696878a97 Add manpage for scrcpy 2019-10-31 18:18:19 +08:00
Romain Vimont
3da95b52bd Rename scrcpy-server.jar to scrcpy-server
The server name ending with .jar has several drawbacks:
 - meson requires the jar executable to attempt to modify it:
     <https://github.com/Genymobile/scrcpy/issues/404#issuecomment-456065923>
     <https://github.com/mesonbuild/meson/issues/4844>
 - meson warns during "ninja install"
     <https://github.com/Genymobile/scrcpy/issues/458>
 - some users try to execute it on the computer as a java executable

Removing the extension solves all these problems.
2019-10-31 10:54:29 +01:00
Romain Vimont
c72f677435 Merge branch 'master' into dev 2019-10-30 23:29:44 +01:00
Romain Vimont
1380f6e00f Fix help for --record-format
Record format requires a parameter.
2019-10-30 22:51:40 +01:00
Romain Vimont
d841718956 Add a script to build the server without gradle
Gradle versions may sometimes cause problems. This script offers an
alternative.
2019-10-30 21:47:20 +01:00
Romain Vimont
17d53be3ef Fix mouse events conversion
The conversion from SDL mouse state to Android mouse state used wrong
constants as mask.

Fixes <https://github.com/Genymobile/scrcpy/issues/635>
2019-10-25 11:09:06 +02:00
Romain Vimont
f9938dbf88 Inject button state for touch/mouse events
The buttons state was forwarded, but ignored by the server.
2019-10-25 11:04:04 +02:00
Romain Vimont
f6c8460ebb Rename window size functions for clarity
Now, get_window_size() returns the current window size (fullscreen or
not), while get_windowed_window_size() always returned the windowed size
(the size when fullscreen is disabled).
2019-10-20 16:08:16 +02:00
26 changed files with 686 additions and 298 deletions

View File

@@ -195,8 +195,7 @@ Then, build:
```bash ```bash
meson x --buildtype release --strip -Db_lto=true meson x --buildtype release --strip -Db_lto=true
cd x ninja -Cx
ninja
``` ```
_Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja
@@ -219,13 +218,13 @@ To run without installing:
After a successful build, you can install _scrcpy_ on the system: After a successful build, you can install _scrcpy_ on the system:
```bash ```bash
sudo ninja install # without sudo on Windows sudo ninja -Cx install # without sudo on Windows
``` ```
This installs two files: This installs two files:
- `/usr/local/bin/scrcpy` - `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server.jar` - `/usr/local/share/scrcpy/scrcpy-server`
Just remove them to "uninstall" the application. Just remove them to "uninstall" the application.
@@ -244,8 +243,7 @@ configuration:
```bash ```bash
meson x --buildtype release --strip -Db_lto=true \ meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar -Dprebuilt_server=/path/to/scrcpy-server
cd x ninja -Cx
ninja sudo ninja -Cx install
sudo ninja install
``` ```

View File

@@ -3,7 +3,7 @@
## Overview ## Overview
This application is composed of two parts: This application is composed of two parts:
- the server (`scrcpy-server.jar`), to be executed on the device, - the server (`scrcpy-server`), to be executed on the device,
- the client (the `scrcpy` binary), executed on the host computer. - the client (the `scrcpy` binary), executed on the host computer.
The client is responsible to push the server to the device and start its The client is responsible to push the server to the device and start its
@@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._
Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing
`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle
build system, the server is built to an (unsigned) APK (renamed to build system, the server is built to an (unsigned) APK (renamed to
`scrcpy-server.jar`). `scrcpy-server`).
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
[apk]: https://en.wikipedia.org/wiki/Android_application_package [apk]: https://en.wikipedia.org/wiki/Android_application_package
@@ -268,3 +268,33 @@ For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and If you find a bug, or have an awesome idea to implement, please discuss and
contribute ;-) contribute ;-)
### Debug the server
The server is pushed to the device by the client on startup.
To debug it, enable the server debugger during configuration:
```bash
meson x -Dserver_debugger=true
# or, if x is already configured
meson configure x -Dserver_debugger=true
```
Then recompile.
When you start scrcpy, it will start a debugger on port 5005 on the device.
Redirect that port to the computer:
```bash
adb forward tcp:5005 tcp:5005
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
- Host: `localhost`
- Port: `5005`
Then click on _Debug_.

View File

@@ -3,7 +3,7 @@
# #
# Here, "portable" means that the client and server binaries are expected to be # Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate # anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar). # locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server).
# #
# In particular, this implies to change the location from where the client push # In particular, this implies to change the location from where the client push
# the server to the device. # the server to the device.
@@ -97,7 +97,7 @@ build-win64-noconsole: prepare-deps-win64
dist-win32: build-server build-win32 build-win32-noconsole dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@@ -112,7 +112,7 @@ dist-win32: build-server build-win32 build-win32-noconsole
dist-win64: build-server build-win64 build-win64-noconsole dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.4-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"

View File

@@ -143,7 +143,6 @@ This is useful for example to mirror only one eye of the Oculus Go:
```bash ```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
scrcpy -c 1224:1440:0:0 # short version
``` ```
If `--max-size` is also specified, resizing is applied after cropping. If `--max-size` is also specified, resizing is applied after cropping.
@@ -226,7 +225,6 @@ The window of app can always be above others by:
```bash ```bash
scrcpy --always-on-top scrcpy --always-on-top
scrcpy -T # short version
``` ```
@@ -358,7 +356,7 @@ To use a specific _adb_ binary, configure its path in the environment variable
ADB=/path/to/adb scrcpy ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file, configure its path in To override the path of the `scrcpy-server` file, configure its path in
`SCRCPY_SERVER_PATH`. `SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345

View File

@@ -93,7 +93,7 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX) # the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix')) conf.set_quoted('PREFIX', get_option('prefix'))
# build a "portable" version (with scrcpy-server.jar accessible from the same # build a "portable" version (with scrcpy-server accessible from the same
# directory as the executable) # directory as the executable)
conf.set('PORTABLE', get_option('portable')) conf.set('PORTABLE', get_option('portable'))
@@ -115,6 +115,9 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows # disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
configure_file(configuration: conf, output: 'config.h') configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src') src_dir = include_directories('src')
@@ -134,6 +137,8 @@ executable('scrcpy', src,
c_args: c_args, c_args: c_args,
link_args: link_args) link_args: link_args)
install_man('scrcpy.1')
### TESTS ### TESTS

238
app/scrcpy.1 Normal file
View File

@@ -0,0 +1,238 @@
.TH "scrcpy" "1"
.SH NAME
scrcpy \- Display and control your Android device
.SH SYNOPSIS
.B scrcpy
.RI [ options ]
.SH DESCRIPTION
.B scrcpy
provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access.
.SH OPTIONS
.TP
.BI "\-b, \-\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 8000000.
.TP
.BI "\-c, \-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
.B \-\-max\-size
value is computed on the cropped size.
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
.TP
.BI "\-F, \-\-record\-format " format
Force recording format (either mp4 or mkv).
.TP
.B \-h, \-\-help
Print this help.
.TP
.BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
Default is 0 (unlimited).
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled).
.TP
.BI "\-p, \-\-port " port
Set the TCP port the client listens on.
Default is 27183.
.TP
.B \-\-prefer\-text
Inject alpha characters and space as text events instead of key events.
This avoids issues when combining multiple keys to enter special characters,
but breaks the expected behavior of alpha keys in games (typically WASD).
.TP
.BI "\-\-push\-target " path
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
Default is "/sdcard/".
.TP
.BI "\-r, \-\-record " file
Record screen to
.IR file .
The format is determined by the
.B \-F/\-\-record\-format
option if set, or by the file extension (.mp4 or .mkv).
.TP
.B \-\-render\-expired\-frames
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
.TP
.BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, disable on quit.
It only shows physical touches (not clicks from scrcpy).
.TP
.B \-T, \-\-always\-on\-top
Make scrcpy window always on top (above other windows).
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.BI \-\-window\-title " text
Set a custom window title.
.SH SHORTCUTS
.TP
.B Ctrl+f
switch fullscreen mode
.TP
.B Ctrl+g
resize window to 1:1 (pixel\-perfect)
.TP
.B Ctrl+x, Double\-click on black borders
resize window to remove black borders
.TP
.B Ctrl+h, Home, Middle\-click
Click on HOME
.TP
.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on)
Click on BACK
.TP
.B Ctrl+s
Click on APP_SWITCH
.TP
.B Ctrl+m
Click on MENU
.TP
.B Ctrl+Up
Click on VOLUME_UP
.TP
.B Ctrl+Down
Click on VOLUME_DOWN
.TP
.B Ctrl+p
Click on POWER (turn screen on/off)
.TP
.B Right\-click (when screen is off)
turn screen on
.TP
.B Ctrl+o
turn device screen off (keep mirroring)
.TP
.B Ctrl+n
expand notification panel
.TP
.B Ctrl+Shift+n
collapse notification panel
.TP
.B Ctrl+c
copy device clipboard to computer
.TP
.B Ctrl+v
paste computer clipboard to device
.TP
.B Ctrl+Shift+v
copy computer clipboard to device
.TP
.B Ctrl+i
enable/disable FPS counter (print frames/second in logs)
.TP
.B Drag & drop APK file
install APK from computer
.SH Environment variables
.TP
.B ADB
Specify the path to adb.
.TP
.B SCRCPY_SERVER_PATH
Specify the path to server binary.
.SH AUTHORS
.B scrcpy
is written by Romain Vimont.
This manual page was written by
.MT mmyangfl@gmail.com
Yangfl
.ME
for the Debian Project (and may be used by others).
.SH "REPORTING BUGS"
Report bugs to
.UR https://github.com/Genymobile/scrcpy/issues
.UE .
.SH COPYRIGHT
Copyright \(co 2018 Genymobile
.UR https://www.genymobile.com
Genymobile
.UE
Copyright \(co 2018\-2019
.MT rom@rom1v.com
Romain Vimont
.ME
Licensed under the Apache License, Version 2.0.
.SH WWW
.UR https://github.com/Genymobile/scrcpy
.UE

View File

@@ -5,7 +5,7 @@
#define MAP(FROM, TO) case FROM: *to = TO; return true #define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false #define FAIL default: return false
static bool bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
@@ -33,7 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) {
return metastate; return metastate;
} }
static enum android_metastate enum android_metastate
convert_meta_state(SDL_Keymod mod) { convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0; enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) { if (mod & KMOD_LSHIFT) {
@@ -74,8 +74,9 @@ convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate); return autocomplete_metastate(metastate);
} }
static bool bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) { convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text) {
switch (from) { switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
@@ -92,6 +93,12 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP); MAP(SDLK_UP, AKEYCODE_DPAD_UP);
} }
if (prefer_text) {
// do not forward alpha and space key events
return false;
}
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return false; return false;
} }
@@ -128,7 +135,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
} }
} }
static enum android_motionevent_buttons enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) { convert_mouse_buttons(uint32_t state) {
enum android_motionevent_buttons buttons = 0; enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) { if (state & SDL_BUTTON_LMASK) {
@@ -140,34 +147,16 @@ convert_mouse_buttons(uint32_t state) {
if (state & SDL_BUTTON_MMASK) { if (state & SDL_BUTTON_MMASK) {
buttons |= AMOTION_EVENT_BUTTON_TERTIARY; buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
} }
if (state & SDL_BUTTON_X1) { if (state & SDL_BUTTON_X1MASK) {
buttons |= AMOTION_EVENT_BUTTON_BACK; buttons |= AMOTION_EVENT_BUTTON_BACK;
} }
if (state & SDL_BUTTON_X2) { if (state & SDL_BUTTON_X2MASK) {
buttons |= AMOTION_EVENT_BUTTON_FORWARD; buttons |= AMOTION_EVENT_BUTTON_FORWARD;
} }
return buttons; return buttons;
} }
bool bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
return false;
}
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
static bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
@@ -177,41 +166,6 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
} }
bool bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
return true;
}
bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
return true;
}
static bool
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
@@ -220,39 +174,3 @@ convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
FAIL; FAIL;
} }
} }
bool
convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = screen_size;
// SDL touch event coordinates are normalized in the range [0; 1]
to->inject_touch_event.position.point.x = from->x * screen_size.width;
to->inject_touch_event.position.point.y = from->y * screen_size.height;
to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0;
return true;
}
bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->inject_scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->inject_scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y;
return true;
}

View File

@@ -7,36 +7,23 @@
#include "config.h" #include "config.h"
#include "control_msg.h" #include "control_msg.h"
struct complete_mouse_motion_event { bool
SDL_MouseMotionEvent *mouse_motion_event; convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to);
struct size screen_size;
};
struct complete_mouse_wheel_event { enum android_metastate
SDL_MouseWheelEvent *mouse_wheel_event; convert_meta_state(SDL_Keymod mod);
struct point position;
};
bool bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to); convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text);
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state);
bool bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size, convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to);
struct control_msg *to);
// the video size may be different from the real device size, so we need the
// size to which the absolute position apply, to scale it accordingly
bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
struct control_msg *to);
bool bool
convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size, convert_touch_action(SDL_EventType from, enum android_motionevent_action *to);
struct control_msg *to);
// on Android, a scroll event requires the current mouse position
bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
struct control_msg *to);
#endif #endif

View File

@@ -212,14 +212,17 @@ clipboard_paste(struct controller *controller) {
} }
void void
input_manager_process_text_input(struct input_manager *input_manager, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) { const SDL_TextInputEvent *event) {
char c = event->text[0]; if (!im->prefer_text) {
if (isalpha(c) || c == ' ') { char c = event->text[0];
SDL_assert(event->text[1] == '\0'); if (isalpha(c) || c == ' ') {
// letters and space are handled as raw key event SDL_assert(event->text[1] == '\0');
return; // letters and space are handled as raw key event
return;
}
} }
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text); msg.inject_text.text = SDL_strdup(event->text);
@@ -227,14 +230,34 @@ input_manager_process_text_input(struct input_manager *input_manager,
LOGW("Could not strdup input text"); LOGW("Could not strdup input text");
return; return;
} }
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
SDL_free(msg.inject_text.text); SDL_free(msg.inject_text.text);
LOGW("Could not request 'inject text'"); LOGW("Could not request 'inject text'");
} }
} }
static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
prefer_text)) {
return false;
}
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
void void
input_manager_process_key(struct input_manager *input_manager, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event,
bool control) { bool control) {
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
@@ -261,7 +284,7 @@ input_manager_process_key(struct input_manager *input_manager,
return; return;
} }
struct controller *controller = input_manager->controller; struct controller *controller = im->controller;
// capture all Ctrl events // capture all Ctrl events
if (ctrl || cmd) { if (ctrl || cmd) {
@@ -336,23 +359,23 @@ input_manager_process_key(struct input_manager *input_manager,
return; return;
case SDLK_f: case SDLK_f:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_switch_fullscreen(input_manager->screen); screen_switch_fullscreen(im->screen);
} }
return; return;
case SDLK_x: case SDLK_x:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_resize_to_fit(input_manager->screen); screen_resize_to_fit(im->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_resize_to_pixel_perfect(input_manager->screen); screen_resize_to_pixel_perfect(im->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && cmd && !repeat && down) { if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter = struct fps_counter *fps_counter =
input_manager->video_buffer->fps_counter; im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter); switch_fps_counter_state(fps_counter);
} }
return; return;
@@ -375,15 +398,30 @@ input_manager_process_key(struct input_manager *input_manager,
} }
struct control_msg msg; struct control_msg msg;
if (convert_input_key(event, &msg)) { if (convert_input_key(event, &msg, im->prefer_text)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'"); LOGW("Could not request 'inject keycode'");
} }
} }
} }
static bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
return true;
}
void void
input_manager_process_mouse_motion(struct input_manager *input_manager, input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event) { const SDL_MouseMotionEvent *event) {
if (!event->state) { if (!event->state) {
// do not send motion events when no button is pressed // do not send motion events when no button is pressed
@@ -394,33 +432,74 @@ input_manager_process_mouse_motion(struct input_manager *input_manager,
return; return;
} }
struct control_msg msg; struct control_msg msg;
if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) { if (convert_mouse_motion(event, im->screen, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'"); LOGW("Could not request 'inject mouse motion event'");
} }
} }
} }
static bool
convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
return false;
}
struct size frame_size = screen->frame_size;
to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = frame_size;
// SDL touch event coordinates are normalized in the range [0; 1]
to->inject_touch_event.position.point.x = from->x * frame_size.width;
to->inject_touch_event.position.point.y = from->y * frame_size.height;
to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0;
return true;
}
void void
input_manager_process_touch(struct input_manager *input_manager, input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event) { const SDL_TouchFingerEvent *event) {
struct control_msg msg; struct control_msg msg;
if (convert_touch(event, input_manager->screen->frame_size, &msg)) { if (convert_touch(event, im->screen, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject touch event'"); LOGW("Could not request 'inject touch event'");
} }
} }
} }
static bool static bool
is_outside_device_screen(struct input_manager *input_manager, int x, int y) is_outside_device_screen(struct input_manager *im, int x, int y)
{ {
return x < 0 || x >= input_manager->screen->frame_size.width || return x < 0 || x >= im->screen->frame_size.width ||
y < 0 || y >= input_manager->screen->frame_size.height; y < 0 || y >= im->screen->frame_size.height;
}
static bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point.x = from->x;
to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
return true;
} }
void void
input_manager_process_mouse_button(struct input_manager *input_manager, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event,
bool control) { bool control) {
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
@@ -429,19 +508,19 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
} }
if (event->type == SDL_MOUSEBUTTONDOWN) { if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) { if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller); press_back_or_turn_screen_on(im->controller);
return; return;
} }
if (control && event->button == SDL_BUTTON_MIDDLE) { if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP); action_home(im->controller, ACTION_DOWN | ACTION_UP);
return; return;
} }
// double-click on black borders resize to fit the device screen // double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside = bool outside =
is_outside_device_screen(input_manager, event->x, event->y); is_outside_device_screen(im, event->x, event->y);
if (outside) { if (outside) {
screen_resize_to_fit(input_manager->screen); screen_resize_to_fit(im->screen);
return; return;
} }
} }
@@ -453,23 +532,41 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
} }
struct control_msg msg; struct control_msg msg;
if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) { if (convert_mouse_button(event, im->screen, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'"); LOGW("Could not request 'inject mouse button event'");
} }
} }
} }
void static bool
input_manager_process_mouse_wheel(struct input_manager *input_manager, convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
const SDL_MouseWheelEvent *event) { struct control_msg *to) {
struct position position = { struct position position = {
.screen_size = input_manager->screen->frame_size, .screen_size = screen->frame_size,
.point = get_mouse_point(input_manager->screen), .point = get_mouse_point(screen),
}; };
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->inject_scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->inject_scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y;
return true;
}
void
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event) {
struct control_msg msg; struct control_msg msg;
if (convert_mouse_wheel(event, position, &msg)) { if (convert_mouse_wheel(event, im->screen, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'"); LOGW("Could not request 'inject mouse wheel event'");
} }
} }

View File

@@ -14,32 +14,33 @@ struct input_manager {
struct controller *controller; struct controller *controller;
struct video_buffer *video_buffer; struct video_buffer *video_buffer;
struct screen *screen; struct screen *screen;
bool prefer_text;
}; };
void void
input_manager_process_text_input(struct input_manager *input_manager, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event); const SDL_TextInputEvent *event);
void void
input_manager_process_key(struct input_manager *input_manager, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event,
bool control); bool control);
void void
input_manager_process_mouse_motion(struct input_manager *input_manager, input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event); const SDL_MouseMotionEvent *event);
void void
input_manager_process_touch(struct input_manager *input_manager, input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event); const SDL_TouchFingerEvent *event);
void void
input_manager_process_mouse_button(struct input_manager *input_manager, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event,
bool control); bool control);
void void
input_manager_process_mouse_wheel(struct input_manager *input_manager, input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event); const SDL_MouseWheelEvent *event);
#endif #endif

View File

@@ -14,24 +14,9 @@
#include "recorder.h" #include "recorder.h"
struct args { struct args {
const char *serial; struct scrcpy_options opts;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
bool fullscreen;
bool no_control;
bool no_display;
bool help; bool help;
bool version; bool version;
bool show_touches;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
}; };
static void usage(const char *arg0) { static void usage(const char *arg0) {
@@ -59,7 +44,7 @@ static void usage(const char *arg0) {
" -f, --fullscreen\n" " -f, --fullscreen\n"
" Start in fullscreen.\n" " Start in fullscreen.\n"
"\n" "\n"
" -F, --record-format\n" " -F, --record-format format\n"
" Force recording format (either mp4 or mkv).\n" " Force recording format (either mp4 or mkv).\n"
"\n" "\n"
" -h, --help\n" " -h, --help\n"
@@ -82,6 +67,13 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n" " Set the TCP port the client listens on.\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" --prefer-text\n"
" Inject alpha characters and space as text events instead of\n"
" key events.\n"
" This avoids issues when combining multiple keys to enter a\n"
" special character, but breaks the expected behavior of alpha\n"
" keys in games (typically WASD).\n"
"\n"
" --push-target path\n" " --push-target path\n"
" Set the target directory for pushing files to the device by\n" " Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n" " drag & drop. It is passed as-is to \"adb push\".\n"
@@ -312,50 +304,63 @@ guess_record_format(const char *filename) {
#define OPT_RENDER_EXPIRED_FRAMES 1000 #define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001 #define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002 #define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006
static bool static bool
parse_args(struct args *args, int argc, char *argv[]) { parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, 'T'}, {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'}, {"crop", required_argument, NULL, OPT_CROP},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'}, {"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'F'}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"render-expired-frames", no_argument, NULL, {"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES}, OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'}, {"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, {"window-title", required_argument, NULL,
OPT_WINDOW_TITLE}, OPT_WINDOW_TITLE},
{NULL, 0, NULL, 0 }, {NULL, 0, NULL, 0 },
}; };
struct scrcpy_options *opts = &args->opts;
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) { NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) { if (!parse_bit_rate(optarg, &opts->bit_rate)) {
return false; return false;
} }
break; break;
case 'c': case 'c':
args->crop = optarg; LOGW("Deprecated option -c. Use --crop instead.");
// fall through
case OPT_CROP:
opts->crop = optarg;
break; break;
case 'f': case 'f':
args->fullscreen = true; opts->fullscreen = true;
break; break;
case 'F': case 'F':
if (!parse_record_format(optarg, &args->record_format)) { LOGW("Deprecated option -F. Use --record-format instead.");
// fall through
case OPT_RECORD_FORMAT:
if (!parse_record_format(optarg, &opts->record_format)) {
return false; return false;
} }
break; break;
@@ -363,47 +368,53 @@ parse_args(struct args *args, int argc, char *argv[]) {
args->help = true; args->help = true;
break; break;
case 'm': case 'm':
if (!parse_max_size(optarg, &args->max_size)) { if (!parse_max_size(optarg, &opts->max_size)) {
return false; return false;
} }
break; break;
case 'n': case 'n':
args->no_control = true; opts->control = false;
break; break;
case 'N': case 'N':
args->no_display = true; opts->display = false;
break; break;
case 'p': case 'p':
if (!parse_port(optarg, &args->port)) { if (!parse_port(optarg, &opts->port)) {
return false; return false;
} }
break; break;
case 'r': case 'r':
args->record_filename = optarg; opts->record_filename = optarg;
break; break;
case 's': case 's':
args->serial = optarg; opts->serial = optarg;
break; break;
case 'S': case 'S':
args->turn_screen_off = true; opts->turn_screen_off = true;
break; break;
case 't': case 't':
args->show_touches = true; opts->show_touches = true;
break; break;
case 'T': case 'T':
args->always_on_top = true; LOGW("Deprecated option -T. Use --always-on-top instead.");
// fall through
case OPT_ALWAYS_ON_TOP:
opts->always_on_top = true;
break; break;
case 'v': case 'v':
args->version = true; args->version = true;
break; break;
case OPT_RENDER_EXPIRED_FRAMES: case OPT_RENDER_EXPIRED_FRAMES:
args->render_expired_frames = true; opts->render_expired_frames = true;
break; break;
case OPT_WINDOW_TITLE: case OPT_WINDOW_TITLE:
args->window_title = optarg; opts->window_title = optarg;
break; break;
case OPT_PUSH_TARGET: case OPT_PUSH_TARGET:
args->push_target = optarg; opts->push_target = optarg;
break;
case OPT_PREFER_TEXT:
opts->prefer_text = true;
break; break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
@@ -411,12 +422,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
} }
} }
if (args->no_display && !args->record_filename) { if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)"); LOGE("-N/--no-display requires screen recording (-r/--record)");
return false; return false;
} }
if (args->no_display && args->fullscreen) { if (!opts->display && opts->fullscreen) {
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
return false; return false;
} }
@@ -427,21 +438,21 @@ parse_args(struct args *args, int argc, char *argv[]) {
return false; return false;
} }
if (args->record_format && !args->record_filename) { if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording"); LOGE("Record format specified without recording");
return false; return false;
} }
if (args->record_filename && !args->record_format) { if (opts->record_filename && !opts->record_format) {
args->record_format = guess_record_format(args->record_filename); opts->record_format = guess_record_format(opts->record_filename);
if (!args->record_format) { if (!opts->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)", LOGE("No format specified for \"%s\" (try with -F mkv)",
args->record_filename); opts->record_filename);
return false; return false;
} }
} }
if (args->no_control && args->turn_screen_off) { if (!opts->control && opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled"); LOGE("Could not request to turn screen off if control is disabled");
return false; return false;
} }
@@ -458,24 +469,11 @@ main(int argc, char *argv[]) {
setbuf(stderr, NULL); setbuf(stderr, NULL);
#endif #endif
struct args args = { struct args args = {
.serial = NULL, .opts = SCRCPY_OPTIONS_DEFAULT,
.crop = NULL,
.record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0,
.help = false, .help = false,
.version = false, .version = false,
.show_touches = false,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
.always_on_top = false,
.no_control = false,
.no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
}; };
if (!parse_args(&args, argc, argv)) { if (!parse_args(&args, argc, argv)) {
return 1; return 1;
} }
@@ -504,25 +502,7 @@ main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif #endif
struct scrcpy_options options = { int res = scrcpy(&args.opts) ? 0 : 1;
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
.always_on_top = args.always_on_top,
.control = !args.no_control,
.display = !args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
};
int res = scrcpy(&options) ? 0 : 1;
avformat_network_deinit(); // ignore failure avformat_network_deinit(); // ignore failure

View File

@@ -135,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd> // <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
recorder->ctx->oformat = (AVOutputFormat *) format; recorder->ctx->oformat = (AVOutputFormat *) format;
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) { if (!ostream) {
avformat_free_context(recorder->ctx); avformat_free_context(recorder->ctx);
@@ -171,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
void void
recorder_close(struct recorder *recorder) { recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx); if (recorder->header_written) {
if (ret < 0) { int ret = av_write_trailer(recorder->ctx);
LOGE("Failed to write trailer to %s", recorder->filename); if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true; recorder->failed = true;
} }
avio_close(recorder->ctx->pb); avio_close(recorder->ctx->pb);

View File

@@ -11,7 +11,8 @@
#include "queue.h" #include "queue.h"
enum recorder_format { enum recorder_format {
RECORDER_FORMAT_MP4 = 1, RECORDER_FORMAT_AUTO,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV, RECORDER_FORMAT_MKV,
}; };

View File

@@ -42,6 +42,7 @@ static struct input_manager input_manager = {
.controller = &controller, .controller = &controller,
.video_buffer = &video_buffer, .video_buffer = &video_buffer,
.screen = &screen, .screen = &screen,
.prefer_text = false, // initialized later
}; };
// init SDL and set appropriate hints // init SDL and set appropriate hints
@@ -217,6 +218,7 @@ event_loop(bool display, bool control) {
case EVENT_RESULT_STOPPED_BY_USER: case EVENT_RESULT_STOPPED_BY_USER:
return true; return true;
case EVENT_RESULT_STOPPED_BY_EOS: case EVENT_RESULT_STOPPED_BY_EOS:
LOGW("Device disconnected");
return false; return false;
case EVENT_RESULT_CONTINUE: case EVENT_RESULT_CONTINUE:
break; break;
@@ -414,6 +416,8 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = true; show_touches_waited = true;
} }
input_manager.prefer_text = options->prefer_text;
ret = event_loop(options->display, options->control); ret = event_loop(options->display, options->control);
LOGD("quit..."); LOGD("quit...");

View File

@@ -3,9 +3,10 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <recorder.h>
#include "config.h" #include "config.h"
#include "input_manager.h"
#include "recorder.h"
struct scrcpy_options { struct scrcpy_options {
const char *serial; const char *serial;
@@ -24,8 +25,29 @@ struct scrcpy_options {
bool display; bool display;
bool turn_screen_off; bool turn_screen_off;
bool render_expired_frames; bool render_expired_frames;
bool prefer_text;
}; };
#define SCRCPY_OPTIONS_DEFAULT { \
.serial = NULL, \
.crop = NULL, \
.record_filename = NULL, \
.window_title = NULL, \
.push_target = NULL, \
.record_format = RECORDER_FORMAT_AUTO, \
.port = DEFAULT_LOCAL_PORT, \
.max_size = DEFAULT_LOCAL_PORT, \
.bit_rate = DEFAULT_BIT_RATE, \
.show_touches = false, \
.fullscreen = false, \
.always_on_top = false, \
.control = true, \
.display = true, \
.turn_screen_off = false, \
.render_expired_frames = false, \
.prefer_text = false, \
}
bool bool
scrcpy(const struct scrcpy_options *options); scrcpy(const struct scrcpy_options *options);

View File

@@ -16,7 +16,7 @@
// get the window size in a struct size // get the window size in a struct size
static struct size static struct size
get_native_window_size(SDL_Window *window) { get_window_size(SDL_Window *window) {
int width; int width;
int height; int height;
SDL_GetWindowSize(window, &width, &height); SDL_GetWindowSize(window, &width, &height);
@@ -29,11 +29,11 @@ get_native_window_size(SDL_Window *window) {
// get the windowed window size // get the windowed window size
static struct size static struct size
get_window_size(const struct screen *screen) { get_windowed_window_size(const struct screen *screen) {
if (screen->fullscreen) { if (screen->fullscreen) {
return screen->windowed_window_size; return screen->windowed_window_size;
} }
return get_native_window_size(screen->window); return get_window_size(screen->window);
} }
// set the window size to be applied when fullscreen is disabled // set the window size to be applied when fullscreen is disabled
@@ -112,8 +112,8 @@ get_optimal_size(struct size current_size, struct size frame_size) {
// same as get_optimal_size(), but read the current size from the window // same as get_optimal_size(), but read the current size from the window
static inline struct size static inline struct size
get_optimal_window_size(const struct screen *screen, struct size frame_size) { get_optimal_window_size(const struct screen *screen, struct size frame_size) {
struct size current_size = get_window_size(screen); struct size windowed_size = get_windowed_window_size(screen);
return get_optimal_size(current_size, frame_size); return get_optimal_size(windowed_size, frame_size);
} }
// initially, there is no current size, so use the frame size as current size // initially, there is no current size, so use the frame size as current size
@@ -229,11 +229,11 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) {
// frame dimension changed, destroy texture // frame dimension changed, destroy texture
SDL_DestroyTexture(screen->texture); SDL_DestroyTexture(screen->texture);
struct size current_size = get_window_size(screen); struct size windowed_size = get_windowed_window_size(screen);
struct size target_size = { struct size target_size = {
(uint32_t) current_size.width * new_frame_size.width (uint32_t) windowed_size.width * new_frame_size.width
/ screen->frame_size.width, / screen->frame_size.width,
(uint32_t) current_size.height * new_frame_size.height (uint32_t) windowed_size.height * new_frame_size.height
/ screen->frame_size.height, / screen->frame_size.height,
}; };
target_size = get_optimal_size(target_size, new_frame_size); target_size = get_optimal_size(target_size, new_frame_size);
@@ -289,7 +289,7 @@ void
screen_switch_fullscreen(struct screen *screen) { screen_switch_fullscreen(struct screen *screen) {
if (!screen->fullscreen) { if (!screen->fullscreen) {
// going to fullscreen, store the current windowed window size // going to fullscreen, store the current windowed window size
screen->windowed_window_size = get_native_window_size(screen->window); screen->windowed_window_size = get_window_size(screen->window);
} }
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) { if (SDL_SetWindowFullscreen(screen->window, new_mode)) {

View File

@@ -13,7 +13,7 @@
#include "net.h" #include "net.h"
#define SOCKET_NAME "scrcpy" #define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar" #define SERVER_FILENAME "scrcpy-server"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
@@ -32,7 +32,7 @@ get_server_path(void) {
// the absolute path is hardcoded // the absolute path is hardcoded
return DEFAULT_SERVER_PATH; return DEFAULT_SERVER_PATH;
#else #else
// use scrcpy-server.jar in the same directory as the executable // use scrcpy-server in the same directory as the executable
char *executable_path = get_executable_path(); char *executable_path = get_executable_path();
if (!executable_path) { if (!executable_path) {
LOGE("Could not get executable path, " LOGE("Could not get executable path, "
@@ -124,8 +124,14 @@ execute_server(struct server *server, const struct server_params *params) {
"shell", "shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
"app_process", "app_process",
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
SERVER_DEBUGGER_PORT,
#endif
"/", // unused "/", // unused
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
server->tunnel_forward ? "true" : "false", server->tunnel_forward ? "true" : "false",
@@ -133,6 +139,17 @@ execute_server(struct server *server, const struct server_params *params) {
"true", // always send frame meta (packet boundaries + timestamp) "true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false", params->control ? "true" : "false",
}; };
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
// From the computer, run
// adb forward tcp:5005 tcp:5005
// Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with:
// Host: localhost
// Port: 5005
// Then click on "Debug"
#endif
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
} }

View File

@@ -3,5 +3,6 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')

View File

@@ -23,21 +23,21 @@ cd -
make -f Makefile.CrossWindows make -f Makefile.CrossWindows
# the generated server must be the same everywhere # the generated server must be the same everywhere
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server
# get version name # get version name
TAG=$(git describe --tags --always) TAG=$(git describe --tags --always)
# create release directory # create release directory
mkdir -p "release-$TAG" mkdir -p "release-$TAG"
cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar" cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG"
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
# generate checksums # generate checksums
cd "release-$TAG" cd "release-$TAG"
sha256sum "scrcpy-server-$TAG.jar" \ sha256sum "scrcpy-server-$TAG" \
"scrcpy-win32-$TAG.zip" \ "scrcpy-win32-$TAG.zip" \
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt

2
run
View File

@@ -20,4 +20,4 @@ then
exit 1 exit 1
fi fi
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@" SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@"

View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy" SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"

62
server/build_without_gradle.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
#
# This script generates the scrcpy binary "manually" (without gradle).
#
# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and
# ANDROID_BUILD_TOOLS environment variables).
#
# Then execute:
#
# BUILD_DIR=my_build_dir ./build_without_gradle.sh
set -e
PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy;
public final class BuildConfig {
public static final boolean DEBUG = false;
}
EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o "$CLASSES_DIR" \
android/view/IRotationWatcher.aidl
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \
-cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
echo "Dexing..."
cd "$CLASSES_DIR"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \
android/view/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
echo "Archiving..."
cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View File

@@ -4,7 +4,7 @@ prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == '' if prebuilt_server == ''
custom_target('scrcpy-server', custom_target('scrcpy-server',
build_always: true, # gradle is responsible for tracking source changes build_always: true, # gradle is responsible for tracking source changes
output: 'scrcpy-server.jar', output: 'scrcpy-server',
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
console: true, console: true,
install: true, install: true,
@@ -16,7 +16,7 @@ else
endif endif
custom_target('scrcpy-server-prebuilt', custom_target('scrcpy-server-prebuilt',
input: prebuilt_server, input: prebuilt_server,
output: 'scrcpy-server.jar', output: 'scrcpy-server',
command: ['cp', '@INPUT@', '@OUTPUT@'], command: ['cp', '@INPUT@', '@OUTPUT@'],
install: true, install: true,
install_dir: 'share/scrcpy') install_dir: 'share/scrcpy')

View File

@@ -82,7 +82,7 @@ public class Controller {
injectText(msg.getText()); injectText(msg.getText());
break; break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT: case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0); injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
break; break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
@@ -177,7 +177,7 @@ public class Controller {
} }
MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties,
pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); pointerCoords, 0, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event); return injectEvent(event);
} }

View File

@@ -7,6 +7,7 @@ import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper;
import android.view.Surface; import android.view.Surface;
import java.io.FileDescriptor; import java.io.FileDescriptor;
@@ -54,6 +55,16 @@ public class ScreenEncoder implements Device.RotationListener {
} }
public void streamScreen(Device device, FileDescriptor fd) throws IOException { public void streamScreen(Device device, FileDescriptor fd) throws IOException {
// Some devices internally create a Handler when creating an input Surface, causing an exception:
// "Can't create handler inside thread that has not called Looper.prepare()"
// <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();
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;

View File

@@ -7,7 +7,7 @@ import java.io.IOException;
public final class Server { public final class Server {
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server";
private Server() { private Server() {
// not instantiable // not instantiable
@@ -67,29 +67,39 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber") @SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) { private static Options createOptions(String... args) {
if (args.length != 6) { if (args.length < 1) {
throw new IllegalArgumentException("Expecting 6 parameters"); throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException("The server version (" + clientVersion + ") does not match the client "
+ "(" + BuildConfig.VERSION_NAME + ")");
}
if (args.length != 7) {
throw new IllegalArgumentException("Expecting 7 parameters");
} }
Options options = new Options(); Options options = new Options();
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[1]); int bitRate = Integer.parseInt(args[2]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[2]); boolean tunnelForward = Boolean.parseBoolean(args[3]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[3]); Rect crop = parseCrop(args[4]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[4]); boolean sendFrameMeta = Boolean.parseBoolean(args[5]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[5]); boolean control = Boolean.parseBoolean(args[6]);
options.setControl(control); options.setControl(control);
return options; return options;