Compare commits

...

44 Commits
api19 ... v1.3

Author SHA1 Message Date
7e42358a7b Bump version to 1.3 2018-08-09 19:14:17 +02:00
caa9e30004 Add crop feature
Add an option to crop the screen on the server. This allows to mirror
only part of the device screen.
2018-08-09 19:12:27 +02:00
e85010fbc2 Move annotation comment
This confused the Android Studio code formatter.
2018-08-09 18:23:38 +02:00
820cd2bb54 Extract video size computation
One method, one thing.
2018-08-09 18:22:52 +02:00
8793c104ee Increase "adb forward" connection attempts
5 seconds might not be sufficient:
<https://github.com/Genymobile/scrcpy/issues/213>

Increase to 10 seconds (it does not harm).
2018-08-09 18:22:42 +02:00
fca806e095 Do not call deprecated av_register_all()
av_register_all() is deprecated in FFmpeg since this commit:
<http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=0694d8702421e7aff1340038559c438b61bb30dd>

It is now useless to call it:
<https://ffmpeg.org/pipermail/ffmpeg-devel/2018-February/225051.html>

Fixes <https://github.com/Genymobile/scrcpy/issues/203>.
2018-08-09 18:18:22 +02:00
280891837e Simplify README for Windows users
So that users stop downloading platform-tools unnecessarily.
2018-07-16 17:13:36 +02:00
49b2e63d13 Forward repeated volume events
Send repeated events when holding volume up/down shortcuts.
2018-06-24 21:51:54 +02:00
c12c64ed41 Send separate DOWN/UP key events
Shortcuts generated instant DOWN/UP key events. Instead, generate DOWN
event on Ctrl+key down and UP event on Ctrl+key up.

Fixes <https://github.com/Genymobile/scrcpy/issues/166>.
2018-06-24 21:50:53 +02:00
2f66acd75d Improve English comment
Replace "implying" by "involving" (both "impliquant" in French).
2018-06-24 20:46:14 +02:00
1846d2f078 Prevent killing unexpected process
A missing initialization (fixed by the previous commit) leaded to kill
unexpected process.

In order to prevent consequences of similar errors in the future, never
call kill() with a non-positive PID.

See <https://github.com/Genymobile/scrcpy/issues/182>.
2018-06-22 19:56:58 +02:00
1a0139321b Fix missing installer initialization
The current_process field of struct installer was not initialized.
Since the installer instance is static, its default value was 0.

The call to installer_stop() then called kill(0, SIGTERM) (on Linux),
which sent SIGTERM to every process in the process group. In particular,
the scrcpy process was killed.

As a consequence, the last cleanup steps, like disabling "show touches",
were not executed.

Fixes <https://github.com/Genymobile/scrcpy/issues/183>.
2018-06-22 18:35:58 +02:00
8890750681 Merge pull request #169 from zopelee/master
Fix meson error: ‘for’ loop initial declarations are only allowed in …
2018-06-08 08:08:00 +02:00
aac9d5057f Fix meson error: ‘for’ loop initial declarations are only allowed in C99 mode. 2018-06-08 12:21:04 +08:00
f705a73149 Use a meson option to crossbuild for Windows
Meson decided to crossbuild for Windows as soon as
meson.is_cross_build() returned true. This made non-Windows crossbuilds
fail.

Instead, add an explicit option "crossbuild_windows".

Fixes <https://github.com/Genymobile/scrcpy/issues/165>.
2018-06-05 20:45:41 +02:00
27a9bd3424 Fix clean recipe in cross Makefile
Make the "clean" recipe also remove the noconsole build directories.
2018-06-05 20:42:35 +02:00
cf121a0f65 Upgrade gradle 2018-06-05 20:33:37 +02:00
c8a5f9dc63 Update links to v1.2 in README 2018-05-28 22:52:52 +02:00
8106bb8215 Bump version to 1.2 2018-05-28 22:32:14 +02:00
f1b3a40375 Rename SHA256SUM to SHA256SUMS
It contains the checksums for several files.
2018-05-28 22:32:14 +02:00
a63dd47f2d Make CreateProcess() flags depend on "noconsole"
On Windows, display the output of external commands (adb) when a console
is available.
2018-05-28 22:28:20 +02:00
6b4bbb1fb3 Update README and FAQ for the new Windows releases
Document how to generate a Windows release from Linux.

It solves the "no output" issue on Windows, so update the FAQ.
2018-05-28 20:45:54 +02:00
aedc2c2da9 Also build "noconsole" binary for Windows
On Windows, an application is either console or gui, it cannot be both.

Scrcpy should be both: it outputs important information to console, but
we still want to be able to ignore the console and launch it without a
visible cmd.exe window.

Therefore, build two binaries:
 - scrcpy.exe
 - scrcpy-noconsole.exe
2018-05-28 20:45:54 +02:00
106b87a4d2 Add cross-compilation scripts for Windows
Build the Windows binary from mingw on Linux, using the official
prebuilt binaries for ffmpeg, SDL2 and adb.

MSYS2 and all its packaged dll are not necessary anymore.
2018-05-28 20:45:54 +02:00
7d68316f45 Indicate that libs are included for Windows 2018-05-28 20:45:54 +02:00
e729b76176 Indicate that scrcpy also works over TCP/IP 2018-05-28 20:45:54 +02:00
6274cc3767 Document APK drag & drop
Add the drag & drop shortcut for installing an APK file to the README
and the help.
2018-05-28 20:45:54 +02:00
9c6f9b24f9 Quote apk path on Windows
Windows will parse the string, so the local name must be quoted.
2018-05-28 20:45:54 +02:00
e2a2973990 Drag and drop to install apk files from computer
<https://github.com/Genymobile/scrcpy/pull/133>
2018-05-28 20:45:54 +02:00
41419a2e77 Fix net_send_all() warning
If len is not positive, w could be returned uninitialized.
2018-05-28 20:21:56 +02:00
e3d0a59c80 Fix proc_show_touches warning
In practice, proc_show_touches may not be used uninitialized, since it
checks the flag options->show_touches, but the compiler can't know that,
so initialize it to avoid the warning.
2018-05-28 20:21:50 +02:00
bb3a7f05ac Release controller lock while processing events
Once the controller took a control event, release the mutex before
processing it, so that the main thread is not blocked to push a new
event.
2018-05-26 15:14:46 +02:00
ec02823045 Fix leak on server start error
Fail on SDL_strdup() failure, and free the duplicated serial on further
error.
2018-05-26 14:20:05 +02:00
435a7fe314 Add missing includes
Include string.h and stdio.h explicitly.
2018-05-13 15:33:13 +02:00
78da66f126 Merge branch 'master' into dev 2018-04-08 12:41:51 +02:00
9aa88b6fc3 Map numpad ENTER key
Forward numpad ENTER key to the device.

