Compare commits
2 Commits
maximized.
...
virtualfin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2d0dcd002 | ||
|
|
9448eae8a4 |
14
BUILD.md
14
BUILD.md
@@ -195,7 +195,8 @@ Then, build:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
meson x --buildtype release --strip -Db_lto=true
|
meson x --buildtype release --strip -Db_lto=true
|
||||||
ninja -Cx
|
cd x
|
||||||
|
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
|
||||||
@@ -218,13 +219,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 -Cx install # without sudo on Windows
|
sudo ninja 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`
|
- `/usr/local/share/scrcpy/scrcpy-server.jar`
|
||||||
|
|
||||||
Just remove them to "uninstall" the application.
|
Just remove them to "uninstall" the application.
|
||||||
|
|
||||||
@@ -243,7 +244,8 @@ 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
|
-Dprebuilt_server=/path/to/scrcpy-server.jar
|
||||||
ninja -Cx
|
cd x
|
||||||
sudo ninja -Cx install
|
ninja
|
||||||
|
sudo ninja install
|
||||||
```
|
```
|
||||||
|
|||||||
34
DEVELOP.md
34
DEVELOP.md
@@ -3,7 +3,7 @@
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This application is composed of two parts:
|
This application is composed of two parts:
|
||||||
- the server (`scrcpy-server`), to be executed on the device,
|
- the server (`scrcpy-server.jar`), 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`).
|
`scrcpy-server.jar`).
|
||||||
|
|
||||||
[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,33 +268,3 @@ 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_.
|
|
||||||
|
|||||||
@@ -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).
|
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar).
|
||||||
#
|
#
|
||||||
# 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 "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp "$(WIN32_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 "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp "$(WIN64_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)/"
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ 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.
|
||||||
@@ -225,6 +226,7 @@ The window of app can always be above others by:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --always-on-top
|
scrcpy --always-on-top
|
||||||
|
scrcpy -T # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -356,7 +358,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` file, configure its path in
|
To override the path of the `scrcpy-server.jar` 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
|
||||||
|
|||||||
@@ -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 accessible from the same
|
# build a "portable" version (with scrcpy-server.jar 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,9 +115,6 @@ 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')
|
||||||
@@ -137,8 +134,6 @@ 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
238
app/scrcpy.1
@@ -1,238 +0,0 @@
|
|||||||
.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
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
|
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
|
||||||
|
|
||||||
#define POINTER_ID_MOUSE UINT64_C(-1);
|
#define POINTER_ID_MOUSE UINT64_C(-1);
|
||||||
|
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2);
|
||||||
|
|
||||||
enum control_msg_type {
|
enum control_msg_type {
|
||||||
CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
bool
|
static 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum android_metastate
|
static 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,9 +74,8 @@ convert_meta_state(SDL_Keymod mod) {
|
|||||||
return autocomplete_metastate(metastate);
|
return autocomplete_metastate(metastate);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
static 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);
|
||||||
@@ -93,12 +92,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -135,7 +128,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum android_motionevent_buttons
|
static 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) {
|
||||||
@@ -147,16 +140,34 @@ 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_X1MASK) {
|
if (state & SDL_BUTTON_X1) {
|
||||||
buttons |= AMOTION_EVENT_BUTTON_BACK;
|
buttons |= AMOTION_EVENT_BUTTON_BACK;
|
||||||
}
|
}
|
||||||
if (state & SDL_BUTTON_X2MASK) {
|
if (state & SDL_BUTTON_X2) {
|
||||||
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);
|
||||||
@@ -166,6 +177,41 @@ 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);
|
||||||
@@ -174,3 +220,39 @@ 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,23 +7,36 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "control_msg.h"
|
#include "control_msg.h"
|
||||||
|
|
||||||
bool
|
struct complete_mouse_motion_event {
|
||||||
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to);
|
SDL_MouseMotionEvent *mouse_motion_event;
|
||||||
|
struct size screen_size;
|
||||||
|
};
|
||||||
|
|
||||||
enum android_metastate
|
struct complete_mouse_wheel_event {
|
||||||
convert_meta_state(SDL_Keymod mod);
|
SDL_MouseWheelEvent *mouse_wheel_event;
|
||||||
|
struct point position;
|
||||||
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to);
|
||||||
bool prefer_text);
|
|
||||||
|
|
||||||
enum android_motionevent_buttons
|
|
||||||
convert_mouse_buttons(uint32_t state);
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to);
|
convert_mouse_button(const SDL_MouseButtonEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
|
// the video size may be different from the real device size, so we need the
|
||||||
|
// size to which the absolute position apply, to scale it accordingly
|
||||||
|
bool
|
||||||
|
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to);
|
convert_touch(const SDL_TouchFingerEvent *from, struct size screen_size,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
|
// on Android, a scroll event requires the current mouse position
|
||||||
|
bool
|
||||||
|
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct position position,
|
||||||
|
struct control_msg *to);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,6 +7,19 @@
|
|||||||
#include "lock_util.h"
|
#include "lock_util.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
|
||||||
|
void
|
||||||
|
input_manager_init(struct input_manager *input_manager,
|
||||||
|
struct controller *controller,
|
||||||
|
struct video_buffer *video_buffer,
|
||||||
|
struct screen *screen) {
|
||||||
|
input_manager->controller = controller;
|
||||||
|
input_manager->video_buffer = video_buffer;
|
||||||
|
input_manager->screen = screen;
|
||||||
|
|
||||||
|
input_manager->ctrl_down = false;
|
||||||
|
input_manager->vfinger.down = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
||||||
// coordinates (as provided in SDL mouse events)
|
// coordinates (as provided in SDL mouse events)
|
||||||
//
|
//
|
||||||
@@ -211,18 +224,35 @@ clipboard_paste(struct controller *controller) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
simulate_virtual_finger(struct input_manager *input_manager, bool down,
|
||||||
|
struct position *position) {
|
||||||
|
SDL_assert(input_manager->vfinger.down != down);
|
||||||
|
input_manager->vfinger.down = down;
|
||||||
|
|
||||||
|
struct control_msg msg;
|
||||||
|
msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
|
||||||
|
msg.inject_touch_event.action = down ? AMOTION_EVENT_ACTION_DOWN
|
||||||
|
: AMOTION_EVENT_ACTION_UP;
|
||||||
|
msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER;
|
||||||
|
msg.inject_touch_event.position = *position;
|
||||||
|
msg.inject_touch_event.pressure = 1.f;
|
||||||
|
msg.inject_touch_event.buttons = 0;
|
||||||
|
|
||||||
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
|
LOGW("Could not request 'inject virtual finger event'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_text_input(struct input_manager *im,
|
input_manager_process_text_input(struct input_manager *input_manager,
|
||||||
const SDL_TextInputEvent *event) {
|
const SDL_TextInputEvent *event) {
|
||||||
if (!im->prefer_text) {
|
|
||||||
char c = event->text[0];
|
char c = event->text[0];
|
||||||
if (isalpha(c) || c == ' ') {
|
if (isalpha(c) || c == ' ') {
|
||||||
SDL_assert(event->text[1] == '\0');
|
SDL_assert(event->text[1] == '\0');
|
||||||
// letters and space are handled as raw key event
|
// letters and space are handled as raw key event
|
||||||
return;
|
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);
|
||||||
@@ -230,34 +260,14 @@ input_manager_process_text_input(struct input_manager *im,
|
|||||||
LOGW("Could not strdup input text");
|
LOGW("Could not strdup input text");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->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 *im,
|
input_manager_process_key(struct input_manager *input_manager,
|
||||||
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
|
||||||
@@ -267,6 +277,14 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
|
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
|
||||||
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
|
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
|
||||||
|
|
||||||
|
// store the Ctrl state to modify mouse events
|
||||||
|
input_manager->ctrl_down = ctrl;
|
||||||
|
|
||||||
|
if (input_manager->vfinger.down && !ctrl) {
|
||||||
|
simulate_virtual_finger(input_manager, false,
|
||||||
|
&input_manager->vfinger.position);
|
||||||
|
}
|
||||||
|
|
||||||
// use Cmd on macOS, Ctrl on other platforms
|
// use Cmd on macOS, Ctrl on other platforms
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
bool cmd = !ctrl && meta;
|
bool cmd = !ctrl && meta;
|
||||||
@@ -284,7 +302,7 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct controller *controller = im->controller;
|
struct controller *controller = input_manager->controller;
|
||||||
|
|
||||||
// capture all Ctrl events
|
// capture all Ctrl events
|
||||||
if (ctrl || cmd) {
|
if (ctrl || cmd) {
|
||||||
@@ -359,23 +377,23 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
case SDLK_f:
|
case SDLK_f:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_switch_fullscreen(im->screen);
|
screen_switch_fullscreen(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_x:
|
case SDLK_x:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_resize_to_fit(im->screen);
|
screen_resize_to_fit(input_manager->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_g:
|
case SDLK_g:
|
||||||
if (!shift && cmd && !repeat && down) {
|
if (!shift && cmd && !repeat && down) {
|
||||||
screen_resize_to_pixel_perfect(im->screen);
|
screen_resize_to_pixel_perfect(input_manager->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 =
|
||||||
im->video_buffer->fps_counter;
|
input_manager->video_buffer->fps_counter;
|
||||||
switch_fps_counter_state(fps_counter);
|
switch_fps_counter_state(fps_counter);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -398,30 +416,15 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_input_key(event, &msg, im->prefer_text)) {
|
if (convert_input_key(event, &msg)) {
|
||||||
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 *im,
|
input_manager_process_mouse_motion(struct input_manager *input_manager,
|
||||||
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
|
||||||
@@ -432,98 +435,70 @@ input_manager_process_mouse_motion(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_mouse_motion(event, im->screen, &msg)) {
|
if (convert_mouse_motion(event, input_manager->screen->frame_size, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->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 *im,
|
input_manager_process_touch(struct input_manager *input_manager,
|
||||||
const SDL_TouchFingerEvent *event) {
|
const SDL_TouchFingerEvent *event) {
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_touch(event, im->screen, &msg)) {
|
if (convert_touch(event, input_manager->screen->frame_size, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->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 *im, int x, int y)
|
is_outside_device_screen(struct input_manager *input_manager, int x, int y)
|
||||||
{
|
{
|
||||||
return x < 0 || x >= im->screen->frame_size.width ||
|
return x < 0 || x >= input_manager->screen->frame_size.width ||
|
||||||
y < 0 || y >= im->screen->frame_size.height;
|
y < 0 || y >= input_manager->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 *im,
|
input_manager_process_mouse_button(struct input_manager *input_manager,
|
||||||
const SDL_MouseButtonEvent *event,
|
const SDL_MouseButtonEvent *event,
|
||||||
bool control) {
|
bool control) {
|
||||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
// simulated from touch events, so it's a duplicate
|
// simulated from touch events, so it's a duplicate
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(im->controller);
|
press_back_or_turn_screen_on(input_manager->controller);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
||||||
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
action_home(input_manager->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) {
|
||||||
|
if (event->clicks >= 2) {
|
||||||
bool outside =
|
bool outside =
|
||||||
is_outside_device_screen(im, event->x, event->y);
|
is_outside_device_screen(input_manager, event->x, event->y);
|
||||||
if (outside) {
|
if (outside) {
|
||||||
screen_resize_to_fit(im->screen);
|
screen_resize_to_fit(input_manager->screen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct virtual_finger *vfinger = &input_manager->vfinger;
|
||||||
|
if (input_manager->ctrl_down && !vfinger->down) {
|
||||||
|
vfinger->position.point.x = event->x;
|
||||||
|
vfinger->position.point.y = event->y;
|
||||||
|
vfinger->position.screen_size =
|
||||||
|
input_manager->screen->frame_size,
|
||||||
|
simulate_virtual_finger(input_manager, true,
|
||||||
|
&vfinger->position);
|
||||||
|
}
|
||||||
|
}
|
||||||
// otherwise, send the click event to the device
|
// otherwise, send the click event to the device
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,41 +507,23 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_mouse_button(event, im->screen, &msg)) {
|
if (convert_mouse_button(event, input_manager->screen->frame_size, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Could not request 'inject mouse button event'");
|
LOGW("Could not request 'inject mouse button event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
|
||||||
struct control_msg *to) {
|
|
||||||
struct position position = {
|
|
||||||
.screen_size = screen->frame_size,
|
|
||||||
.point = get_mouse_point(screen),
|
|
||||||
};
|
|
||||||
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
|
||||||
|
|
||||||
to->inject_scroll_event.position = position;
|
|
||||||
|
|
||||||
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
|
void
|
||||||
input_manager_process_mouse_wheel(struct input_manager *im,
|
input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
||||||
const SDL_MouseWheelEvent *event) {
|
const SDL_MouseWheelEvent *event) {
|
||||||
|
struct position position = {
|
||||||
|
.screen_size = input_manager->screen->frame_size,
|
||||||
|
.point = get_mouse_point(input_manager->screen),
|
||||||
|
};
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_mouse_wheel(event, im->screen, &msg)) {
|
if (convert_mouse_wheel(event, position, &msg)) {
|
||||||
if (!controller_push_msg(im->controller, &msg)) {
|
if (!controller_push_msg(input_manager->controller, &msg)) {
|
||||||
LOGW("Could not request 'inject mouse wheel event'");
|
LOGW("Could not request 'inject mouse wheel event'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,33 +14,45 @@ 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;
|
|
||||||
|
bool ctrl_down;
|
||||||
|
|
||||||
|
struct virtual_finger {
|
||||||
|
bool down;
|
||||||
|
struct position position;
|
||||||
|
} vfinger;
|
||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_text_input(struct input_manager *im,
|
input_manager_init(struct input_manager *input_manager,
|
||||||
|
struct controller *controller,
|
||||||
|
struct video_buffer *video_buffer,
|
||||||
|
struct screen *screen);
|
||||||
|
|
||||||
|
void
|
||||||
|
input_manager_process_text_input(struct input_manager *input_manager,
|
||||||
const SDL_TextInputEvent *event);
|
const SDL_TextInputEvent *event);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_key(struct input_manager *im,
|
input_manager_process_key(struct input_manager *input_manager,
|
||||||
const SDL_KeyboardEvent *event,
|
const SDL_KeyboardEvent *event,
|
||||||
bool control);
|
bool control);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_motion(struct input_manager *im,
|
input_manager_process_mouse_motion(struct input_manager *input_manager,
|
||||||
const SDL_MouseMotionEvent *event);
|
const SDL_MouseMotionEvent *event);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_touch(struct input_manager *im,
|
input_manager_process_touch(struct input_manager *input_manager,
|
||||||
const SDL_TouchFingerEvent *event);
|
const SDL_TouchFingerEvent *event);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_button(struct input_manager *im,
|
input_manager_process_mouse_button(struct input_manager *input_manager,
|
||||||
const SDL_MouseButtonEvent *event,
|
const SDL_MouseButtonEvent *event,
|
||||||
bool control);
|
bool control);
|
||||||
|
|
||||||
void
|
void
|
||||||
input_manager_process_mouse_wheel(struct input_manager *im,
|
input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
||||||
const SDL_MouseWheelEvent *event);
|
const SDL_MouseWheelEvent *event);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
140
app/src/main.c
140
app/src/main.c
@@ -14,9 +14,24 @@
|
|||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
|
||||||
struct args {
|
struct args {
|
||||||
struct scrcpy_options opts;
|
const char *serial;
|
||||||
|
const char *crop;
|
||||||
|
const char *record_filename;
|
||||||
|
const char *window_title;
|
||||||
|
const char *push_target;
|
||||||
|
enum recorder_format record_format;
|
||||||
|
bool fullscreen;
|
||||||
|
bool no_control;
|
||||||
|
bool no_display;
|
||||||
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) {
|
||||||
@@ -44,7 +59,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 format\n"
|
" -F, --record-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"
|
||||||
@@ -67,13 +82,6 @@ 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"
|
||||||
@@ -304,63 +312,50 @@ 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, OPT_ALWAYS_ON_TOP},
|
{"always-on-top", no_argument, NULL, 'T'},
|
||||||
{"bit-rate", required_argument, NULL, 'b'},
|
{"bit-rate", required_argument, NULL, 'b'},
|
||||||
{"crop", required_argument, NULL, OPT_CROP},
|
{"crop", required_argument, NULL, 'c'},
|
||||||
{"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, OPT_PUSH_TARGET},
|
{"push-target", required_argument, NULL,
|
||||||
|
OPT_PUSH_TARGET},
|
||||||
{"record", required_argument, NULL, 'r'},
|
{"record", required_argument, NULL, 'r'},
|
||||||
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
|
{"record-format", required_argument, NULL, 'F'},
|
||||||
{"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, &opts->bit_rate)) {
|
if (!parse_bit_rate(optarg, &args->bit_rate)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'c':
|
case 'c':
|
||||||
LOGW("Deprecated option -c. Use --crop instead.");
|
args->crop = optarg;
|
||||||
// fall through
|
|
||||||
case OPT_CROP:
|
|
||||||
opts->crop = optarg;
|
|
||||||
break;
|
break;
|
||||||
case 'f':
|
case 'f':
|
||||||
opts->fullscreen = true;
|
args->fullscreen = true;
|
||||||
break;
|
break;
|
||||||
case 'F':
|
case 'F':
|
||||||
LOGW("Deprecated option -F. Use --record-format instead.");
|
if (!parse_record_format(optarg, &args->record_format)) {
|
||||||
// fall through
|
|
||||||
case OPT_RECORD_FORMAT:
|
|
||||||
if (!parse_record_format(optarg, &opts->record_format)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -368,53 +363,47 @@ 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, &opts->max_size)) {
|
if (!parse_max_size(optarg, &args->max_size)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
opts->control = false;
|
args->no_control = true;
|
||||||
break;
|
break;
|
||||||
case 'N':
|
case 'N':
|
||||||
opts->display = false;
|
args->no_display = true;
|
||||||
break;
|
break;
|
||||||
case 'p':
|
case 'p':
|
||||||
if (!parse_port(optarg, &opts->port)) {
|
if (!parse_port(optarg, &args->port)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'r':
|
case 'r':
|
||||||
opts->record_filename = optarg;
|
args->record_filename = optarg;
|
||||||
break;
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
opts->serial = optarg;
|
args->serial = optarg;
|
||||||
break;
|
break;
|
||||||
case 'S':
|
case 'S':
|
||||||
opts->turn_screen_off = true;
|
args->turn_screen_off = true;
|
||||||
break;
|
break;
|
||||||
case 't':
|
case 't':
|
||||||
opts->show_touches = true;
|
args->show_touches = true;
|
||||||
break;
|
break;
|
||||||
case 'T':
|
case 'T':
|
||||||
LOGW("Deprecated option -T. Use --always-on-top instead.");
|
args->always_on_top = true;
|
||||||
// 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:
|
||||||
opts->render_expired_frames = true;
|
args->render_expired_frames = true;
|
||||||
break;
|
break;
|
||||||
case OPT_WINDOW_TITLE:
|
case OPT_WINDOW_TITLE:
|
||||||
opts->window_title = optarg;
|
args->window_title = optarg;
|
||||||
break;
|
break;
|
||||||
case OPT_PUSH_TARGET:
|
case OPT_PUSH_TARGET:
|
||||||
opts->push_target = optarg;
|
args->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
|
||||||
@@ -422,12 +411,12 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->display && !opts->record_filename) {
|
if (args->no_display && !args->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 (!opts->display && opts->fullscreen) {
|
if (args->no_display && args->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;
|
||||||
}
|
}
|
||||||
@@ -438,21 +427,21 @@ parse_args(struct args *args, int argc, char *argv[]) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->record_format && !opts->record_filename) {
|
if (args->record_format && !args->record_filename) {
|
||||||
LOGE("Record format specified without recording");
|
LOGE("Record format specified without recording");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->record_filename && !opts->record_format) {
|
if (args->record_filename && !args->record_format) {
|
||||||
opts->record_format = guess_record_format(opts->record_filename);
|
args->record_format = guess_record_format(args->record_filename);
|
||||||
if (!opts->record_format) {
|
if (!args->record_format) {
|
||||||
LOGE("No format specified for \"%s\" (try with -F mkv)",
|
LOGE("No format specified for \"%s\" (try with -F mkv)",
|
||||||
opts->record_filename);
|
args->record_filename);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->control && opts->turn_screen_off) {
|
if (args->no_control && args->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;
|
||||||
}
|
}
|
||||||
@@ -469,11 +458,24 @@ main(int argc, char *argv[]) {
|
|||||||
setbuf(stderr, NULL);
|
setbuf(stderr, NULL);
|
||||||
#endif
|
#endif
|
||||||
struct args args = {
|
struct args args = {
|
||||||
.opts = SCRCPY_OPTIONS_DEFAULT,
|
.serial = NULL,
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
@@ -502,7 +504,25 @@ main(int argc, char *argv[]) {
|
|||||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int res = scrcpy(&args.opts) ? 0 : 1;
|
struct scrcpy_options options = {
|
||||||
|
.serial = args.serial,
|
||||||
|
.crop = args.crop,
|
||||||
|
.port = args.port,
|
||||||
|
.record_filename = args.record_filename,
|
||||||
|
.window_title = args.window_title,
|
||||||
|
.push_target = args.push_target,
|
||||||
|
.record_format = args.record_format,
|
||||||
|
.max_size = args.max_size,
|
||||||
|
.bit_rate = args.bit_rate,
|
||||||
|
.show_touches = args.show_touches,
|
||||||
|
.fullscreen = args.fullscreen,
|
||||||
|
.always_on_top = args.always_on_top,
|
||||||
|
.control = !args.no_control,
|
||||||
|
.display = !args.no_display,
|
||||||
|
.turn_screen_off = args.turn_screen_off,
|
||||||
|
.render_expired_frames = args.render_expired_frames,
|
||||||
|
};
|
||||||
|
int res = scrcpy(&options) ? 0 : 1;
|
||||||
|
|
||||||
avformat_network_deinit(); // ignore failure
|
avformat_network_deinit(); // ignore failure
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,6 @@ 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);
|
||||||
@@ -174,16 +171,11 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
recorder_close(struct recorder *recorder) {
|
recorder_close(struct recorder *recorder) {
|
||||||
if (recorder->header_written) {
|
|
||||||
int ret = av_write_trailer(recorder->ctx);
|
int ret = av_write_trailer(recorder->ctx);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||||
recorder->failed = true;
|
recorder->failed = true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// the recorded file is empty
|
|
||||||
recorder->failed = true;
|
|
||||||
}
|
|
||||||
avio_close(recorder->ctx->pb);
|
avio_close(recorder->ctx->pb);
|
||||||
avformat_free_context(recorder->ctx);
|
avformat_free_context(recorder->ctx);
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
|
|
||||||
enum recorder_format {
|
enum recorder_format {
|
||||||
RECORDER_FORMAT_AUTO,
|
RECORDER_FORMAT_MP4 = 1,
|
||||||
RECORDER_FORMAT_MP4,
|
|
||||||
RECORDER_FORMAT_MKV,
|
RECORDER_FORMAT_MKV,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,7 @@ static struct decoder decoder;
|
|||||||
static struct recorder recorder;
|
static struct recorder recorder;
|
||||||
static struct controller controller;
|
static struct controller controller;
|
||||||
static struct file_handler file_handler;
|
static struct file_handler file_handler;
|
||||||
|
static struct input_manager input_manager;
|
||||||
static struct input_manager input_manager = {
|
|
||||||
.controller = &controller,
|
|
||||||
.video_buffer = &video_buffer,
|
|
||||||
.screen = &screen,
|
|
||||||
.prefer_text = false, // initialized later
|
|
||||||
};
|
|
||||||
|
|
||||||
// init SDL and set appropriate hints
|
// init SDL and set appropriate hints
|
||||||
static bool
|
static bool
|
||||||
@@ -144,7 +138,12 @@ handle_event(SDL_Event *event, bool control) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
screen_handle_window_event(&screen, &event->window);
|
switch (event->window.event) {
|
||||||
|
case SDL_WINDOWEVENT_EXPOSED:
|
||||||
|
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||||
|
screen_render(&screen);
|
||||||
|
break;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_TEXTINPUT:
|
case SDL_TEXTINPUT:
|
||||||
if (!control) {
|
if (!control) {
|
||||||
@@ -213,7 +212,6 @@ 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;
|
||||||
@@ -308,6 +306,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input_manager_init(&input_manager, &controller, &video_buffer, &screen);
|
||||||
|
|
||||||
if (!server_connect_to(&server)) {
|
if (!server_connect_to(&server)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
@@ -411,8 +411,6 @@ 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...");
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
|
|
||||||
#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;
|
||||||
@@ -25,29 +24,8 @@ 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);
|
||||||
|
|
||||||
|
|||||||
114
app/src/screen.c
114
app/src/screen.c
@@ -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_window_size(SDL_Window *window) {
|
get_native_window_size(SDL_Window *window) {
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
SDL_GetWindowSize(window, &width, &height);
|
SDL_GetWindowSize(window, &width, &height);
|
||||||
@@ -29,20 +29,11 @@ get_window_size(SDL_Window *window) {
|
|||||||
|
|
||||||
// get the windowed window size
|
// get the windowed window size
|
||||||
static struct size
|
static struct size
|
||||||
get_windowed_window_size(const struct screen *screen) {
|
get_window_size(const struct screen *screen) {
|
||||||
if (screen->fullscreen || screen->maximized) {
|
if (screen->fullscreen) {
|
||||||
return screen->windowed_window_size;
|
return screen->windowed_window_size;
|
||||||
}
|
}
|
||||||
return get_window_size(screen->window);
|
return get_native_window_size(screen->window);
|
||||||
}
|
|
||||||
|
|
||||||
// apply the windowed window size if fullscreen and maximized are disabled
|
|
||||||
static void
|
|
||||||
apply_windowed_size(struct screen *screen) {
|
|
||||||
if (!screen->fullscreen && !screen->maximized) {
|
|
||||||
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
|
|
||||||
screen->windowed_window_size.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the window size to be applied when fullscreen is disabled
|
// set the window size to be applied when fullscreen is disabled
|
||||||
@@ -50,8 +41,12 @@ static void
|
|||||||
set_window_size(struct screen *screen, struct size new_size) {
|
set_window_size(struct screen *screen, struct size new_size) {
|
||||||
// setting the window size during fullscreen is implementation defined,
|
// setting the window size during fullscreen is implementation defined,
|
||||||
// so apply the resize only after fullscreen is disabled
|
// so apply the resize only after fullscreen is disabled
|
||||||
|
if (screen->fullscreen) {
|
||||||
|
// SDL_SetWindowSize will be called when fullscreen will be disabled
|
||||||
screen->windowed_window_size = new_size;
|
screen->windowed_window_size = new_size;
|
||||||
apply_windowed_size(screen);
|
} else {
|
||||||
|
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
||||||
@@ -117,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 windowed_size = get_windowed_window_size(screen);
|
struct size current_size = get_window_size(screen);
|
||||||
return get_optimal_size(windowed_size, frame_size);
|
return get_optimal_size(current_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
|
||||||
@@ -199,8 +194,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->windowed_window_size = window_size;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,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 windowed_size = get_windowed_window_size(screen);
|
struct size current_size = get_window_size(screen);
|
||||||
struct size target_size = {
|
struct size target_size = {
|
||||||
(uint32_t) windowed_size.width * new_frame_size.width
|
(uint32_t) current_size.width * new_frame_size.width
|
||||||
/ screen->frame_size.width,
|
/ screen->frame_size.width,
|
||||||
(uint32_t) windowed_size.height * new_frame_size.height
|
(uint32_t) current_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);
|
||||||
@@ -294,6 +287,10 @@ screen_render(struct screen *screen) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
screen_switch_fullscreen(struct screen *screen) {
|
screen_switch_fullscreen(struct screen *screen) {
|
||||||
|
if (!screen->fullscreen) {
|
||||||
|
// going to fullscreen, store the current windowed window size
|
||||||
|
screen->windowed_window_size = get_native_window_size(screen->window);
|
||||||
|
}
|
||||||
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)) {
|
||||||
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
||||||
@@ -301,7 +298,11 @@ screen_switch_fullscreen(struct screen *screen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
screen->fullscreen = !screen->fullscreen;
|
screen->fullscreen = !screen->fullscreen;
|
||||||
apply_windowed_size(screen);
|
if (!screen->fullscreen) {
|
||||||
|
// fullscreen disabled, restore expected windowed window size
|
||||||
|
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
|
||||||
|
screen->windowed_window_size.height);
|
||||||
|
}
|
||||||
|
|
||||||
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
||||||
screen_render(screen);
|
screen_render(screen);
|
||||||
@@ -309,75 +310,20 @@ screen_switch_fullscreen(struct screen *screen) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
screen_resize_to_fit(struct screen *screen) {
|
screen_resize_to_fit(struct screen *screen) {
|
||||||
if (screen->fullscreen) {
|
if (!screen->fullscreen) {
|
||||||
return;
|
struct size optimal_size = get_optimal_window_size(screen,
|
||||||
}
|
screen->frame_size);
|
||||||
|
SDL_SetWindowSize(screen->window, optimal_size.width,
|
||||||
if (screen->maximized) {
|
optimal_size.height);
|
||||||
SDL_RestoreWindow(screen->window);
|
|
||||||
screen->maximized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct size optimal_size =
|
|
||||||
get_optimal_window_size(screen, screen->frame_size);
|
|
||||||
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
|
|
||||||
LOGD("Resized to optimal size");
|
LOGD("Resized to optimal size");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_resize_to_pixel_perfect(struct screen *screen) {
|
screen_resize_to_pixel_perfect(struct screen *screen) {
|
||||||
if (screen->fullscreen) {
|
if (!screen->fullscreen) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screen->maximized) {
|
|
||||||
SDL_RestoreWindow(screen->window);
|
|
||||||
screen->maximized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
||||||
screen->frame_size.height);
|
screen->frame_size.height);
|
||||||
LOGD("Resized to pixel-perfect");
|
LOGD("Resized to pixel-perfect");
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
screen_handle_window_event(struct screen *screen,
|
|
||||||
const SDL_WindowEvent *event) {
|
|
||||||
switch (event->event) {
|
|
||||||
case SDL_WINDOWEVENT_EXPOSED:
|
|
||||||
screen_render(screen);
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
|
||||||
if (!screen->fullscreen && !screen->maximized) {
|
|
||||||
// Backup the previous size: if we receive the MAXIMIZED event,
|
|
||||||
// then the new size must be ignored (it's the maximized size).
|
|
||||||
// We could not rely on the window flags due to race conditions
|
|
||||||
// (they could be updated asynchronously, at least on X11).
|
|
||||||
screen->windowed_window_size_backup =
|
|
||||||
screen->windowed_window_size;
|
|
||||||
|
|
||||||
// Save the windowed size, so that it is available once the
|
|
||||||
// window is maximized or fullscreen is enabled.
|
|
||||||
screen->windowed_window_size = get_window_size(screen->window);
|
|
||||||
}
|
|
||||||
screen_render(screen);
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_MAXIMIZED:
|
|
||||||
// The backup size must be non-nul.
|
|
||||||
SDL_assert(screen->windowed_window_size_backup.width);
|
|
||||||
SDL_assert(screen->windowed_window_size_backup.height);
|
|
||||||
// Revert the last size, it was updated while screen was maximized.
|
|
||||||
screen->windowed_window_size = screen->windowed_window_size_backup;
|
|
||||||
#ifdef DEBUG
|
|
||||||
// Reset the backup to invalid values to detect unexpected usage
|
|
||||||
screen->windowed_window_size_backup.width = 0;
|
|
||||||
screen->windowed_window_size_backup.height = 0;
|
|
||||||
#endif
|
|
||||||
screen->maximized = true;
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT_RESTORED:
|
|
||||||
screen->maximized = false;
|
|
||||||
apply_windowed_size(screen);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,10 @@ struct screen {
|
|||||||
SDL_Renderer *renderer;
|
SDL_Renderer *renderer;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
struct size frame_size;
|
struct size frame_size;
|
||||||
// The window size the last time it was not maximized or fullscreen.
|
//used only in fullscreen mode to know the windowed window size
|
||||||
struct size windowed_window_size;
|
struct size windowed_window_size;
|
||||||
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
|
|
||||||
// able to revert the size to its non-maximized value.
|
|
||||||
struct size windowed_window_size_backup;
|
|
||||||
bool has_frame;
|
bool has_frame;
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool maximized;
|
|
||||||
bool no_window;
|
bool no_window;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -38,13 +34,8 @@ struct screen {
|
|||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.windowed_window_size_backup = { \
|
|
||||||
.width = 0, \
|
|
||||||
.height = 0, \
|
|
||||||
}, \
|
|
||||||
.has_frame = false, \
|
.has_frame = false, \
|
||||||
.fullscreen = false, \
|
.fullscreen = false, \
|
||||||
.maximized = false, \
|
|
||||||
.no_window = false, \
|
.no_window = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +76,4 @@ screen_resize_to_fit(struct screen *screen);
|
|||||||
void
|
void
|
||||||
screen_resize_to_pixel_perfect(struct screen *screen);
|
screen_resize_to_pixel_perfect(struct screen *screen);
|
||||||
|
|
||||||
// react to window events
|
|
||||||
void
|
|
||||||
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "net.h"
|
#include "net.h"
|
||||||
|
|
||||||
#define SOCKET_NAME "scrcpy"
|
#define SOCKET_NAME "scrcpy"
|
||||||
#define SERVER_FILENAME "scrcpy-server"
|
#define SERVER_FILENAME "scrcpy-server.jar"
|
||||||
|
|
||||||
#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 in the same directory as the executable
|
// use scrcpy-server.jar 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,11 +124,6 @@ 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",
|
||||||
max_size_string,
|
max_size_string,
|
||||||
@@ -138,17 +133,6 @@ 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]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,5 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s
|
|||||||
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
|
option('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 from the same directory as the scrcpy executable')
|
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
|
||||||
option('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')
|
|
||||||
|
|||||||
@@ -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" dist/scrcpy-win32/scrcpy-server
|
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar
|
||||||
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server
|
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar
|
||||||
|
|
||||||
# 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" "release-$TAG/scrcpy-server-$TAG"
|
cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar"
|
||||||
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" \
|
sha256sum "scrcpy-server-$TAG.jar" \
|
||||||
"scrcpy-win32-$TAG.zip" \
|
"scrcpy-win32-$TAG.zip" \
|
||||||
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt
|
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt
|
||||||
|
|
||||||
|
|||||||
2
run
2
run
@@ -20,4 +20,4 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@"
|
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"
|
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy"
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -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',
|
output: 'scrcpy-server.jar',
|
||||||
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',
|
output: 'scrcpy-server.jar',
|
||||||
command: ['cp', '@INPUT@', '@OUTPUT@'],
|
command: ['cp', '@INPUT@', '@OUTPUT@'],
|
||||||
install: true,
|
install: true,
|
||||||
install_dir: 'share/scrcpy')
|
install_dir: 'share/scrcpy')
|
||||||
|
|||||||
@@ -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(), msg.getButtons());
|
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), 0);
|
||||||
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, buttons, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
|
pointerCoords, 0, 0, 1f, 1f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||||
return injectEvent(event);
|
return injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,11 +161,7 @@ public final class Device {
|
|||||||
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public void setScreenPowerMode(int mode) {
|
public void setScreenPowerMode(int mode) {
|
||||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
IBinder d = SurfaceControl.getBuiltInDisplay(0);
|
||||||
if (d == null) {
|
|
||||||
Ln.e("Could not get built-in display");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SurfaceControl.setDisplayPowerMode(d, mode);
|
SurfaceControl.setDisplayPowerMode(d, mode);
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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;
|
||||||
@@ -55,16 +54,6 @@ 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;
|
||||||
|
|||||||
@@ -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";
|
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
||||||
|
|
||||||
private Server() {
|
private Server() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
|
|||||||
@@ -1,102 +1,44 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.Ln;
|
|
||||||
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public class ClipboardManager {
|
public class ClipboardManager {
|
||||||
|
|
||||||
private static final String PACKAGE_NAME = "com.android.shell";
|
|
||||||
private static final int USER_ID = 0;
|
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getPrimaryClipMethod;
|
private final Method getPrimaryClipMethod;
|
||||||
private Method setPrimaryClipMethod;
|
private final Method setPrimaryClipMethod;
|
||||||
|
|
||||||
public ClipboardManager(IInterface manager) {
|
public ClipboardManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
|
||||||
|
|
||||||
private Method getGetPrimaryClipMethod() {
|
|
||||||
if (getPrimaryClipMethod == null) {
|
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
|
||||||
} else {
|
|
||||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getPrimaryClipMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Method getSetPrimaryClipMethod() {
|
|
||||||
if (setPrimaryClipMethod == null) {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
|
||||||
} else {
|
|
||||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class,
|
|
||||||
String.class, int.class);
|
|
||||||
}
|
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
Ln.e("Could not find method", e);
|
throw new AssertionError(e);
|
||||||
}
|
|
||||||
}
|
|
||||||
return setPrimaryClipMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException,
|
|
||||||
IllegalAccessException {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
return (ClipData) method.invoke(manager, PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException,
|
|
||||||
IllegalAccessException {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
method.invoke(manager, clipData, PACKAGE_NAME);
|
|
||||||
} else {
|
|
||||||
method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CharSequence getText() {
|
public CharSequence getText() {
|
||||||
Method method = getGetPrimaryClipMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
ClipData clipData = getPrimaryClip(method, manager);
|
ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell");
|
||||||
if (clipData == null || clipData.getItemCount() == 0) {
|
if (clipData == null || clipData.getItemCount() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return clipData.getItemAt(0).getText();
|
return clipData.getItemAt(0).getText();
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
throw new AssertionError(e);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setText(CharSequence text) {
|
public void setText(CharSequence text) {
|
||||||
Method method = getSetPrimaryClipMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ClipData clipData = ClipData.newPlainText(null, text);
|
ClipData clipData = ClipData.newPlainText(null, text);
|
||||||
try {
|
try {
|
||||||
setPrimaryClip(method, manager, clipData);
|
setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell");
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.Ln;
|
|
||||||
|
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
|
|
||||||
@@ -15,33 +13,22 @@ public final class InputManager {
|
|||||||
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
|
||||||
|
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method injectInputEventMethod;
|
private final Method injectInputEventMethod;
|
||||||
|
|
||||||
public InputManager(IInterface manager) {
|
public InputManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
|
||||||
|
|
||||||
private Method getInjectInputEventMethod() {
|
|
||||||
if (injectInputEventMethod == null) {
|
|
||||||
try {
|
try {
|
||||||
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
Ln.e("Could not find method", e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return injectInputEventMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||||
Method method = getInjectInputEventMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return (Boolean) method.invoke(manager, inputEvent, mode);
|
return (Boolean) injectInputEventMethod.invoke(manager, inputEvent, mode);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
throw new AssertionError(e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.Ln;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
@@ -11,35 +9,24 @@ import java.lang.reflect.Method;
|
|||||||
|
|
||||||
public final class PowerManager {
|
public final class PowerManager {
|
||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method isScreenOnMethod;
|
private final Method isScreenOnMethod;
|
||||||
|
|
||||||
public PowerManager(IInterface manager) {
|
public PowerManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
|
||||||
|
|
||||||
private Method getIsScreenOnMethod() {
|
|
||||||
if (isScreenOnMethod == null) {
|
|
||||||
try {
|
try {
|
||||||
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
|
||||||
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
|
||||||
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
isScreenOnMethod = manager.getClass().getMethod(methodName);
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
Ln.e("Could not find method", e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isScreenOnMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isScreenOn() {
|
public boolean isScreenOn() {
|
||||||
Method method = getIsScreenOnMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
return (Boolean) method.invoke(manager);
|
return (Boolean) isScreenOnMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
throw new AssertionError(e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,49 +17,35 @@ public class StatusBarManager {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Method getExpandNotificationsPanelMethod() {
|
public void expandNotificationsPanel() {
|
||||||
if (expandNotificationsPanelMethod == null) {
|
if (expandNotificationsPanelMethod == null) {
|
||||||
try {
|
try {
|
||||||
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
Ln.e("Could not find method", e);
|
Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device");
|
||||||
}
|
|
||||||
}
|
|
||||||
return expandNotificationsPanelMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Method getCollapsePanelsMethod() {
|
|
||||||
if (collapsePanelsMethod == null) {
|
|
||||||
try {
|
|
||||||
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return collapsePanelsMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void expandNotificationsPanel() {
|
|
||||||
Method method = getExpandNotificationsPanelMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
method.invoke(manager);
|
expandNotificationsPanelMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void collapsePanels() {
|
public void collapsePanels() {
|
||||||
Method method = getCollapsePanelsMethod();
|
if (collapsePanelsMethod == null) {
|
||||||
if (method == null) {
|
try {
|
||||||
|
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
Ln.e("ServiceBarManager.collapsePanels() is not available on this device");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
method.invoke(manager);
|
collapsePanelsMethod.invoke(manager);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.Ln;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi")
|
||||||
public final class SurfaceControl {
|
public final class SurfaceControl {
|
||||||
|
|
||||||
@@ -28,9 +23,6 @@ public final class SurfaceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Method getBuiltInDisplayMethod;
|
|
||||||
private static Method setDisplayPowerModeMethod;
|
|
||||||
|
|
||||||
private SurfaceControl() {
|
private SurfaceControl() {
|
||||||
// only static methods
|
// only static methods
|
||||||
}
|
}
|
||||||
@@ -84,62 +76,24 @@ public final class SurfaceControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Method getGetBuiltInDisplayMethod() {
|
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
|
||||||
if (getBuiltInDisplayMethod == null) {
|
|
||||||
try {
|
try {
|
||||||
// the method signature has changed in Android Q
|
// the method signature has changed in Android Q
|
||||||
// <https://github.com/Genymobile/scrcpy/issues/586>
|
// <https://github.com/Genymobile/scrcpy/issues/586>
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
|
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
|
||||||
} else {
|
|
||||||
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
|
|
||||||
}
|
}
|
||||||
} catch (NoSuchMethodException e) {
|
return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId);
|
||||||
Ln.e("Could not find method", e);
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return getBuiltInDisplayMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IBinder getBuiltInDisplay() {
|
|
||||||
Method method = getGetBuiltInDisplayMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
// call getBuiltInDisplay(0)
|
|
||||||
return (IBinder) method.invoke(null, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// call getInternalDisplayToken()
|
|
||||||
return (IBinder) method.invoke(null);
|
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Method getSetDisplayPowerModeMethod() {
|
|
||||||
if (setDisplayPowerModeMethod == null) {
|
|
||||||
try {
|
|
||||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Ln.e("Could not find method", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return setDisplayPowerModeMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
|
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
|
||||||
Method method = getSetDisplayPowerModeMethod();
|
|
||||||
if (method == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
method.invoke(null, displayToken, mode);
|
CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode);
|
||||||
} catch (InvocationTargetException | IllegalAccessException e) {
|
} catch (Exception e) {
|
||||||
Ln.e("Could not invoke " + method.getName(), e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user