Fixes <https://github.com/Genymobile/scrcpy/issues/117>.
2018-04-08 12:40:05 +02:00
f8ad7df3be Add FAQ section about KWin crash
Link to the workaround to keep the compositor enabled while _scrcpy_ is
running.
2018-04-05 02:03:26 +02:00
0871bca9c7 Avoid pointer arithmetic on "void *"
Fix the following warning (with -Wpedantic enabled):

    pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]
2018-04-04 10:50:12 +02:00
b2b5404883 Use const pointers when possible
Sending data only require to read the input buffer, so declare it const.
2018-04-04 10:50:07 +02:00
1bddb80b5f Change volume shortcuts
I could not make Ctrl+'+' and Ctrl+'-' work for every keyboard on every
platform.

Instead, use Ctrl+UP and Ctrl+DOWN (like in VLC) to change the volume.

Fixes <https://github.com/Genymobile/scrcpy/issues/103>.
2018-04-03 14:25:11 +02:00
6323f3974f Document 32 bits packages Windows in README
To build for Windows 32 bits, use the i686 packages instead.
2018-03-31 11:07:25 +02:00
16a3de1796 Make checkstyle happy
Reorder the imports to remove checkstyle warnings.
2018-03-28 22:05:34 +02:00
6161f7688c Install on macOS via Homebrew in README
The application is now packaged for Homebrew:
<https://github.com/Homebrew/homebrew-core/pull/25173>

Give instructions to install it from Homebrew for macOS (it's much
easier).

Thanks to @stek29 for the formula ;-)
2018-03-28 14:14:56 +02:00
2a02fb3611 Document how to make a portable build on Windows
On MSYS2, ./gradlew does not work as expected, so use its absolute path.
2018-03-28 11:08:01 +02:00
40 changed files with 936 additions and 233 deletions

32
FAQ.md
View File

@ -7,29 +7,8 @@ with it.
Here are the common reported problems and their status.
### On Windows, I have no output in the console
When run in `cmd.exe`, the application does not print anything. Even `scrcpy
--help` have no output. We don't know why yet.
However, if you run the very same `scrcpy.exe` from
[MSYS2](https://www.msys2.org/) (`mingw64`), then it correctly prints output.
As a workaround, redirect outputs to files, so that you can read the files
afterwards:
```bash
scrcpy >stdout 2>stderr
type stdout
type stderr
```
_Note that all SDL logs are printed to stderr._
### On Windows, when I start the application, nothing happens
The previous problem does not help to get a clue about the cause.
### On Windows, my device is not detected
The most common is your device not being detected by `adb`, or is unauthorized.
Check everything is ok by calling:
@ -79,3 +58,12 @@ meson x --buildtype release -Dhidpi_support=false
```
However, the video will be displayed at lower resolution.
### KWin compositor crashes
On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
As a workaround, [disable "Block compositing"][kwin].
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613

View File

@ -1,62 +0,0 @@
# This makefile provides recipes to build a "portable" version of scrcpy.
#
# Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
#
# "make release-portable" builds a zip containing the client and the server.
#
# This is a simple Makefile because Meson is not flexible enough to execute some
# arbitrary commands.
.PHONY: default clean build-portable release-portable dist-portable dist-portable-zip sums test check
GRADLE ?= ./gradlew
PORTABLE_BUILD_DIR := build-portable
DIST := dist
TARGET_DIR := scrcpy
VERSION := $(shell git describe --tags --always)
TARGET := $(TARGET_DIR)-$(VERSION).zip
default:
@echo 'You must specify a target. Try: make release-portable'
clean:
$(GRADLE) clean
rm -rf "$(PORTABLE_BUILD_DIR)" "$(DIST)"
build-portable:
[ -d "$(PORTABLE_BUILD_DIR)" ] || ( mkdir "$(PORTABLE_BUILD_DIR)" && \
meson "$(PORTABLE_BUILD_DIR)" \
--buildtype release --strip -Db_lto=true \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(PORTABLE_BUILD_DIR)"
release-portable: clean dist-portable-zip sums
@echo "Release created in $(DIST)/."
dist-portable: build-portable
mkdir -p "$(DIST)/$(TARGET_DIR)"
cp "$(PORTABLE_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(TARGET_DIR)/"
cp "$(PORTABLE_BUILD_DIR)"/app/scrcpy "$(DIST)/$(TARGET_DIR)/"
dist-portable-zip: dist-portable
cd "$(DIST)"; \
zip -r "$(TARGET)" "$(TARGET_DIR)"
sums:
cd "$(DIST)"; \
sha256sum *.zip > SHA256SUM.txt
test: build-portable
$(GRADLE) test
ninja -C "$(PORTABLE_BUILD_DIR)" test
check: build-portable
$(GRADLE) check
ninja -C "$(PORTABLE_BUILD_DIR)" test

136
Makefile.CrossWindows Normal file
View File

@ -0,0 +1,136 @@
# This makefile provides recipes to build a "portable" version of scrcpy for
# Windows.
#
# Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
.PHONY: default clean \
build-server \
prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win32-noconsole \
build-win64 build-win64-noconsole \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
sums release
GRADLE ?= ./gradlew
SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole
WIN64_BUILD_DIR := build-win64
WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32
WIN64_TARGET_DIR := scrcpy-win64
VERSION := $(shell git describe --tags --always)
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
release: clean zip-win32 zip-win64 sums
@echo "Release created in $(DIST)/."
clean:
$(GRADLE) clean
rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \
"$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)"
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dbuild_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
-$(MAKE) -C prebuilt-deps prepare-win32
build-win32: prepare-deps-win32
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
meson "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32
[ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64:
-$(MAKE) -C prebuilt-deps prepare-win64
build-win64: prepare-deps-win64
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
meson "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64
[ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.0-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.0-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
zip-win64: dist-win64
cd "$(DIST)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
sums:
cd "$(DIST)"; \
sha256sum *.zip > SHA256SUMS.txt

115
README.md
View File

@ -1,8 +1,8 @@
# scrcpy
# scrcpy (v1.2)
This application provides display and control of Android devices connected on
USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
and _MacOS_.
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
It works on _GNU/Linux_, _Windows_ and _MacOS_.
![screenshot](assets/screenshot-debian-600.jpg)
@ -14,16 +14,12 @@ The Android part requires at least API 21 (Android 5.0).
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
On Windows, if you use the [prebuilt application](#windows), it is already
included. Otherwise, just download the [platform-tools][platform-tools-windows]
and extract the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
On Windows, just [download scrcpy for Windows](#windows), `adb` is included.
Make sure you [enabled adb debugging][enable-adb] on your device(s).
The client requires [FFmpeg] and [LibSDL2].
The client requires [FFmpeg] and [LibSDL2]. On Windows, they are included in the
[prebuilt application](#windows).
[adb]: https://developer.android.com/studio/command-line/adb.html
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
@ -80,18 +76,54 @@ Two [AUR] packages have been created by users:
#### Windows
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available:
- [`scrcpy-windows-with-deps-v1.1.zip`][direct-windows-with-deps].
_(SHA-256: 27eb36c15937601d1062c1dc0b45faae0e06fefea2019aadeb4fa7f76a07bb4c)_
- [`scrcpy-win32-v1.2.zip`][direct-win32].
_(SHA-256: a1fe1de67ec75dcf970ca5d97a04c26ff0f2d61871f2ef51b6f2f0bf666966b2)_
- [`scrcpy-win64-v1.2.zip`][direct-win64].
_(SHA-256: 35ae3bcee51771e7c51b8a8be87aef2295c9f267606a7cf83ebb0a4d583ef536)_
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-windows-with-deps-v1.1.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-win32-v1.2.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-win64-v1.2.zip
_(It's just a portable version including _dll_ copied from MSYS2.)_
Instead, you may want to build it manually.
Instead, you may want to build it manually. You need [MSYS2] to build the
project. From an MSYS2 terminal, install the required packages:
In that case, download the [platform-tools][platform-tools-windows] and extract
the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
##### Cross-compile from Linux
This is the preferred method (and the way the release is built).
From _Debian_, install _mingw_:
```bash
sudo apt install mingw-w64 mingw-w64-tools
```
You also need the JDK to build the server:
```bash
sudo apt install openjdk-8-jdk
```
Then generate the releases:
```bash
make -f Makefile.CrossWindows
```
It will generate win32 and win64 releases into `dist/`.
##### In MSYS2
From Windows, you need [MSYS2] to build the project. From an MSYS2 terminal,
install the required packages:
[MSYS2]: http://www.msys2.org/
@ -107,6 +139,20 @@ pacman -S mingw-w64-x86_64-make \
mingw-w64-x86_64-meson
```
For a 32 bits version, replace `x86_64` by `i686`:
```bash
# runtime dependencies
pacman -S mingw-w64-i686-SDL2 \
mingw-w64-i686-ffmpeg
# client build dependencies
pacman -S mingw-w64-i686-make \
mingw-w64-i686-gcc \
mingw-w64-i686-pkg-config \
mingw-w64-i686-meson
```
Java (>= 7) is not available in MSYS2, so if you plan to build the server,
install it manually and make it available from the `PATH`:
@ -116,10 +162,17 @@ export PATH="$JAVA_HOME/bin:$PATH"
#### Mac OS
Use [Homebrew] to install the packages:
The application is available in [Homebrew]. Just install it:
[Homebrew]: https://brew.sh/
```bash
brew install scrcpy
```
Instead, you may want to build it manually. Install the packages:
```bash
# runtime dependencies
brew install sdl2 ffmpeg
@ -194,10 +247,10 @@ Since the server binary, that will be pushed to the Android device, does not
depend on your system and architecture, you may want to use the prebuilt binary
instead:
- [`scrcpy-server-v1.1.jar`][direct-scrcpy-server].
_(SHA-256: 14826512bf38447ec94adf3b531676ce038d19e7e06757fb4e537882b17e77b3)_
- [`scrcpy-server-v1.2.jar`][direct-scrcpy-server].
_(SHA-256: cb39654ed2fda3d30ddff292806950ccc5c394375ea12b974f790c7f38f61f60)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-server-v1.1.jar
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-server-v1.2.jar
In that case, the build does not require Java or the Android SDK.
@ -241,6 +294,12 @@ screen is smaller, or cannot decode such a high definition):
scrcpy -m 1024
```
The device screen may be cropped to mirror only part of the screen:
```bash
scrcpy -c 1224:1440:0:0 # 1224x1440 at offset (0,0)
```
If several devices are listed in `adb devices`, you must specify the _serial_:
```bash
@ -273,12 +332,13 @@ To run without installing:
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| click on `APP_SWITCH` | `Ctrl`+`s` |
| click on `MENU` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_DOWN` | `Ctrl`+`-` |
| click on `VOLUME_UP` | `Ctrl`+`` _(up)_ |
| click on `VOLUME_DOWN` | `Ctrl`+`` _(down)_ |
| click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click²_ |
| paste computer clipboard to device | `Ctrl`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| install APK from computer | drag & drop APK file |
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
@ -324,5 +384,8 @@ Read the [developers page].
## Articles
- [Introducing scrcpy](https://blog.rom1v.com/2018/03/introducing-scrcpy/)
- [Scrcpy now works wirelessly](https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/)
- [Introducing scrcpy][article-intro]
- [Scrcpy now works wirelessly][article-tcpip]
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/

View File

@ -9,6 +9,7 @@ src = [
'src/fpscounter.c',
'src/frames.c',
'src/inputmanager.c',
'src/installer.c',
'src/lockutil.c',
'src/net.c',
'src/scrcpy.c',
@ -18,12 +19,54 @@ src = [
'src/tinyxpm.c',
]
dependencies = [
dependency('libavformat'),
dependency('libavcodec'),
dependency('libavutil'),
dependency('sdl2'),
]
if not get_option('crossbuild_windows')
# native build
dependencies = [
dependency('libavformat'),
dependency('libavcodec'),
dependency('libavutil'),
dependency('sdl2'),
]
else
# cross-compile mingw32 build (from Linux to Windows)
cc = meson.get_compiler('c')
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin'
sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib'
sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include'
sdl2 = declare_dependency(
dependencies: [
cc.find_library('SDL2', dirs: sdl2_bin_dir),
cc.find_library('SDL2main', dirs: sdl2_lib_dir),
],
include_directories: include_directories(sdl2_include_dir)
)
prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared')
prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev')
ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin'
ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include'
ffmpeg = declare_dependency(
dependencies: [
cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir),
cc.find_library('avformat-58', dirs: ffmpeg_bin_dir),
cc.find_library('avutil-56', dirs: ffmpeg_bin_dir),
],
include_directories: include_directories(ffmpeg_include_dir)
)
dependencies = [
ffmpeg,
sdl2,
cc.find_library('mingw32')
]
endif
cc = meson.get_compiler('c')
@ -42,7 +85,7 @@ conf = configuration_data()
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
# the version, updated on release
conf.set_quoted('SCRCPY_VERSION', '1.1')
conf.set_quoted('SCRCPY_VERSION', '1.3')
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
@ -83,10 +126,22 @@ conf.set('SKIP_FRAMES', get_option('skip_frames'))
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src')
executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true)
if get_option('windows_noconsole')
c_args = [ '-mwindows' ]
link_args = [ '-mwindows' ]
else
c_args = []
link_args = []
endif
executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: c_args, link_args: link_args)
### TESTS

View File

@ -72,6 +72,21 @@ process_t adb_push(const char *serial, const char *local, const char *remote) {
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_install(const char *serial, const char *local) {
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted (see sys/win/command.c)
size_t len = strlen(local);
char quoted[len + 3];
memcpy(&quoted[1], local, len);
quoted[0] = '"';
quoted[len + 1] = '"';
quoted[len + 2] = '\0';
local = quoted;
#endif
const char *const adb_cmd[] = {"install", "-r", local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_remove_path(const char *serial, const char *path) {
const char *const adb_cmd[] = {"shell", "rm", path};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));

View File

@ -42,6 +42,7 @@ process_t adb_forward_remove(const char *serial, uint16_t local_port);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t adb_push(const char *serial, const char *local, const char *remote);
process_t adb_install(const char *serial, const char *local);
process_t adb_remove_path(const char *serial, const char *path);
// convenience function to wait for a successful process execution

View File

@ -1,6 +1,7 @@
#include "controlevent.h"
#include <SDL2/SDL_stdinc.h>
#include <string.h>
#include "lockutil.h"
#include "log.h"

View File

@ -54,27 +54,32 @@ static SDL_bool process_event(struct controller *controller, const struct contro
static int run_controller(void *data) {
struct controller *controller = data;
mutex_lock(controller->mutex);
for (;;) {
mutex_lock(controller->mutex);
while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
cond_wait(controller->event_cond, controller->mutex);
}
if (controller->stopped) {
// stop immediately, do not process further events
mutex_unlock(controller->mutex);
break;
}
struct control_event event;
while (control_event_queue_take(&controller->queue, &event)) {
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
goto end;
}
#ifdef BUILD_DEBUG
bool non_empty = control_event_queue_take(&controller->queue, &event);
SDL_assert(non_empty);
#else
control_event_queue_take(&controller->queue, &event);
#endif
mutex_unlock(controller->mutex);
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
break;
}
}
end:
mutex_unlock(controller->mutex);
return 0;
}

View File

@ -73,6 +73,7 @@ static enum android_metastate convert_meta_state(SDL_Keymod mod) {
static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to) {
switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB);

View File

@ -29,57 +29,58 @@ static struct point get_mouse_point(struct screen *screen) {
};
}
static SDL_bool is_ctrl_down(void) {
const Uint8 *state = SDL_GetKeyboardState(NULL);
return state[SDL_SCANCODE_LCTRL] || state[SDL_SCANCODE_RCTRL];
}
static const int ACTION_DOWN = 1;
static const int ACTION_UP = 1 << 1;
static void send_keycode(struct controller *controller, enum android_keycode keycode, const char *name) {
static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) {
// send DOWN event
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
return;
if (actions & ACTION_DOWN) {
control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
return;
}
}
// send UP event
control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (UP)", name);
if (actions & ACTION_UP) {
control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (UP)", name);
}
}
}
static inline void action_home(struct controller *controller) {
send_keycode(controller, AKEYCODE_HOME, "HOME");
static inline void action_home(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_HOME, actions, "HOME");
}
static inline void action_back(struct controller *controller) {
send_keycode(controller, AKEYCODE_BACK, "BACK");
static inline void action_back(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_BACK, actions, "BACK");
}
static inline void action_app_switch(struct controller *controller) {
send_keycode(controller, AKEYCODE_APP_SWITCH, "APP_SWITCH");
static inline void action_app_switch(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH");
}
static inline void action_power(struct controller *controller) {
send_keycode(controller, AKEYCODE_POWER, "POWER");
static inline void action_power(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_POWER, actions, "POWER");
}
static inline void action_volume_up(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_UP, "VOLUME_UP");
static inline void action_volume_up(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP");
}
static inline void action_volume_down(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN");
static inline void action_volume_down(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN");
}
static inline void action_menu(struct controller *controller) {
send_keycode(controller, AKEYCODE_MENU, "MENU");
static inline void action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
}
// turn the screen on if it was off, press BACK otherwise
@ -128,18 +129,6 @@ static void clipboard_paste(struct controller *controller) {
void input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event) {
if (is_ctrl_down()) {
switch (event->text[0]) {
case '+':
action_volume_up(input_manager->controller);
break;
case '-':
action_volume_down(input_manager->controller);
break;
}
return;
}
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
control_event.text_event.text = SDL_strdup(event->text);
@ -158,51 +147,74 @@ void input_manager_process_key(struct input_manager *input_manager,
// capture all Ctrl events
if (ctrl) {
SDL_bool repeat = event->repeat;
// only consider keydown events, and ignore repeated events
if (repeat || event->type != SDL_KEYDOWN) {
return;
}
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (shift) {
// currently, there is no shortcut implying SHIFT
// currently, there is no shortcut involving SHIFT
return;
}
SDL_Keycode keycode = event->keysym.sym;
int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP;
SDL_bool repeat = event->repeat;
switch (keycode) {
case SDLK_h:
action_home(input_manager->controller);
if (!repeat) {
action_home(input_manager->controller, action);
}
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
action_back(input_manager->controller);
if (!repeat) {
action_back(input_manager->controller, action);
}
return;
case SDLK_s:
action_app_switch(input_manager->controller);
if (!repeat) {
action_app_switch(input_manager->controller, action);
}
return;
case SDLK_m:
action_menu(input_manager->controller);
if (!repeat) {
action_menu(input_manager->controller, action);
}
return;
case SDLK_p:
action_power(input_manager->controller);
if (!repeat) {
action_power(input_manager->controller, action);
}
return;
case SDLK_DOWN:
// forward repeated events
action_volume_down(input_manager->controller, action);
return;
case SDLK_UP:
// forward repeated events
action_volume_up(input_manager->controller, action);
return;
case SDLK_v:
clipboard_paste(input_manager->controller);
if (!repeat && event->type == SDL_KEYDOWN) {
clipboard_paste(input_manager->controller);
}
return;
case SDLK_f:
screen_switch_fullscreen(input_manager->screen);
if (!repeat && event->type == SDL_KEYDOWN) {
screen_switch_fullscreen(input_manager->screen);
}
return;
case SDLK_x:
screen_resize_to_fit(input_manager->screen);
if (!repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_fit(input_manager->screen);
}
return;
case SDLK_g:
screen_resize_to_pixel_perfect(input_manager->screen);
if (!repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_pixel_perfect(input_manager->screen);
}
return;
case SDLK_i:
switch_fps_counter_state(input_manager->frames);
if (!repeat && event->type == SDL_KEYDOWN) {
switch_fps_counter_state(input_manager->frames);
}
return;
}
@ -239,7 +251,7 @@ void input_manager_process_mouse_button(struct input_manager *input_manager,
return;
}
if (event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller);
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
return;
}
// double-click on black borders resize to fit the device screen

180
app/src/installer.c Normal file
View File

@ -0,0 +1,180 @@
#include "installer.h"
#include <string.h>
#include "command.h"
#include "lockutil.h"
#include "log.h"
// NOTE(adopi) this can be more generic:
// it could be used with a command queue instead of a filename queue
// then we would have a generic invoker (useful if we want to handle more async commands)
SDL_bool apk_queue_is_empty(const struct apk_queue *queue) {
return queue->head == queue->tail;
}
SDL_bool apk_queue_is_full(const struct apk_queue *queue) {
return (queue->head + 1) % APK_QUEUE_SIZE == queue->tail;
}
SDL_bool apk_queue_init(struct apk_queue *queue) {
queue->head = 0;
queue->tail = 0;
return SDL_TRUE;
}
void apk_queue_destroy(struct apk_queue *queue) {
int i = queue->tail;
while (i != queue->head) {
SDL_free(queue->data[i]);
i = (i + 1) % APK_QUEUE_SIZE;
}
}
SDL_bool apk_queue_push(struct apk_queue *queue, const char *apk) {
if (apk_queue_is_full(queue)) {
return SDL_FALSE;
}
queue->data[queue->head] = SDL_strdup(apk);
queue->head = (queue->head + 1) % APK_QUEUE_SIZE;
return SDL_TRUE;
}
SDL_bool apk_queue_take(struct apk_queue *queue, char **apk) {
if (apk_queue_is_empty(queue)) {
return SDL_FALSE;
}
// transfer ownership
*apk = queue->data[queue->tail];
queue->tail = (queue->tail + 1) % APK_QUEUE_SIZE;
return SDL_TRUE;
}
SDL_bool installer_init(struct installer *installer, const char *serial) {
if (!apk_queue_init(&installer->queue)) {
return SDL_FALSE;
}
if (!(installer->mutex = SDL_CreateMutex())) {
return SDL_FALSE;
}
if (!(installer->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(installer->mutex);
return SDL_FALSE;
}
if (serial) {
installer->serial = SDL_strdup(serial);
if (!installer->serial) {
LOGW("Cannot strdup serial");
return SDL_FALSE;
}
} else {
installer->serial = NULL;
}
// lazy initialization
installer->initialized = SDL_FALSE;
installer->stopped = SDL_FALSE;
installer->current_process = PROCESS_NONE;
return SDL_TRUE;
}
void installer_destroy(struct installer *installer) {
SDL_DestroyCond(installer->event_cond);
SDL_DestroyMutex(installer->mutex);
apk_queue_destroy(&installer->queue);
SDL_free((void *) installer->serial);
}
SDL_bool installer_install_apk(struct installer *installer, const char *apk) {
SDL_bool res;
// start installer if it's used for the first time
if (!installer->initialized) {
if (!installer_start(installer)) {
return SDL_FALSE;
}
installer->initialized = SDL_TRUE;
}
mutex_lock(installer->mutex);
SDL_bool was_empty = apk_queue_is_empty(&installer->queue);
res = apk_queue_push(&installer->queue, apk);
if (was_empty) {
cond_signal(installer->event_cond);
}
mutex_unlock(installer->mutex);
return res;
}
static int run_installer(void *data) {
struct installer *installer = data;
for (;;) {
mutex_lock(installer->mutex);
while (!installer->stopped && apk_queue_is_empty(&installer->queue)) {
cond_wait(installer->event_cond, installer->mutex);
}
if (installer->stopped) {
// stop immediately, do not process further events
mutex_unlock(installer->mutex);
break;
}
char *current_apk;
#ifdef BUILD_DEBUG
bool non_empty = apk_queue_take(&installer->queue, &current_apk);
SDL_assert(non_empty);
#else
apk_queue_take(&installer->queue, &current_apk);
#endif
LOGI("Installing %s...", current_apk);
process_t process = adb_install(installer->serial, current_apk);
installer->current_process = process;
mutex_unlock(installer->mutex);
if (process_check_success(process, "adb install")) {
LOGI("%s installed successfully", current_apk);
} else {
LOGE("Failed to install %s", current_apk);
}
SDL_free(current_apk);
}
return 0;
}
SDL_bool installer_start(struct installer *installer) {
LOGD("Starting installer thread");
installer->thread = SDL_CreateThread(run_installer, "installer", installer);
if (!installer->thread) {
LOGC("Could not start installer thread");
return SDL_FALSE;
}
return SDL_TRUE;
}
void installer_stop(struct installer *installer) {
mutex_lock(installer->mutex);
installer->stopped = SDL_TRUE;
cond_signal(installer->event_cond);
if (installer->current_process != PROCESS_NONE) {
if (!cmd_terminate(installer->current_process)) {
LOGW("Cannot terminate install process");
}
cmd_simple_wait(installer->current_process, NULL);
installer->current_process = PROCESS_NONE;
}
mutex_unlock(installer->mutex);
}
void installer_join(struct installer *installer) {
SDL_WaitThread(installer->thread, NULL);
}

40
app/src/installer.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef APK_INSTALLER_H
#define APK_INSTALLER_H
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h>
#include "command.h"
#define APK_QUEUE_SIZE 16
// NOTE(AdoPi) apk_queue and control_event can use a generic queue
struct apk_queue {
char *data[APK_QUEUE_SIZE];
int tail;
int head;
};
struct installer {
const char *serial;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *event_cond;
SDL_bool stopped;
SDL_bool initialized;
process_t current_process;
struct apk_queue queue;
};
SDL_bool installer_init(struct installer *installer, const char *serial);
void installer_destroy(struct installer *installer);
SDL_bool installer_start(struct installer *installer);
void installer_stop(struct installer *installer);
void installer_join(struct installer *installer);
// install an apk
SDL_bool installer_install_apk(struct installer *installer, const char *filename);
#endif

View File

@ -10,6 +10,7 @@
struct args {
const char *serial;
const char *crop;
SDL_bool help;
SDL_bool version;
SDL_bool show_touches;
@ -29,6 +30,12 @@ static void usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n"
"\n"
" -c, --crop width:height:x:y\n"
" Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n"
"\n"
" -h, --help\n"
" Print this help.\n"
"\n"
@ -81,10 +88,10 @@ static void usage(const char *arg0) {
" Ctrl+m\n"
" click on MENU\n"
"\n"
" Ctrl+'+'\n"
" Ctrl+Up\n"
" click on VOLUME_UP\n"
"\n"
" Ctrl+'-'\n"
" Ctrl+Down\n"
" click on VOLUME_DOWN\n"
"\n"
" Ctrl+p\n"
@ -98,6 +105,9 @@ static void usage(const char *arg0) {
"\n"
" Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
" install APK from computer\n"
"\n",
arg0,
DEFAULT_BIT_RATE,
@ -189,6 +199,7 @@ static SDL_bool parse_port(char *optarg, Uint16 *port) {
static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'},
@ -198,13 +209,16 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
{NULL, 0, NULL, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "b:hm:p:s:tv", long_options, NULL)) != -1) {
while ((c = getopt_long(argc, argv, "b:c:hm:p:s:tv", long_options, NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) {
return SDL_FALSE;
}
break;
case 'c':
args->crop = optarg;
break;
case 'h':
args->help = SDL_TRUE;
break;
@ -250,6 +264,7 @@ int main(int argc, char *argv[]) {
#endif
struct args args = {
.serial = NULL,
.crop = NULL,
.help = SDL_FALSE,
.version = SDL_FALSE,
.show_touches = SDL_FALSE,
@ -271,7 +286,9 @@ int main(int argc, char *argv[]) {
return 0;
}
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
if (avformat_network_init()) {
return 1;
@ -283,6 +300,7 @@ int main(int argc, char *argv[]) {
struct scrcpy_options options = {
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.max_size = args.max_size,
.bit_rate = args.bit_rate,

View File

@ -82,19 +82,19 @@ ssize_t net_recv_all(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, MSG_WAITALL);
}
ssize_t net_send(socket_t socket, void *buf, size_t len) {
ssize_t net_send(socket_t socket, const void *buf, size_t len) {
return send(socket, buf, len, 0);
}
ssize_t net_send_all(socket_t socket, void *buf, size_t len) {
ssize_t w;
ssize_t net_send_all(socket_t socket, const void *buf, size_t len) {
ssize_t w = 0;
while (len > 0) {
w = send(socket, buf, len, 0);
if (w == -1) {
return -1;
}
len -= w;
buf += w;
buf = (char *) buf + w;
}
return w;
}

View File

@ -26,8 +26,8 @@ socket_t net_accept(socket_t server_socket);
// the _all versions wait/retry until len bytes have been written/read
ssize_t net_recv(socket_t socket, void *buf, size_t len);
ssize_t net_recv_all(socket_t socket, void *buf, size_t len);
ssize_t net_send(socket_t socket, void *buf, size_t len);
ssize_t net_send_all(socket_t socket, void *buf, size_t len);
ssize_t net_send(socket_t socket, const void *buf, size_t len);
ssize_t net_send_all(socket_t socket, const void *buf, size_t len);
// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
SDL_bool net_shutdown(socket_t socket, int how);
SDL_bool net_close(socket_t socket);

View File

@ -22,12 +22,14 @@
#include "screen.h"
#include "server.h"
#include "tinyxpm.h"
#include "installer.h"
static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER;
static struct frames frames;
static struct decoder decoder;
static struct controller controller;
static struct installer installer;
static struct input_manager input_manager = {
.controller = &controller,
@ -102,6 +104,9 @@ static void event_loop(void) {
case SDL_MOUSEBUTTONUP:
input_manager_process_mouse_button(&input_manager, &event.button);
break;
case SDL_DROPFILE:
installer_install_apk(&installer, event.drop.file);
break;
}
}
}
@ -121,11 +126,11 @@ static void wait_show_touches(process_t process) {
SDL_bool scrcpy(const struct scrcpy_options *options) {
if (!server_start(&server, options->serial, options->port,
options->max_size, options->bit_rate)) {
options->max_size, options->bit_rate, options->crop)) {
return SDL_FALSE;
}
process_t proc_show_touches;
process_t proc_show_touches = PROCESS_NONE;
SDL_bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
@ -169,6 +174,12 @@ SDL_bool scrcpy(const struct scrcpy_options *options) {
goto finally_destroy_server;
}
if (!installer_init(&installer, server.serial)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_frames;
}
decoder_init(&decoder, &frames, device_socket);
// now we consumed the header values, the socket receives the video stream
@ -176,7 +187,7 @@ SDL_bool scrcpy(const struct scrcpy_options *options) {
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_frames;
goto finally_destroy_installer;
}
if (!controller_init(&controller, device_socket)) {
@ -214,6 +225,10 @@ finally_stop_decoder:
// stop the server before decoder_join() to wake up the decoder
server_stop(&server);
decoder_join(&decoder);
finally_destroy_installer:
installer_stop(&installer);
installer_join(&installer);
installer_destroy(&installer);
finally_destroy_frames:
frames_destroy(&frames);
finally_destroy_server:

View File

@ -5,6 +5,7 @@
struct scrcpy_options {
const char *serial;
const char *crop;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;

View File

@ -3,6 +3,7 @@
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
@ -76,7 +77,8 @@ static SDL_bool disable_tunnel(struct server *server) {
}
static process_t execute_server(const char *serial,
Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) {
Uint16 max_size, Uint32 bit_rate,
const char *crop, SDL_bool tunnel_forward) {
char max_size_string[6];
char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size);
@ -90,6 +92,7 @@ static process_t execute_server(const char *serial,
max_size_string,
bit_rate_string,
tunnel_forward ? "true" : "false",
crop ? crop : "",
};
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
@ -146,20 +149,25 @@ void server_init(struct server *server) {
}
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate) {
Uint16 max_size, Uint32 bit_rate, const char *crop) {
server->local_port = local_port;
if (serial) {
server->serial = SDL_strdup(serial);
if (!server->serial) {
return SDL_FALSE;
}
}
if (!push_server(serial)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
server->server_copied_to_device = SDL_TRUE;
if (!enable_tunnel(server)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
@ -176,17 +184,20 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
SDL_free((void *) server->serial);
return SDL_FALSE;
}
}
// server will connect to our server socket
server->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward);
server->process = execute_server(serial, max_size, bit_rate, crop,
server->tunnel_forward);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
SDL_free((void *) server->serial);
return SDL_FALSE;
}
@ -199,7 +210,7 @@ socket_t server_connect_to(struct server *server) {
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
Uint32 attempts = 50;
Uint32 attempts = 100;
Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay);
}

View File

@ -31,7 +31,7 @@ void server_init(struct server *server);
// push, enable tunnel et start the server
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate);
Uint16 max_size, Uint32 bit_rate, const char *crop);
// block until the communication with the server is established
socket_t server_connect_to(struct server *server);

View File

@ -1,9 +1,11 @@
#include "command.h"
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "log.h"
pid_t cmd_execute(const char *path, const char *const argv[]) {
pid_t pid = fork();
@ -20,6 +22,10 @@ pid_t cmd_execute(const char *path, const char *const argv[]) {
}
SDL_bool cmd_terminate(pid_t pid) {
if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid);
abort();
}
return kill(pid, SIGTERM) != -1;
}

View File

@ -1,5 +1,6 @@
#include "command.h"
#include "config.h"
#include "log.h"
#include "strutil.h"
@ -20,7 +21,12 @@ HANDLE cmd_execute(const char *path, const char *const argv[]) {
return NULL;
}
if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
#ifdef WINDOWS_NOCONSOLE
int flags = CREATE_NO_WINDOW;
#else
int flags = 0;
#endif
if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi)) {
return NULL;
}

View File

@ -1,4 +1,5 @@
#include <assert.h>
#include <string.h>
#include "controlevent.h"

View File

@ -1,4 +1,5 @@
#include <assert.h>
#include <string.h>
#include "controlevent.h"

View File

@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.1.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

20
cross_win32.txt Normal file
View File

@ -0,0 +1,20 @@
# apt install mingw-w64 mingw-w64-tools
[binaries]
name = 'mingw'
c = '/usr/bin/i686-w64-mingw32-gcc'
cpp = '/usr/bin/i686-w64-mingw32-g++'
ar = '/usr/bin/i686-w64-mingw32-ar'
strip = '/usr/bin/i686-w64-mingw32-strip'
pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config'
[host_machine]
system = 'windows'
cpu_family = 'x86'
cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.0-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.0-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32'

20
cross_win64.txt Normal file
View File

@ -0,0 +1,20 @@
# apt install mingw-w64 mingw-w64-tools
[binaries]
name = 'mingw'
c = '/usr/bin/x86_64-w64-mingw32-gcc'
cpp = '/usr/bin/x86_64-w64-mingw32-g++'
ar = '/usr/bin/x86_64-w64-mingw32-ar'
strip = '/usr/bin/x86_64-w64-mingw32-strip'
pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config'
[host_machine]
system = 'windows'
cpu_family = 'x86'
cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.0-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.0-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32'

View File

@ -1,6 +1,6 @@
#Mon Jan 29 16:38:49 CET 2018
#Mon Jun 04 11:48:32 CEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

@ -1,4 +1,4 @@
project('scrcpy', 'c', meson_version: '>= 0.37')
project('scrcpy', 'c', meson_version: '>= 0.37', default_options : 'c_std=c99')
if get_option('build_app')
subdir('app')

View File

@ -1,5 +1,7 @@
option('build_app', type: 'boolean', value: true, description: 'Build the client')
option('build_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('override_server_path', type: 'string', description: 'Hardcoded path to find the server at runtime')
option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame')

4
prebuilt-deps/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*
!/.gitignore
!/Makefile
!/prepare-dep

40
prebuilt-deps/Makefile Normal file
View File

@ -0,0 +1,40 @@
.PHONY: prepare-win32 prepare-win64 \
prepare-ffmpeg-shared-win32 \
prepare-ffmpeg-dev-win32 \
prepare-ffmpeg-shared-win64 \
prepare-ffmpeg-dev-win64 \
prepare-sdl2 \
prepare-adb
prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.0-win32-shared.zip \
530c92df0ca14c35901b4b681847d62da3c50a0cc9b7ced37b04968f6b5c243d \
ffmpeg-4.0-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.0-win32-dev.zip \
e2f5200b5e73c4d0abb9b89c4ffc0438f92a0aadc54c81cf57e18c81a9f11c6b \
ffmpeg-4.0-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.0-win64-shared.zip \
8fe2d344463dbefc2db4239a4203a55ed0324faceaae57276a40c4fabda84c37 \
ffmpeg-4.0-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.0-win64-dev.zip \
facced738eabfc53fa92834dea8b24426f64db61298688fed480145945be07fa \
ffmpeg-4.0-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \
ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \
SDL2-2.0.8
prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r27.0.1-windows.zip \
880662adfb0d6911ff250b9e13930ae1a4110fc36d5866afd4f8f56d935f7939 \
platform-tools

58
prebuilt-deps/prepare-dep Executable file
View File

@ -0,0 +1,58 @@
#!/bin/bash
set -e
url="$1"
sum="$2"
dir="$3"
checksum() {
local file="$1"
local sum="$2"
echo "$file: verifying checksum..."
echo "$sum $file" | sha256sum -c
}
get_file() {
local url="$1"
local file="$2"
local sum="$3"
if [[ -f "$file" ]]
then
echo "$file: found"
else
echo "$file: not found, downloading..."
wget "$url" -O "$file"
fi
checksum "$file" "$sum"
}
extract() {
local file="$1"
echo "Extracting $file..."
if [[ "$file" == *.zip ]]
then
unzip -q "$file"
elif [[ "$file" == *.tar.gz ]]
then
tar xf "$file"
else
echo "Unsupported file: $file"
return 1
fi
}
get_dep() {
local url="$1"
local sum="$2"
local dir="$3"
local file="${url##*/}"
if [[ -d "$dir" ]]
then
echo "$dir: found"
else
echo "$dir: not found"
get_file "$url" "$file" "$sum"
extract "$file"
fi
}
get_dep "$1" "$2" "$3"

View File

@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 27
versionCode 2
versionName "1.1"
versionCode 4
versionName "1.3"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.RemoteException;
import android.view.IRotationWatcher;
@ -20,7 +21,7 @@ public final class Device {
private RotationListener rotationListener;
public Device(Options options) {
screenInfo = computeScreenInfo(options.getMaxSize());
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) throws RemoteException {
@ -40,18 +41,40 @@ public final class Device {
return screenInfo;
}
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {
if (rotated) {
// the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop);
}
if (!contentRect.intersect(crop)) {
// intersect() changes contentRect so that it is intersected with crop
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
contentRect = new Rect(); // empty
}
}
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotated);
}
private static String formatCrop(Rect rect) {
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
}
@SuppressWarnings("checkstyle:MagicNumber")
private ScreenInfo computeScreenInfo(int maxSize) {
private static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8
int h = deviceSize.getHeight() & ~7;
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");
@ -68,12 +91,12 @@ public final class Device {
w = portrait ? minor : major;
h = portrait ? major : minor;
}
Size videoSize = new Size(w, h);
return new ScreenInfo(deviceSize, videoSize, rotated);
return new Size(w, h);
}
public Point getPhysicalPoint(Position position) {
@SuppressWarnings("checkstyle:HiddenField") // it hides the field on purpose, to read it with a lock
// it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
@ -82,10 +105,10 @@ public final class Device {
// the device may have been rotated since the event was generated, so ignore the event
return null;
}
Size deviceSize = screenInfo.getDeviceSize();
Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint();
int scaledX = point.x * deviceSize.getWidth() / videoSize.getWidth();
int scaledY = point.y * deviceSize.getHeight() / videoSize.getHeight();
int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY);
}
@ -108,4 +131,8 @@ public final class Device {
public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener;
}
static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}
}

View File

@ -1,9 +1,12 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
public class Options {
private int maxSize;
private int bitRate;
private boolean tunnelForward;
private Rect crop;
public int getMaxSize() {
return maxSize;
@ -28,4 +31,12 @@ public class Options {
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
}

View File

@ -56,12 +56,12 @@ public class ScreenEncoder implements Device.RotationListener {
do {
MediaCodec codec = createCodec();
IBinder display = createDisplay();
Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect();
Rect contentRect = device.getScreenInfo().getContentRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, deviceRect, videoRect);
setDisplaySurface(display, surface, contentRect, videoRect);
codec.start();
try {
alive = encode(codec, outputStream);

View File

@ -1,18 +1,20 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
public final class ScreenInfo {
private final Size deviceSize;
private final Rect contentRect; // device size, possibly cropped
private final Size videoSize;
private final boolean rotated;
public ScreenInfo(Size deviceSize, Size videoSize, boolean rotated) {
this.deviceSize = deviceSize;
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) {
this.contentRect = contentRect;
this.videoSize = videoSize;
this.rotated = rotated;
}
public Size getDeviceSize() {
return deviceSize;
public Rect getContentRect() {
return contentRect;
}
public Size getVideoSize() {
@ -24,6 +26,6 @@ public final class ScreenInfo {
if (rotated == newRotated) {
return this;
}
return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), newRotated);
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
}
}

View File

@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.io.IOException;
public final class Server {
@ -63,9 +65,31 @@ public final class Server {
boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward);
if (args.length < 4) {
return options;
}
Rect crop = parseCrop(args[3]);
options.setCrop(crop);
return options;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override

View File

@ -3,6 +3,9 @@ package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
@ -10,8 +13,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
public class ControlEventReaderTest {