Compare commits
131 Commits
Author | SHA1 | Date | |
---|---|---|---|
2dc6a2e30c | |||
80233b7b72 | |||
60c882841f | |||
348c478359 | |||
c90247ffb6 | |||
f31010f4ee | |||
280891837e | |||
49b2e63d13 | |||
c12c64ed41 | |||
2f66acd75d | |||
1846d2f078 | |||
1a0139321b | |||
8890750681 | |||
aac9d5057f | |||
f705a73149 | |||
27a9bd3424 | |||
cf121a0f65 | |||
c8a5f9dc63 | |||
8106bb8215 | |||
f1b3a40375 | |||
a63dd47f2d | |||
6b4bbb1fb3 | |||
aedc2c2da9 | |||
106b87a4d2 | |||
7d68316f45 | |||
e729b76176 | |||
6274cc3767 | |||
9c6f9b24f9 | |||
e2a2973990 | |||
41419a2e77 | |||
e3d0a59c80 | |||
bb3a7f05ac | |||
ec02823045 | |||
435a7fe314 | |||
78da66f126 | |||
9aa88b6fc3 | |||
f8ad7df3be | |||
0871bca9c7 | |||
b2b5404883 | |||
1bddb80b5f | |||
6323f3974f | |||
16a3de1796 | |||
6161f7688c | |||
2a02fb3611 | |||
71f50fb697 | |||
82efff34e8 | |||
860006e082 | |||
57eaf05289 | |||
dd2a5c1ecf | |||
66ec252893 | |||
b13d25b9f4 | |||
b449c09442 | |||
8b84492830 | |||
4d50832f8e | |||
e0e8dfeb3b | |||
f4d6449af7 | |||
64963fff62 | |||
b7d9b8739c | |||
88f6b43c8f | |||
324a264233 | |||
3bb2cda955 | |||
35298bb0c6 | |||
73c332e3e4 | |||
15014f2689 | |||
29b5c5b8f4 | |||
88ee6bc928 | |||
35e9a64c34 | |||
9cac38fe58 | |||
301c52b603 | |||
f00c6c5b13 | |||
3b3803da0d | |||
f5cf6c1b2c | |||
2573df9727 | |||
c65cb36d3b | |||
821ec9843c | |||
f16bd88802 | |||
f3e8834a3c | |||
080df5eb5d | |||
047179f232 | |||
2992dda497 | |||
6406f64edc | |||
f3f19d4e1d | |||
d744837f13 | |||
f7bc0bd5b5 | |||
8a3c6a3ae7 | |||
c530d95881 | |||
0b1e59186f | |||
e69f6f710d | |||
b858204786 | |||
e8510a8cc3 | |||
1038bad385 | |||
2b3ed5bcdb | |||
9e328ef98b | |||
f9a63ec272 | |||
f6d0893316 | |||
ed65cd72fd | |||
70599998eb | |||
e87cd175cc | |||
c075ad0a1e | |||
dac7196bd6 | |||
14b15ceb06 | |||
b9bb4ff740 | |||
cc4a015256 | |||
8476b4aab8 | |||
a1491862e4 | |||
c87d94ee27 | |||
675704c71c | |||
9396ea6d42 | |||
f9562f537a | |||
a34fbd23e9 | |||
e8b8a570e7 | |||
0e9a76c0c4 | |||
f9f305d19d | |||
727d1ef1e2 | |||
c2ac6fe7bd | |||
e2a7abcd53 | |||
e4d64e8752 | |||
fffeedffda | |||
82b4acee73 | |||
633b18d786 | |||
ab780ce26d | |||
84ad6633a6 | |||
1b0cea61a5 | |||
42f6341a14 | |||
acd2dc3183 | |||
db396f2138 | |||
e6feb991db | |||
e3f5d3b49b | |||
a7979e4e74 | |||
38b56f552e | |||
8ace3d1781 |
238
DEVELOP.md
Normal file
238
DEVELOP.md
Normal file
@ -0,0 +1,238 @@
|
||||
# scrcpy for developers
|
||||
|
||||
## Overview
|
||||
|
||||
This application is composed of two parts:
|
||||
- the server (`scrcpy-server.jar`), to be executed on the device,
|
||||
- the client (the `scrcpy` binary), executed on the host computer.
|
||||
|
||||
The client is responsible to push the server to the device and start its
|
||||
execution.
|
||||
|
||||
Once the client and the server are connected to each other, the server initially
|
||||
sends device information (name and initial screen dimensions), then starts to
|
||||
send a raw H.264 video stream of the device screen. The client decodes the video
|
||||
frames, and display them as soon as possible, without buffering, to minimize
|
||||
latency. The client is not aware of the device rotation (which is handled by the
|
||||
server), it just knows the dimensions of the video frames.
|
||||
|
||||
The client captures relevant keyboard and mouse events, that it transmits to the
|
||||
server, which injects them to the device.
|
||||
|
||||
|
||||
|
||||
## Server
|
||||
|
||||
|
||||
### Privileges
|
||||
|
||||
Capturing the screen requires some privileges, which are granted to `shell`.
|
||||
|
||||
The server is a Java application (with a [`public static void main(String...
|
||||
args)`][main] method), compiled against the Android framework, and executed as
|
||||
`shell` on the Android device.
|
||||
|
||||
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
|
||||
|
||||
To run such a Java application, the classes must be [_dexed_][dex] (typically,
|
||||
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
|
||||
`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run
|
||||
with:
|
||||
|
||||
adb shell CLASSPATH=/data/local/tmp/classes.dex \
|
||||
app_process / my.package.MainClass
|
||||
|
||||
_The path `/data/local/tmp` is a good candidate to push the server, since it's
|
||||
readable and writable by `shell`, but not world-writable, so a malicious
|
||||
application may not replace the server just before the client executes it._
|
||||
|
||||
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
|
||||
build system, the server is built to an (unsigned) APK (renamed to
|
||||
`scrcpy-server.jar`).
|
||||
|
||||
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
|
||||
[apk]: https://en.wikipedia.org/wiki/Android_application_package
|
||||
|
||||
|
||||
### Hidden methods
|
||||
|
||||
Although compiled against the Android framework, [hidden] methods and classes are
|
||||
not directly accessible (and they may differ from one Android version to
|
||||
another).
|
||||
|
||||
They can be called using reflection though. The communication with hidden
|
||||
components is provided by [_wrappers_ classes][wrappers] and [aidl].
|
||||
|
||||
[hidden]: https://stackoverflow.com/a/31908373/1987178
|
||||
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers
|
||||
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view
|
||||
|
||||
|
||||
### Threading
|
||||
|
||||
The server uses 2 threads:
|
||||
|
||||
- the **main** thread, encoding and streaming the video to the client;
|
||||
- the **controller** thread, listening for _control events_ (typically,
|
||||
keyboard and mouse events) from the client.
|
||||
|
||||
Since the video encoding is typically hardware, there would be no benefit in
|
||||
encoding and streaming in two different threads.
|
||||
|
||||
|
||||
### Screen video encoding
|
||||
|
||||
The encoding is managed by [`ScreenEncoder`].
|
||||
|
||||
The video is encoded using the [`MediaCodec`] API. The codec takes its input
|
||||
from a [surface] associated to the display, and writes the resulting H.264
|
||||
stream to the provided output stream (the socket connected to the client).
|
||||
|
||||
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
|
||||
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
|
||||
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
|
||||
|
||||
On device [rotation], the codec, surface and display are reinitialized, and a
|
||||
new video stream is produced.
|
||||
|
||||
New frames are produced only when changes occur on the surface. This is good
|
||||
because it avoids to send unnecessary frames, but there are drawbacks:
|
||||
|
||||
- it does not send any frame on start if the device screen does not change,
|
||||
- after fast motion changes, the last frame may have poor quality.
|
||||
|
||||
Both problems are [solved][repeat] by the flag
|
||||
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
|
||||
|
||||
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
|
||||
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
|
||||
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
|
||||
|
||||
|
||||
### Input events injection
|
||||
|
||||
_Control events_ are received from the client by the [`EventController`] (run in
|
||||
a separate thread). There are 5 types of input events:
|
||||
- keycode (cf [`KeyEvent`]),
|
||||
- text (special characters may not be handled by keycodes directly),
|
||||
- mouse motion/click,
|
||||
- mouse scroll,
|
||||
- custom command (e.g. to switch the screen on).
|
||||
|
||||
All of them may need to inject input events to the system. To do so, they use
|
||||
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
|
||||
[`InputManager` wrapper][inject-wrapper]).
|
||||
|
||||
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
|
||||
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
|
||||
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
|
||||
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
|
||||
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
|
||||
|
||||
|
||||
|
||||
## Client
|
||||
|
||||
The client relies on [SDL], which provides cross-platform API for UI, input
|
||||
events, threading, etc.
|
||||
|
||||
The video stream is decoded by [libav] (FFmpeg).
|
||||
|
||||
[SDL]: https://www.libsdl.org
|
||||
[libav]: https://www.libav.org/
|
||||
|
||||
### Initialization
|
||||
|
||||
On startup, in addition to _libav_ and _SDL_ initialization, the client must
|
||||
push and start the server on the device, and open a socket so that they may
|
||||
communicate.
|
||||
|
||||
Note that the client-server roles are expressed at the application level:
|
||||
|
||||
- the server _serves_ video stream and handle requests from the client,
|
||||
- the client _controls_ the device through the server.
|
||||
|
||||
However, the roles are inverted at the network level:
|
||||
|
||||
- the client opens a server socket and listen on a port before starting the
|
||||
server,
|
||||
- the server connects to the client.
|
||||
|
||||
This role inversion guarantees that the connection will not fail due to race
|
||||
conditions, and avoids polling.
|
||||
|
||||
Once the server is connected, it sends the device information (name and initial
|
||||
screen dimensions). Thus, the client may init the window and renderer, before
|
||||
the first frame is available.
|
||||
|
||||
To minimize startup time, SDL initialization is performed while listening for
|
||||
the connection from the server (see commit [90a46b4]).
|
||||
|
||||
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
|
||||
|
||||
|
||||
### Threading
|
||||
|
||||
The client uses 3 threads:
|
||||
|
||||
- the **main** thread, executing the SDL event loop,
|
||||
- the **decoder** thread, decoding video frames,
|
||||
- the **controller** thread, sending _control events_ to the server.
|
||||
|
||||
|
||||
### Decoder
|
||||
|
||||
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264
|
||||
stream from the socket, and notifies the main thread when a new frame is
|
||||
available.
|
||||
|
||||
There are two [frames] simultaneously in memory:
|
||||
- the **decoding** frame, written by the decoder from the decoder thread,
|
||||
- the **rendering** frame, rendered in a texture from the main thread.
|
||||
|
||||
When a new decoded frame is available, the decoder _swaps_ the decoding and
|
||||
rendering frame (with proper synchronization). Thus, it immediatly starts
|
||||
to decode a new frame while the main thread renders the last one.
|
||||
|
||||
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c
|
||||
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h
|
||||
|
||||
|
||||
### Controller
|
||||
|
||||
The [controller] is responsible to send _control events_ to the device. It runs
|
||||
in a separate thread, to avoid I/O on the main thread.
|
||||
|
||||
On SDL event, received on the main thread, the [input manager][inputmanager]
|
||||
creates appropriate [_control events_][controlevent]. It is responsible to
|
||||
convert SDL events to Android events (using [convert]). It pushes the _control
|
||||
events_ to a blocking queue hold by the controller. On its own thread, the
|
||||
controller takes events from the queue, that it serializes and sends to the
|
||||
client.
|
||||
|
||||
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
|
||||
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
|
||||
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
|
||||
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
|
||||
|
||||
|
||||
### UI and event loop
|
||||
|
||||
Initialization, input events and rendering are all [managed][scrcpy] in the main
|
||||
thread.
|
||||
|
||||
Events are handled in the [event loop], which either updates the [screen] or
|
||||
delegates to the [input manager][inputmanager].
|
||||
|
||||
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c
|
||||
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
|
||||
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
|
||||
|
||||
|
||||
## Hack
|
||||
|
||||
For more details, go read the code!
|
||||
|
||||
If you find a bug, or have an awesome idea to implement, please discuss and
|
||||
contribute ;-)
|
69
FAQ.md
Normal file
69
FAQ.md
Normal file
@ -0,0 +1,69 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Common issues
|
||||
|
||||
The application is very young, it is not unlikely that you encounter problems
|
||||
with it.
|
||||
|
||||
Here are the common reported problems and their status.
|
||||
|
||||
|
||||
### 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:
|
||||
|
||||
adb devices
|
||||
|
||||
Windows may need some [drivers] to detect your device.
|
||||
|
||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||
|
||||
If you still encounter problems, please see [issue 9].
|
||||
|
||||
[issue 9]: https://github.com/Genymobile/scrcpy/issues/9
|
||||
|
||||
|
||||
### I get a black screen for some applications like Silence
|
||||
|
||||
This is expected, they requested to protect the screen.
|
||||
|
||||
In [Silence], you can disable it in settings → Privacy → Screen security.
|
||||
|
||||
[silence]: https://f-droid.org/en/packages/org.smssecure.smssecure/
|
||||
|
||||
See [issue 36].
|
||||
|
||||
[issue 36]: https://github.com/Genymobile/scrcpy/issues/36
|
||||
|
||||
|
||||
### Mouse clicks do not work
|
||||
|
||||
On some devices, you may need to enable an option to allow [simulating input].
|
||||
|
||||
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
### Mouse clicks at wrong location
|
||||
|
||||
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
||||
scaled. See [issue 15].
|
||||
|
||||
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
|
||||
|
||||
A workaround is to build with HiDPI support disabled:
|
||||
|
||||
```bash
|
||||
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
|
62
Makefile
62
Makefile
@ -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
136
Makefile.CrossWindows
Normal 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
|
424
README.md
424
README.md
@ -1,141 +1,385 @@
|
||||
# ScrCpy
|
||||
# scrcpy (v1.2)
|
||||
|
||||
This project displays screens of Android devices plugged on USB in live.
|
||||
This application provides display and control of Android devices connected on
|
||||
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
||||
It works on _GNU/Linux_, _Windows_ and _MacOS_.
|
||||
|
||||

|
||||
|
||||
|
||||
## Run
|
||||
## Requirements
|
||||
|
||||
### Runtime requirements
|
||||
The Android part requires at least API 21 (Android 5.0).
|
||||
|
||||
This projects requires _FFmpeg_, _LibSDL2_ and _LibSDL2-net_.
|
||||
You need [adb]. It is available in the [Android SDK platform
|
||||
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
|
||||
|
||||
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]. 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
|
||||
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
|
||||
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
|
||||
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
|
||||
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
|
||||
|
||||
## Build and install
|
||||
|
||||
### System-specific steps
|
||||
|
||||
#### Linux
|
||||
|
||||
Install the packages from your package manager. For example, on Debian:
|
||||
Install the required packages from your package manager.
|
||||
|
||||
sudo apt install ffmpeg libsdl2-2.0.0 libsdl2-net-2.0.0
|
||||
##### Debian/Ubuntu
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
sudo apt install ffmpeg libsdl2-2.0.0
|
||||
|
||||
# client build dependencies
|
||||
sudo apt install make gcc pkg-config meson \
|
||||
libavcodec-dev libavformat-dev libavutil-dev \
|
||||
libsdl2-dev
|
||||
|
||||
# server build dependencies
|
||||
sudo apt install openjdk-8-jdk
|
||||
```
|
||||
|
||||
##### Fedora
|
||||
|
||||
```bash
|
||||
# enable RPM fusion free
|
||||
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
|
||||
|
||||
# client build dependencies
|
||||
sudo dnf install SDL2-devel ffms2-devel meson gcc make
|
||||
|
||||
# server build dependencies
|
||||
sudo dnf install java
|
||||
```
|
||||
|
||||
##### Arch Linux
|
||||
|
||||
Two [AUR] packages have been created by users:
|
||||
|
||||
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
|
||||
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
|
||||
|
||||
#### Windows
|
||||
|
||||
From [MSYS2]:
|
||||
For Windows, for simplicity, prebuilt archives with all the dependencies
|
||||
(including `adb`) are available:
|
||||
|
||||
pacman -S mingw-w64-x86_64-SDL2
|
||||
pacman -S mingw-w64-x86_64-SDL2_net
|
||||
pacman -S mingw-w64-x86_64-ffmpeg
|
||||
- [`scrcpy-win32-v1.2.zip`][direct-win32].
|
||||
_(SHA-256: a1fe1de67ec75dcf970ca5d97a04c26ff0f2d61871f2ef51b6f2f0bf666966b2)_
|
||||
- [`scrcpy-win64-v1.2.zip`][direct-win64].
|
||||
_(SHA-256: 35ae3bcee51771e7c51b8a8be87aef2295c9f267606a7cf83ebb0a4d583ef536)_
|
||||
|
||||
[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
|
||||
|
||||
Instead, you may want to build it manually.
|
||||
|
||||
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/
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
pacman -S mingw-w64-x86_64-SDL2 \
|
||||
mingw-w64-x86_64-ffmpeg
|
||||
|
||||
#### MacOS
|
||||
# client build dependencies
|
||||
pacman -S mingw-w64-x86_64-make \
|
||||
mingw-w64-x86_64-gcc \
|
||||
mingw-w64-x86_64-pkg-config \
|
||||
mingw-w64-x86_64-meson
|
||||
```
|
||||
|
||||
TODO
|
||||
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`:
|
||||
|
||||
```bash
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
```
|
||||
|
||||
#### Mac OS
|
||||
|
||||
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:
|
||||
|
||||
|
||||
## Build
|
||||
```bash
|
||||
# runtime dependencies
|
||||
brew install sdl2 ffmpeg
|
||||
|
||||
The project is divided into two parts:
|
||||
- the server, running on the device (in `server/`);
|
||||
- the client, running on the computer (in `app/`).
|
||||
# client build dependencies
|
||||
brew install pkg-config meson
|
||||
```
|
||||
|
||||
The server is a raw Java project requiring Android SDK. It not an Android
|
||||
project: the target file is a `.jar`, and a `main()` method is executed with
|
||||
_shell_ rights.
|
||||
Additionally, if you want to build the server, install Java 8 from Caskroom, and
|
||||
make it avaliable from the `PATH`:
|
||||
|
||||
The client is a C project using [SDL] and [FFmpeg], built with [Meson]/[Ninja].
|
||||
```bash
|
||||
brew tap caskroom/versions
|
||||
brew cask install java8
|
||||
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
```
|
||||
|
||||
The root directory contains a `Makefile` to build both parts.
|
||||
#### Docker
|
||||
|
||||
[sdl]: https://www.libsdl.org/
|
||||
[ffmpeg]: https://www.ffmpeg.org/
|
||||
[meson]: http://mesonbuild.com/
|
||||
[ninja]: https://ninja-build.org/
|
||||
See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
|
||||
|
||||
### Common steps
|
||||
|
||||
### Build requirements
|
||||
|
||||
Install the [Android SDK], the JDK 8 (`openjdk-8-jdk`), and the packages
|
||||
described below.
|
||||
Install the [Android SDK] (_Android Studio_), and set `ANDROID_HOME` to
|
||||
its directory. For example:
|
||||
|
||||
[Android SDK]: https://developer.android.com/studio/index.html
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME=~/android/sdk
|
||||
```
|
||||
|
||||
#### Linux
|
||||
Clone the project:
|
||||
|
||||
sudo apt install make gcc openjdk-8-jdk pkg-config meson zip \
|
||||
libavcodec-dev libavformat-dev libavutil-dev \
|
||||
libsdl2-dev libsdl2-net-dev
|
||||
```bash
|
||||
git clone https://github.com/Genymobile/scrcpy
|
||||
cd scrcpy
|
||||
```
|
||||
|
||||
Then, build:
|
||||
|
||||
```bash
|
||||
meson x --buildtype release --strip -Db_lto=true
|
||||
cd x
|
||||
ninja
|
||||
```
|
||||
|
||||
You can test it from here:
|
||||
|
||||
```bash
|
||||
ninja run
|
||||
```
|
||||
|
||||
Or you can install it on the system:
|
||||
|
||||
```bash
|
||||
sudo ninja install # without sudo on Windows
|
||||
```
|
||||
|
||||
This installs two files:
|
||||
|
||||
- `/usr/local/bin/scrcpy`
|
||||
- `/usr/local/share/scrcpy/scrcpy-server.jar`
|
||||
|
||||
Just remove them to "uninstall" the application.
|
||||
|
||||
|
||||
#### Windows
|
||||
#### Prebuilt server
|
||||
|
||||
Install these packages:
|
||||
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:
|
||||
|
||||
pacman -S mingw-w64-x86_64-make
|
||||
pacman -S mingw-w64-x86_64-gcc
|
||||
pacman -S mingw-w64-x86_64-pkg-config
|
||||
pacman -S mingw-w64-x86_64-meson
|
||||
pacman -S zip
|
||||
- [`scrcpy-server-v1.2.jar`][direct-scrcpy-server].
|
||||
_(SHA-256: cb39654ed2fda3d30ddff292806950ccc5c394375ea12b974f790c7f38f61f60)_
|
||||
|
||||
Java 8 is not available in MSYS2, so install it manually and make it available
|
||||
from the `PATH`:
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.2/scrcpy-server-v1.2.jar
|
||||
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
In that case, the build does not require Java or the Android SDK.
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
### Build
|
||||
|
||||
Make sure your `ANDROID_HOME` variable is set to your Android SDK directory:
|
||||
|
||||
export ANDROID_HOME=~/android/sdk
|
||||
|
||||
From the project root directory, execute:
|
||||
|
||||
make build
|
||||
|
||||
To run the build:
|
||||
|
||||
make run
|
||||
|
||||
It is also pass arguments to `scrcpy` via `make`:
|
||||
|
||||
make run ARGS="-p 1234"
|
||||
|
||||
The purpose of this command is to execute `scrcpy` during the development.
|
||||
|
||||
|
||||
### Test
|
||||
|
||||
To execute unit tests:
|
||||
|
||||
make test
|
||||
|
||||
The server-side tests require JUnit 4:
|
||||
|
||||
sudo apt install junit4
|
||||
|
||||
|
||||
### Generate a release
|
||||
|
||||
From the project root directory, execute:
|
||||
|
||||
make release
|
||||
|
||||
This will generate the application in `dist/scrcpy/`.
|
||||
|
||||
```bash
|
||||
meson x --buildtype release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=/path/to/scrcpy-server.jar
|
||||
cd x
|
||||
ninja
|
||||
sudo ninja install
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Plug a device, and from `dist/scrcpy/`, execute:
|
||||
_At runtime, `adb` must be accessible from your `PATH`._
|
||||
|
||||
./scrcpy
|
||||
If everything is ok, just plug an Android device, and execute:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
It accepts command-line arguments, listed by:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
For example, to decrease video bitrate to 2Mbps (default is 8Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy -b 2M
|
||||
```
|
||||
|
||||
To limit the video dimensions (e.g. if the device is 2540×1440, but the host
|
||||
screen is smaller, or cannot decode such a high definition):
|
||||
|
||||
```bash
|
||||
scrcpy -m 1024
|
||||
```
|
||||
|
||||
If several devices are listed in `adb devices`, you must specify the _serial_:
|
||||
|
||||
./scrcpy 0123456789abcdef
|
||||
```bash
|
||||
scrcpy -s 0123456789abcdef
|
||||
```
|
||||
|
||||
To change the default port (useful to launch several `scrcpy` simultaneously):
|
||||
To show physical touches while scrcpy is running:
|
||||
|
||||
./scrcpy -p 1234
|
||||
```bash
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
Other options are available, check `scrcpy --help`.
|
||||
To run without installing:
|
||||
|
||||
```bash
|
||||
./run x [options]
|
||||
```
|
||||
|
||||
(where `x` is your build directory).
|
||||
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Action | Shortcut |
|
||||
| -------------------------------------- |:---------------------------- |
|
||||
| switch fullscreen mode | `Ctrl`+`f` |
|
||||
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
|
||||
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
|
||||
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
|
||||
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
|
||||
| click on `APP_SWITCH` | `Ctrl`+`s` |
|
||||
| click on `MENU` | `Ctrl`+`m` |
|
||||
| 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._
|
||||
|
||||
|
||||
## Why _scrcpy_?
|
||||
|
||||
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
|
||||
|
||||
[`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een.
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## Common issues
|
||||
|
||||
See the [FAQ](FAQ.md).
|
||||
|
||||
|
||||
## Developers
|
||||
|
||||
Read the [developers page].
|
||||
|
||||
[developers page]: DEVELOP.md
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## Articles
|
||||
|
||||
- [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/
|
||||
|
@ -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', '0.1')
|
||||
conf.set_quoted('SCRCPY_VERSION', '1.2')
|
||||
|
||||
# the prefix used during configuration (meson --prefix=PREFIX)
|
||||
conf.set_quoted('PREFIX', get_option('prefix'))
|
||||
@ -73,16 +116,32 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
|
||||
|
||||
# the default video bitrate, in bits/second
|
||||
# overridden by option --bit-rate
|
||||
conf.set('DEFAULT_BIT_RATE', '4000000') # 4Mbps
|
||||
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||
|
||||
# whether the app should always display the most recent available frame, even
|
||||
# if the previous one has not been displayed
|
||||
# SKIP_FRAMES improves latency at the cost of framerate
|
||||
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')
|
||||
|
||||
executable('scrcpy', src, dependencies: dependencies, install: true)
|
||||
src_dir = include_directories('src')
|
||||
|
||||
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
|
||||
@ -93,8 +152,6 @@ tests = [
|
||||
['test_strutil', ['tests/test_strutil.c', 'src/strutil.c']],
|
||||
]
|
||||
|
||||
src_dir = include_directories('src')
|
||||
|
||||
foreach t : tests
|
||||
exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies)
|
||||
test(t[0], exe)
|
||||
|
@ -4,10 +4,9 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "log.h"
|
||||
|
||||
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
||||
|
||||
static const char *adb_command;
|
||||
|
||||
static inline const char *get_adb_command() {
|
||||
@ -45,6 +44,13 @@ process_t adb_forward(const char *serial, uint16_t local_port, const char *devic
|
||||
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||
}
|
||||
|
||||
process_t adb_forward_remove(const char *serial, uint16_t local_port) {
|
||||
char local[4 + 5 + 1]; // tcp:PORT
|
||||
sprintf(local, "tcp:%" PRIu16, local_port);
|
||||
const char *const adb_cmd[] = {"forward", "--remove", local};
|
||||
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||
}
|
||||
|
||||
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
|
||||
char local[4 + 5 + 1]; // tcp:PORT
|
||||
char remote[108 + 14 + 1]; // localabstract:NAME
|
||||
@ -66,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("ed[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));
|
||||
|
@ -7,12 +7,11 @@
|
||||
|
||||
// <https://stackoverflow.com/a/44383330/1987178>
|
||||
#ifdef _WIN32
|
||||
# define PRIexitcode "lu"
|
||||
# ifdef _WIN64
|
||||
# define PRIsizet PRIu64
|
||||
# define PRIexitcode "lu"
|
||||
# else
|
||||
# define PRIsizet PRIu32
|
||||
# define PRIexitcode "u"
|
||||
# endif
|
||||
#else
|
||||
# define PRIsizet "zu"
|
||||
@ -39,9 +38,11 @@ SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
|
||||
|
||||
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
|
||||
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
|
||||
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
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#include <SDL2/SDL_stdinc.h>
|
||||
|
||||
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
||||
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
|
||||
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
#include "controlevent.h"
|
||||
|
||||
#include <SDL2/SDL_stdinc.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lockutil.h"
|
||||
#include "log.h"
|
||||
@ -36,11 +37,12 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
|
||||
// write length (1 byte) + date (non nul-terminated)
|
||||
size_t len = strlen(event->text_event.text);
|
||||
if (len > TEXT_MAX_LENGTH) {
|
||||
// injecting a text takes time, so limit the text length
|
||||
len = TEXT_MAX_LENGTH;
|
||||
}
|
||||
buf[1] = (Uint8) len;
|
||||
memcpy(&buf[2], &event->text_event.text, len);
|
||||
return 2 + len;
|
||||
write16(&buf[1], (Uint16) len);
|
||||
memcpy(&buf[3], event->text_event.text, len);
|
||||
return 3 + len;
|
||||
}
|
||||
case CONTROL_EVENT_TYPE_MOUSE:
|
||||
buf[1] = event->mouse_event.action;
|
||||
@ -61,6 +63,12 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
|
||||
}
|
||||
}
|
||||
|
||||
void control_event_destroy(struct control_event *event) {
|
||||
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
|
||||
SDL_free(event->text_event.text);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
|
||||
return queue->head == queue->tail;
|
||||
}
|
||||
@ -77,7 +85,11 @@ SDL_bool control_event_queue_init(struct control_event_queue *queue) {
|
||||
}
|
||||
|
||||
void control_event_queue_destroy(struct control_event_queue *queue) {
|
||||
// nothing to do in the current implementation
|
||||
int i = queue->tail;
|
||||
while (i != queue->head) {
|
||||
control_event_destroy(&queue->data[i]);
|
||||
i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {
|
||||
|
@ -9,8 +9,8 @@
|
||||
#include "common.h"
|
||||
|
||||
#define CONTROL_EVENT_QUEUE_SIZE 64
|
||||
#define SERIALIZED_EVENT_MAX_SIZE 33
|
||||
#define TEXT_MAX_LENGTH 31
|
||||
#define TEXT_MAX_LENGTH 300
|
||||
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
|
||||
|
||||
enum control_event_type {
|
||||
CONTROL_EVENT_TYPE_KEYCODE,
|
||||
@ -20,7 +20,7 @@ enum control_event_type {
|
||||
CONTROL_EVENT_TYPE_COMMAND,
|
||||
};
|
||||
|
||||
#define CONTROL_EVENT_COMMAND_SCREEN_ON 0
|
||||
#define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0
|
||||
|
||||
struct control_event {
|
||||
enum control_event_type type;
|
||||
@ -31,7 +31,7 @@ struct control_event {
|
||||
enum android_metastate metastate;
|
||||
} keycode_event;
|
||||
struct {
|
||||
char text[TEXT_MAX_LENGTH + 1]; // nul-terminated string
|
||||
char *text; // owned, to be freed by SDL_free()
|
||||
} text_event;
|
||||
struct {
|
||||
enum android_motionevent_action action;
|
||||
@ -68,4 +68,6 @@ SDL_bool control_event_queue_is_full(const struct control_event_queue *queue);
|
||||
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event);
|
||||
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event);
|
||||
|
||||
void control_event_destroy(struct control_event *event);
|
||||
|
||||
#endif
|
||||
|
@ -54,25 +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)) {
|
||||
if (!process_event(controller, &event)) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
@ -145,8 +146,8 @@ SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
|
||||
|
||||
to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
|
||||
to->mouse_event.position.screen_size = screen_size;
|
||||
to->mouse_event.position.point.x = (Uint16) from->x;
|
||||
to->mouse_event.position.point.y = (Uint16) from->y;
|
||||
to->mouse_event.position.point.x = from->x;
|
||||
to->mouse_event.position.point.y = from->y;
|
||||
|
||||
return SDL_TRUE;
|
||||
}
|
||||
@ -172,7 +173,10 @@ SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
|
||||
to->scroll_event.position = position;
|
||||
|
||||
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
|
||||
to->scroll_event.hscroll = mul * from->x;
|
||||
// SDL behavior seems inconsistent between horizontal and vertical scrolling
|
||||
// so reverse the horizontal
|
||||
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
|
||||
to->scroll_event.hscroll = -mul * from->x;
|
||||
to->scroll_event.vscroll = mul * from->y;
|
||||
|
||||
return SDL_TRUE;
|
||||
|
@ -40,37 +40,33 @@ static void notify_stopped(void) {
|
||||
|
||||
static int run_decoder(void *data) {
|
||||
struct decoder *decoder = data;
|
||||
int ret = 0;
|
||||
|
||||
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
if (!codec) {
|
||||
LOGE("H.264 decoder not found");
|
||||
return -1;
|
||||
goto run_end;
|
||||
}
|
||||
|
||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!codec_ctx) {
|
||||
LOGC("Could not allocate decoder context");
|
||||
return -1;
|
||||
goto run_end;
|
||||
}
|
||||
|
||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Could not open H.264 codec");
|
||||
ret = -1;
|
||||
goto run_finally_free_codec_ctx;
|
||||
}
|
||||
|
||||
AVFormatContext *format_ctx = avformat_alloc_context();
|
||||
if (!format_ctx) {
|
||||
LOGC("Could not allocate format context");
|
||||
ret = -1;
|
||||
goto run_finally_close_codec;
|
||||
}
|
||||
|
||||
unsigned char *buffer = av_malloc(BUFSIZE);
|
||||
if (!buffer) {
|
||||
LOGC("Could not allocate buffer");
|
||||
ret = -1;
|
||||
goto run_finally_free_format_ctx;
|
||||
}
|
||||
|
||||
@ -80,7 +76,6 @@ static int run_decoder(void *data) {
|
||||
// avformat_open_input takes ownership of 'buffer'
|
||||
// so only free the buffer before avformat_open_input()
|
||||
av_free(buffer);
|
||||
ret = -1;
|
||||
goto run_finally_free_format_ctx;
|
||||
}
|
||||
|
||||
@ -88,7 +83,6 @@ static int run_decoder(void *data) {
|
||||
|
||||
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
|
||||
LOGE("Could not open video stream");
|
||||
ret = -1;
|
||||
goto run_finally_free_avio_ctx;
|
||||
}
|
||||
|
||||
@ -97,10 +91,25 @@ static int run_decoder(void *data) {
|
||||
packet.data = NULL;
|
||||
packet.size = 0;
|
||||
|
||||
while (!av_read_frame(format_ctx, &packet) && !avio_ctx->eof_reached) {
|
||||
while (!av_read_frame(format_ctx, &packet)) {
|
||||
// the new decoding/encoding API has been introduced by:
|
||||
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
|
||||
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 37, 0)
|
||||
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0)
|
||||
int ret;
|
||||
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
|
||||
LOGE("Could not send video packet: %d", ret);
|
||||
goto run_quit;
|
||||
}
|
||||
ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame);
|
||||
if (!ret) {
|
||||
// a frame was received
|
||||
push_frame(decoder);
|
||||
} else if (ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Could not receive video frame: %d", ret);
|
||||
av_packet_unref(&packet);
|
||||
goto run_quit;
|
||||
}
|
||||
#else
|
||||
while (packet.size > 0) {
|
||||
int got_picture;
|
||||
int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet);
|
||||
@ -114,19 +123,12 @@ static int run_decoder(void *data) {
|
||||
packet.size -= len;
|
||||
packet.data += len;
|
||||
}
|
||||
#else
|
||||
int ret;
|
||||
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
|
||||
LOGE("Could not send video packet: %d", ret);
|
||||
goto run_quit;
|
||||
}
|
||||
if ((ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame)) < 0) {
|
||||
LOGE("Could not receive video frame: %d", ret);
|
||||
goto run_quit;
|
||||
}
|
||||
|
||||
push_frame(decoder);
|
||||
#endif
|
||||
av_packet_unref(&packet);
|
||||
|
||||
if (avio_ctx->eof_reached) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOGD("End of frames");
|
||||
@ -142,7 +144,8 @@ run_finally_close_codec:
|
||||
run_finally_free_codec_ctx:
|
||||
avcodec_free_context(&codec_ctx);
|
||||
notify_stopped();
|
||||
return ret;
|
||||
run_end:
|
||||
return 0;
|
||||
}
|
||||
|
||||
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) {
|
||||
|
@ -4,10 +4,24 @@
|
||||
#include "lockutil.h"
|
||||
#include "log.h"
|
||||
|
||||
static struct point get_mouse_point(void) {
|
||||
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer coordinates (as provided in SDL mouse events)
|
||||
//
|
||||
// See my question:
|
||||
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
|
||||
static void convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
|
||||
SDL_Rect viewport;
|
||||
float scale_x, scale_y;
|
||||
SDL_RenderGetViewport(renderer, &viewport);
|
||||
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
|
||||
*x = (int) (*x / scale_x) - viewport.x;
|
||||
*y = (int) (*y / scale_y) - viewport.y;
|
||||
}
|
||||
|
||||
static struct point get_mouse_point(struct screen *screen) {
|
||||
int x;
|
||||
int y;
|
||||
SDL_GetMouseState(&x, &y);
|
||||
convert_to_renderer_coordinates(screen->renderer, &x, &y);
|
||||
SDL_assert_release(x >= 0 && x < 0x10000 && y >= 0 && y < 0x10000);
|
||||
return (struct point) {
|
||||
.x = (Uint16) x,
|
||||
@ -15,65 +29,66 @@ static struct point get_mouse_point(void) {
|
||||
};
|
||||
}
|
||||
|
||||
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 = {
|
||||
.type = CONTROL_EVENT_TYPE_KEYCODE,
|
||||
.keycode_event = {
|
||||
.action = AKEY_EVENT_ACTION_DOWN,
|
||||
.keycode = keycode,
|
||||
.metastate = 0,
|
||||
},
|
||||
};
|
||||
struct control_event control_event;
|
||||
control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
|
||||
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 void turn_screen_on(struct controller *controller) {
|
||||
struct control_event control_event = {
|
||||
.type = CONTROL_EVENT_TYPE_COMMAND,
|
||||
.command_event = {
|
||||
.action = CONTROL_EVENT_COMMAND_SCREEN_ON,
|
||||
},
|
||||
};
|
||||
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
|
||||
static void press_back_or_turn_screen_on(struct controller *controller) {
|
||||
struct control_event control_event;
|
||||
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
|
||||
control_event.command_event.action = CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON;
|
||||
|
||||
if (!controller_push_event(controller, &control_event)) {
|
||||
LOGW("Cannot turn screen on");
|
||||
}
|
||||
@ -91,24 +106,36 @@ static void switch_fps_counter_state(struct frames *frames) {
|
||||
mutex_unlock(frames->mutex);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
static void clipboard_paste(struct controller *controller) {
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
LOGW("Cannot get clipboard text: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
if (!*text) {
|
||||
// empty text
|
||||
SDL_free(text);
|
||||
return;
|
||||
}
|
||||
|
||||
struct control_event control_event;
|
||||
control_event.type = CONTROL_EVENT_TYPE_TEXT;
|
||||
strncpy(control_event.text_event.text, event->text, TEXT_MAX_LENGTH);
|
||||
control_event.text_event.text[TEXT_MAX_LENGTH] = '\0';
|
||||
control_event.text_event.text = text;
|
||||
if (!controller_push_event(controller, &control_event)) {
|
||||
SDL_free(text);
|
||||
LOGW("Cannot send clipboard paste event");
|
||||
}
|
||||
}
|
||||
|
||||
void input_manager_process_text_input(struct input_manager *input_manager,
|
||||
const SDL_TextInputEvent *event) {
|
||||
struct control_event control_event;
|
||||
control_event.type = CONTROL_EVENT_TYPE_TEXT;
|
||||
control_event.text_event.text = SDL_strdup(event->text);
|
||||
if (!control_event.text_event.text) {
|
||||
LOGW("Cannot strdup input text");
|
||||
return;
|
||||
}
|
||||
if (!controller_push_event(input_manager->controller, &control_event)) {
|
||||
LOGW("Cannot send text event");
|
||||
}
|
||||
@ -116,48 +143,78 @@ void input_manager_process_text_input(struct input_manager *input_manager,
|
||||
|
||||
void input_manager_process_key(struct input_manager *input_manager,
|
||||
const SDL_KeyboardEvent *event) {
|
||||
SDL_Keycode keycode = event->keysym.sym;
|
||||
SDL_bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
|
||||
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
|
||||
SDL_bool repeat = event->repeat;
|
||||
|
||||
// capture all Ctrl events
|
||||
if (ctrl) {
|
||||
// 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:
|
||||
if (!repeat) {
|
||||
action_app_switch(input_manager->controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_m:
|
||||
action_app_switch(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:
|
||||
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;
|
||||
}
|
||||
|
||||
@ -188,9 +245,26 @@ void input_manager_process_mouse_motion(struct input_manager *input_manager,
|
||||
|
||||
void input_manager_process_mouse_button(struct input_manager *input_manager,
|
||||
const SDL_MouseButtonEvent *event) {
|
||||
if (event->button == SDL_BUTTON_RIGHT && event->type == SDL_MOUSEBUTTONDOWN) {
|
||||
turn_screen_on(input_manager->controller);
|
||||
return;
|
||||
if (event->type == SDL_MOUSEBUTTONDOWN) {
|
||||
if (event->button == SDL_BUTTON_RIGHT) {
|
||||
press_back_or_turn_screen_on(input_manager->controller);
|
||||
return;
|
||||
}
|
||||
if (event->button == SDL_BUTTON_MIDDLE) {
|
||||
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
|
||||
return;
|
||||
}
|
||||
// double-click on black borders resize to fit the device screen
|
||||
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
||||
SDL_bool outside_device_screen =
|
||||
event->x < 0 || event->x >= input_manager->screen->frame_size.width ||
|
||||
event->y < 0 || event->y >= input_manager->screen->frame_size.height;
|
||||
if (outside_device_screen) {
|
||||
screen_resize_to_fit(input_manager->screen);
|
||||
return;
|
||||
}
|
||||
// otherwise, send the click event to the device
|
||||
}
|
||||
};
|
||||
struct control_event control_event;
|
||||
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
|
||||
@ -204,12 +278,12 @@ void input_manager_process_mouse_wheel(struct input_manager *input_manager,
|
||||
const SDL_MouseWheelEvent *event) {
|
||||
struct position position = {
|
||||
.screen_size = input_manager->screen->frame_size,
|
||||
.point = get_mouse_point(),
|
||||
.point = get_mouse_point(input_manager->screen),
|
||||
};
|
||||
struct control_event control_event;
|
||||
if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) {
|
||||
if (!controller_push_event(input_manager->controller, &control_event)) {
|
||||
LOGW("Cannot send wheel button event");
|
||||
LOGW("Cannot send mouse wheel event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
180
app/src/installer.c
Normal file
180
app/src/installer.c
Normal 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, ¤t_apk);
|
||||
SDL_assert(non_empty);
|
||||
#else
|
||||
apk_queue_take(&installer->queue, ¤t_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
40
app/src/installer.h
Normal 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
|
@ -12,6 +12,7 @@ struct args {
|
||||
const char *serial;
|
||||
SDL_bool help;
|
||||
SDL_bool version;
|
||||
SDL_bool show_touches;
|
||||
Uint16 port;
|
||||
Uint16 max_size;
|
||||
Uint32 bit_rate;
|
||||
@ -45,6 +46,10 @@ static void usage(const char *arg0) {
|
||||
" The device serial number. Mandatory only if several devices\n"
|
||||
" are connected to adb.\n"
|
||||
"\n"
|
||||
" -t, --show-touches\n"
|
||||
" Enable \"show touches\" on start, disable on quit.\n"
|
||||
" It only shows physical touches (not clicks from scrcpy).\n"
|
||||
"\n"
|
||||
" -v, --version\n"
|
||||
" Print the version of scrcpy.\n"
|
||||
"\n"
|
||||
@ -57,33 +62,45 @@ static void usage(const char *arg0) {
|
||||
" resize window to 1:1 (pixel-perfect)\n"
|
||||
"\n"
|
||||
" Ctrl+x\n"
|
||||
" Double-click on black borders\n"
|
||||
" resize window to remove black borders\n"
|
||||
"\n"
|
||||
" Ctrl+h\n"
|
||||
" Home\n"
|
||||
" Middle-click\n"
|
||||
" click on HOME\n"
|
||||
"\n"
|
||||
" Ctrl+b\n"
|
||||
" Ctrl+Backspace\n"
|
||||
" Right-click (when screen is on)\n"
|
||||
" click on BACK\n"
|
||||
"\n"
|
||||
" Ctrl+m\n"
|
||||
" Ctrl+s\n"
|
||||
" click on APP_SWITCH\n"
|
||||
"\n"
|
||||
" Ctrl+'+'\n"
|
||||
" Ctrl+m\n"
|
||||
" click on MENU\n"
|
||||
"\n"
|
||||
" Ctrl+Up\n"
|
||||
" click on VOLUME_UP\n"
|
||||
"\n"
|
||||
" Ctrl+'-'\n"
|
||||
" Ctrl+Down\n"
|
||||
" click on VOLUME_DOWN\n"
|
||||
"\n"
|
||||
" Ctrl+p\n"
|
||||
" click on POWER (turn screen on/off)\n"
|
||||
"\n"
|
||||
" Right-click\n"
|
||||
" Right-click (when screen is off)\n"
|
||||
" turn screen on\n"
|
||||
"\n"
|
||||
" Ctrl+v\n"
|
||||
" paste computer clipboard to device\n"
|
||||
"\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,
|
||||
@ -174,47 +191,45 @@ 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'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"max-size", required_argument, NULL, 'm'},
|
||||
{"port", required_argument, NULL, 'p'},
|
||||
{"serial", required_argument, NULL, 's'},
|
||||
{"version", no_argument, NULL, 'v'},
|
||||
{NULL, 0, NULL, 0 },
|
||||
{"bit-rate", required_argument, NULL, 'b'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"max-size", required_argument, NULL, 'm'},
|
||||
{"port", required_argument, NULL, 'p'},
|
||||
{"serial", required_argument, NULL, 's'},
|
||||
{"show-touches", no_argument, NULL, 't'},
|
||||
{"version", no_argument, NULL, 'v'},
|
||||
{NULL, 0, NULL, 0 },
|
||||
};
|
||||
int c;
|
||||
while ((c = getopt_long(argc, argv, "b:hm:p:s:v", long_options, NULL)) != -1) {
|
||||
while ((c = getopt_long(argc, argv, "b:hm:p:s:tv", long_options, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'b': {
|
||||
case 'b':
|
||||
if (!parse_bit_rate(optarg, &args->bit_rate)) {
|
||||
return SDL_FALSE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'h': {
|
||||
case 'h':
|
||||
args->help = SDL_TRUE;
|
||||
break;
|
||||
}
|
||||
case 'm': {
|
||||
case 'm':
|
||||
if (!parse_max_size(optarg, &args->max_size)) {
|
||||
return SDL_FALSE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'p': {
|
||||
case 'p':
|
||||
if (!parse_port(optarg, &args->port)) {
|
||||
return SDL_FALSE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 's': {
|
||||
case 's':
|
||||
args->serial = optarg;
|
||||
break;
|
||||
}
|
||||
case 'v': {
|
||||
case 't':
|
||||
args->show_touches = SDL_TRUE;
|
||||
break;
|
||||
case 'v':
|
||||
args->version = SDL_TRUE;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return SDL_FALSE;
|
||||
@ -230,10 +245,17 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
#ifdef __WINDOWS__
|
||||
// disable buffering, we want logs immediately
|
||||
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
|
||||
setbuf(stdout, NULL);
|
||||
setbuf(stderr, NULL);
|
||||
#endif
|
||||
struct args args = {
|
||||
.serial = NULL,
|
||||
.help = SDL_FALSE,
|
||||
.version = SDL_FALSE,
|
||||
.show_touches = SDL_FALSE,
|
||||
.port = DEFAULT_LOCAL_PORT,
|
||||
.max_size = DEFAULT_MAX_SIZE,
|
||||
.bit_rate = DEFAULT_BIT_RATE,
|
||||
@ -262,7 +284,14 @@ int main(int argc, char *argv[]) {
|
||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
||||
#endif
|
||||
|
||||
int res = scrcpy(args.serial, args.port, args.max_size, args.bit_rate) ? 0 : 1;
|
||||
struct scrcpy_options options = {
|
||||
.serial = args.serial,
|
||||
.port = args.port,
|
||||
.max_size = args.max_size,
|
||||
.bit_rate = args.bit_rate,
|
||||
.show_touches = args.show_touches,
|
||||
};
|
||||
int res = scrcpy(&options) ? 0 : 1;
|
||||
|
||||
avformat_network_deinit(); // ignore failure
|
||||
|
||||
|
@ -18,6 +18,26 @@
|
||||
typedef struct in_addr IN_ADDR;
|
||||
#endif
|
||||
|
||||
socket_t net_connect(Uint32 addr, Uint16 port) {
|
||||
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
perror("socket");
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
SOCKADDR_IN sin;
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_addr.s_addr = htonl(addr);
|
||||
sin.sin_port = htons(port);
|
||||
|
||||
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
|
||||
perror("connect");
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
return sock;
|
||||
}
|
||||
|
||||
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
|
||||
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock == INVALID_SOCKET) {
|
||||
@ -62,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;
|
||||
}
|
||||
|
@ -9,8 +9,6 @@
|
||||
#define SHUT_RD SD_RECEIVE
|
||||
#define SHUT_WR SD_SEND
|
||||
#define SHUT_RDWR SD_BOTH
|
||||
typedef SIZE_T size_t;
|
||||
typedef SSIZE_T ssize_t;
|
||||
typedef SOCKET socket_t;
|
||||
#else
|
||||
# include <sys/socket.h>
|
||||
@ -21,14 +19,15 @@
|
||||
SDL_bool net_init(void);
|
||||
void net_cleanup(void);
|
||||
|
||||
socket_t net_connect(Uint32 addr, Uint16 port);
|
||||
socket_t net_listen(Uint32 addr, Uint16 port, int backlog);
|
||||
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);
|
||||
|
120
app/src/scrcpy.c
120
app/src/scrcpy.c
@ -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,
|
||||
@ -35,7 +37,29 @@ static struct input_manager input_manager = {
|
||||
.screen = &screen,
|
||||
};
|
||||
|
||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
||||
#endif
|
||||
|
||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
|
||||
// not triggered. As a workaround, handle them in an event handler.
|
||||
//
|
||||
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
|
||||
// <https://stackoverflow.com/a/40693139/1987178>
|
||||
static int event_watcher(void *data, SDL_Event *event) {
|
||||
if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
// called from another thread, not very safe, but it's a workaround!
|
||||
screen_render(&screen);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void event_loop(void) {
|
||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||
SDL_AddEventWatch(event_watcher, NULL);
|
||||
#endif
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
@ -57,16 +81,15 @@ static void event_loop(void) {
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (event.window.event) {
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
screen_render(&screen);
|
||||
break;
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
screen_render(&screen);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SDL_TEXTINPUT: {
|
||||
case SDL_TEXTINPUT:
|
||||
input_manager_process_text_input(&input_manager, &event.text);
|
||||
break;
|
||||
}
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP:
|
||||
input_manager_process_key(&input_manager, &event.key);
|
||||
@ -74,38 +97,61 @@ static void event_loop(void) {
|
||||
case SDL_MOUSEMOTION:
|
||||
input_manager_process_mouse_motion(&input_manager, &event.motion);
|
||||
break;
|
||||
case SDL_MOUSEWHEEL: {
|
||||
case SDL_MOUSEWHEEL:
|
||||
input_manager_process_mouse_wheel(&input_manager, &event.wheel);
|
||||
break;
|
||||
}
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP: {
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
input_manager_process_mouse_button(&input_manager, &event.button);
|
||||
break;
|
||||
}
|
||||
case SDL_DROPFILE:
|
||||
installer_install_apk(&installer, event.drop.file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate) {
|
||||
if (!server_start(&server, serial, local_port, max_size, bit_rate)) {
|
||||
static process_t set_show_touches_enabled(const char *serial, SDL_bool enabled) {
|
||||
const char *value = enabled ? "1" : "0";
|
||||
const char *const adb_cmd[] = {
|
||||
"shell", "settings", "put", "system", "show_touches", value
|
||||
};
|
||||
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||
}
|
||||
|
||||
static void wait_show_touches(process_t process) {
|
||||
// reap the process, ignore the result
|
||||
process_check_success(process, "show_touches");
|
||||
}
|
||||
|
||||
SDL_bool scrcpy(const struct scrcpy_options *options) {
|
||||
if (!server_start(&server, options->serial, options->port,
|
||||
options->max_size, options->bit_rate)) {
|
||||
return SDL_FALSE;
|
||||
}
|
||||
|
||||
process_t proc_show_touches = PROCESS_NONE;
|
||||
SDL_bool show_touches_waited;
|
||||
if (options->show_touches) {
|
||||
LOGI("Enable show_touches");
|
||||
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
|
||||
show_touches_waited = SDL_FALSE;
|
||||
}
|
||||
|
||||
SDL_bool ret = SDL_TRUE;
|
||||
|
||||
if (!SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1")) {
|
||||
LOGW("Cannot request to keep default signal handlers");
|
||||
}
|
||||
|
||||
if (!sdl_init_and_configure()) {
|
||||
ret = SDL_FALSE;
|
||||
goto finally_destroy_server;
|
||||
}
|
||||
|
||||
// SDL initialization replace the signal handler for SIGTERM, so Ctrl+C is
|
||||
// managed by the event loop. This blocking call blocks the event loop, so
|
||||
// timeout the connection not to block indefinitely in case of SIGTERM.
|
||||
#define SERVER_CONNECT_TIMEOUT_MS 2000
|
||||
socket_t device_socket = server_connect_to(&server, serial, SERVER_CONNECT_TIMEOUT_MS);
|
||||
socket_t device_socket = server_connect_to(&server);
|
||||
if (device_socket == INVALID_SOCKET) {
|
||||
server_stop(&server, serial);
|
||||
server_stop(&server);
|
||||
ret = SDL_FALSE;
|
||||
goto finally_destroy_server;
|
||||
}
|
||||
@ -117,25 +163,31 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
|
||||
// therefore, we transmit the screen size before the video stream, to be able
|
||||
// to init the window immediately
|
||||
if (!device_read_info(device_socket, device_name, &frame_size)) {
|
||||
server_stop(&server, serial);
|
||||
server_stop(&server);
|
||||
ret = SDL_FALSE;
|
||||
goto finally_destroy_server;
|
||||
}
|
||||
|
||||
if (!frames_init(&frames)) {
|
||||
server_stop(&server, serial);
|
||||
server_stop(&server);
|
||||
ret = SDL_FALSE;
|
||||
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
|
||||
// start the decoder
|
||||
if (!decoder_start(&decoder)) {
|
||||
ret = SDL_FALSE;
|
||||
server_stop(&server, serial);
|
||||
goto finally_destroy_frames;
|
||||
server_stop(&server);
|
||||
goto finally_destroy_installer;
|
||||
}
|
||||
|
||||
if (!controller_init(&controller, device_socket)) {
|
||||
@ -153,10 +205,16 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
|
||||
goto finally_stop_and_join_controller;
|
||||
}
|
||||
|
||||
event_loop();
|
||||
if (options->show_touches) {
|
||||
wait_show_touches(proc_show_touches);
|
||||
show_touches_waited = SDL_TRUE;
|
||||
}
|
||||
|
||||
event_loop();
|
||||
LOGD("quit...");
|
||||
|
||||
screen_destroy(&screen);
|
||||
|
||||
finally_stop_and_join_controller:
|
||||
controller_stop(&controller);
|
||||
controller_join(&controller);
|
||||
@ -165,11 +223,25 @@ finally_destroy_controller:
|
||||
finally_stop_decoder:
|
||||
decoder_stop(&decoder);
|
||||
// stop the server before decoder_join() to wake up the decoder
|
||||
server_stop(&server, serial);
|
||||
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:
|
||||
if (options->show_touches) {
|
||||
if (!show_touches_waited) {
|
||||
// wait the process which enabled "show touches"
|
||||
wait_show_touches(proc_show_touches);
|
||||
}
|
||||
LOGI("Disable show_touches");
|
||||
proc_show_touches = set_show_touches_enabled(options->serial, SDL_FALSE);
|
||||
wait_show_touches(proc_show_touches);
|
||||
}
|
||||
|
||||
server_destroy(&server);
|
||||
|
||||
return ret;
|
||||
|
@ -3,6 +3,14 @@
|
||||
|
||||
#include <SDL2/SDL_stdinc.h>
|
||||
|
||||
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate);
|
||||
struct scrcpy_options {
|
||||
const char *serial;
|
||||
Uint16 port;
|
||||
Uint16 max_size;
|
||||
Uint32 bit_rate;
|
||||
SDL_bool show_touches;
|
||||
};
|
||||
|
||||
SDL_bool scrcpy(const struct scrcpy_options *options);
|
||||
|
||||
#endif
|
||||
|
@ -18,8 +18,8 @@ SDL_bool sdl_init_and_configure(void) {
|
||||
|
||||
atexit(SDL_Quit);
|
||||
|
||||
// Bilinear resizing
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
|
||||
// Use the best available scale quality
|
||||
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
|
||||
LOGW("Could not enable bilinear filtering");
|
||||
}
|
||||
|
||||
@ -136,12 +136,21 @@ void screen_init(struct screen *screen) {
|
||||
*screen = (struct screen) SCREEN_INITIALIZER;
|
||||
}
|
||||
|
||||
static inline SDL_Texture *create_texture(SDL_Renderer *renderer, struct size frame_size) {
|
||||
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
|
||||
frame_size.width, frame_size.height);
|
||||
}
|
||||
|
||||
SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, struct size frame_size) {
|
||||
screen->frame_size = frame_size;
|
||||
|
||||
struct size window_size = get_initial_optimal_size(frame_size);
|
||||
Uint32 window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
||||
#ifdef HIDPI_SUPPORT
|
||||
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
#endif
|
||||
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
|
||||
window_size.width, window_size.height, SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE);
|
||||
window_size.width, window_size.height, window_flags);
|
||||
if (!screen->window) {
|
||||
LOGC("Could not create window: %s", SDL_GetError());
|
||||
return SDL_FALSE;
|
||||
@ -170,8 +179,7 @@ SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, s
|
||||
SDL_FreeSurface(icon);
|
||||
|
||||
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height);
|
||||
screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
|
||||
frame_size.width, frame_size.height);
|
||||
screen->texture = create_texture(screen->renderer, frame_size);
|
||||
if (!screen->texture) {
|
||||
LOGC("Could not create texture: %s", SDL_GetError());
|
||||
screen_destroy(screen);
|
||||
@ -220,8 +228,7 @@ static SDL_bool prepare_for_frame(struct screen *screen, struct size new_frame_s
|
||||
|
||||
LOGD("New texture: %" PRIu16 "x%" PRIu16,
|
||||
screen->frame_size.width, screen->frame_size.height);
|
||||
screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
|
||||
new_frame_size.width, new_frame_size.height);
|
||||
screen->texture = create_texture(screen->renderer, new_frame_size);
|
||||
if (!screen->texture) {
|
||||
LOGC("Could not create texture: %s", SDL_GetError());
|
||||
return SDL_FALSE;
|
||||
|
153
app/src/server.c
153
app/src/server.c
@ -3,7 +3,9 @@
|
||||
#include <errno.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <SDL2/SDL_assert.h>
|
||||
#include <SDL2/SDL_timer.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "log.h"
|
||||
@ -37,17 +39,45 @@ static SDL_bool remove_server(const char *serial) {
|
||||
return process_check_success(process, "adb shell rm");
|
||||
}
|
||||
|
||||
static SDL_bool enable_tunnel(const char *serial, Uint16 local_port) {
|
||||
static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
|
||||
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
|
||||
return process_check_success(process, "adb reverse");
|
||||
}
|
||||
|
||||
static SDL_bool disable_tunnel(const char *serial) {
|
||||
static SDL_bool disable_tunnel_reverse(const char *serial) {
|
||||
process_t process = adb_reverse_remove(serial, SOCKET_NAME);
|
||||
return process_check_success(process, "adb reverse --remove");
|
||||
}
|
||||
|
||||
static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate) {
|
||||
static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) {
|
||||
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
|
||||
return process_check_success(process, "adb forward");
|
||||
}
|
||||
|
||||
static SDL_bool disable_tunnel_forward(const char *serial, Uint16 local_port) {
|
||||
process_t process = adb_forward_remove(serial, local_port);
|
||||
return process_check_success(process, "adb forward --remove");
|
||||
}
|
||||
|
||||
static SDL_bool enable_tunnel(struct server *server) {
|
||||
if (enable_tunnel_reverse(server->serial, server->local_port)) {
|
||||
return SDL_TRUE;
|
||||
}
|
||||
|
||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||
server->tunnel_forward = SDL_TRUE;
|
||||
return enable_tunnel_forward(server->serial, server->local_port);
|
||||
}
|
||||
|
||||
static SDL_bool disable_tunnel(struct server *server) {
|
||||
if (server->tunnel_forward) {
|
||||
return disable_tunnel_forward(server->serial, server->local_port);
|
||||
}
|
||||
return disable_tunnel_reverse(server->serial);
|
||||
}
|
||||
|
||||
static process_t execute_server(const char *serial,
|
||||
Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) {
|
||||
char max_size_string[6];
|
||||
char bit_rate_string[11];
|
||||
sprintf(max_size_string, "%"PRIu16, max_size);
|
||||
@ -60,17 +90,51 @@ static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_
|
||||
"com.genymobile.scrcpy.Server",
|
||||
max_size_string,
|
||||
bit_rate_string,
|
||||
tunnel_forward ? "true" : "false",
|
||||
};
|
||||
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
||||
}
|
||||
|
||||
static socket_t listen_on_port(Uint16 port) {
|
||||
#define IPV4_LOCALHOST 0x7F000001
|
||||
|
||||
static socket_t listen_on_port(Uint16 port) {
|
||||
return net_listen(IPV4_LOCALHOST, port, 1);
|
||||
}
|
||||
|
||||
static socket_t connect_and_read_byte(Uint16 port) {
|
||||
socket_t socket = net_connect(IPV4_LOCALHOST, port);
|
||||
if (socket == INVALID_SOCKET) {
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
char byte;
|
||||
// the connection may succeed even if the server behind the "adb tunnel"
|
||||
// is not listening, so read one byte to detect a working connection
|
||||
if (net_recv_all(socket, &byte, 1) != 1) {
|
||||
// the server is not listening yet behind the adb tunnel
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) {
|
||||
do {
|
||||
LOGD("Remaining connection attempts: %d", (int) attempts);
|
||||
socket_t socket = connect_and_read_byte(port);
|
||||
if (socket != INVALID_SOCKET) {
|
||||
// it worked!
|
||||
return socket;
|
||||
}
|
||||
if (attempts) {
|
||||
SDL_Delay(delay);
|
||||
}
|
||||
} while (--attempts > 0);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
static void close_socket(socket_t *socket) {
|
||||
SDL_assert(*socket != INVALID_SOCKET);
|
||||
net_shutdown(*socket, SHUT_RDWR);
|
||||
if (!net_close(*socket)) {
|
||||
LOGW("Cannot close socket");
|
||||
return;
|
||||
@ -84,63 +148,91 @@ 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) {
|
||||
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(serial, local_port)) {
|
||||
if (!enable_tunnel(server)) {
|
||||
SDL_free((void *) server->serial);
|
||||
return SDL_FALSE;
|
||||
}
|
||||
|
||||
// At the application level, the device part is "the server" because it
|
||||
// serves video stream and control. However, at network level, the client
|
||||
// listens and the server connects to the client. That way, the client can
|
||||
// listen before starting the server app, so there is no need to try to
|
||||
// connect until the server socket is listening on the device.
|
||||
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
|
||||
// "adb forward", so the app socket is the client
|
||||
if (!server->tunnel_forward) {
|
||||
// At the application level, the device part is "the server" because it
|
||||
// serves video stream and control. However, at the network level, the
|
||||
// client listens and the server connects to the client. That way, the
|
||||
// client can listen before starting the server app, so there is no need to
|
||||
// try to connect until the server socket is listening on the device.
|
||||
|
||||
server->server_socket = listen_on_port(local_port);
|
||||
if (server->server_socket == INVALID_SOCKET) {
|
||||
LOGE("Could not listen on port %" PRIu16, local_port);
|
||||
disable_tunnel(serial);
|
||||
return SDL_FALSE;
|
||||
server->server_socket = listen_on_port(local_port);
|
||||
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->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward);
|
||||
if (server->process == PROCESS_NONE) {
|
||||
close_socket(&server->server_socket);
|
||||
disable_tunnel(serial);
|
||||
if (!server->tunnel_forward) {
|
||||
close_socket(&server->server_socket);
|
||||
}
|
||||
disable_tunnel(server);
|
||||
SDL_free((void *) server->serial);
|
||||
return SDL_FALSE;
|
||||
}
|
||||
|
||||
server->adb_reverse_enabled = SDL_TRUE;
|
||||
server->tunnel_enabled = SDL_TRUE;
|
||||
|
||||
return SDL_TRUE;
|
||||
}
|
||||
|
||||
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms) {
|
||||
server->device_socket = net_accept(server->server_socket);
|
||||
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 delay = 100; // ms
|
||||
server->device_socket = connect_to_server(server->local_port, attempts, delay);
|
||||
}
|
||||
|
||||
if (server->device_socket == INVALID_SOCKET) {
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
|
||||
// we don't need the server socket anymore
|
||||
close_socket(&server->server_socket);
|
||||
if (!server->tunnel_forward) {
|
||||
// we don't need the server socket anymore
|
||||
close_socket(&server->server_socket);
|
||||
}
|
||||
|
||||
// the server is started, we can clean up the jar from the temporary folder
|
||||
remove_server(serial); // ignore failure
|
||||
remove_server(server->serial); // ignore failure
|
||||
server->server_copied_to_device = SDL_FALSE;
|
||||
|
||||
// we don't need the adb tunnel anymore
|
||||
disable_tunnel(serial); // ignore failure
|
||||
server->adb_reverse_enabled = SDL_FALSE;
|
||||
disable_tunnel(server); // ignore failure
|
||||
server->tunnel_enabled = SDL_FALSE;
|
||||
|
||||
return server->device_socket;
|
||||
}
|
||||
|
||||
void server_stop(struct server *server, const char *serial) {
|
||||
void server_stop(struct server *server) {
|
||||
SDL_assert(server->process != PROCESS_NONE);
|
||||
|
||||
if (!cmd_terminate(server->process)) {
|
||||
@ -150,13 +242,13 @@ void server_stop(struct server *server, const char *serial) {
|
||||
cmd_simple_wait(server->process, NULL); // ignore exit code
|
||||
LOGD("Server terminated");
|
||||
|
||||
if (server->adb_reverse_enabled) {
|
||||
if (server->tunnel_enabled) {
|
||||
// ignore failure
|
||||
disable_tunnel(serial);
|
||||
disable_tunnel(server);
|
||||
}
|
||||
|
||||
if (server->server_copied_to_device) {
|
||||
remove_server(serial); // ignore failure
|
||||
remove_server(server->serial); // ignore failure
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,4 +259,5 @@ void server_destroy(struct server *server) {
|
||||
if (server->device_socket != INVALID_SOCKET) {
|
||||
close_socket(&server->device_socket);
|
||||
}
|
||||
SDL_free((void *) server->serial);
|
||||
}
|
||||
|
@ -5,18 +5,24 @@
|
||||
#include "net.h"
|
||||
|
||||
struct server {
|
||||
const char *serial;
|
||||
process_t process;
|
||||
socket_t server_socket;
|
||||
socket_t server_socket; // only used if !tunnel_forward
|
||||
socket_t device_socket;
|
||||
SDL_bool adb_reverse_enabled;
|
||||
Uint16 local_port;
|
||||
SDL_bool tunnel_enabled;
|
||||
SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse"
|
||||
SDL_bool server_copied_to_device;
|
||||
};
|
||||
|
||||
#define SERVER_INITIALIZER { \
|
||||
.serial = NULL, \
|
||||
.process = PROCESS_NONE, \
|
||||
.server_socket = INVALID_SOCKET, \
|
||||
.device_socket = INVALID_SOCKET, \
|
||||
.adb_reverse_enabled = SDL_FALSE, \
|
||||
.local_port = 0, \
|
||||
.tunnel_enabled = SDL_FALSE, \
|
||||
.tunnel_forward = SDL_FALSE, \
|
||||
.server_copied_to_device = SDL_FALSE, \
|
||||
}
|
||||
|
||||
@ -28,10 +34,10 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po
|
||||
Uint16 max_size, Uint32 bit_rate);
|
||||
|
||||
// block until the communication with the server is established
|
||||
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms);
|
||||
socket_t server_connect_to(struct server *server);
|
||||
|
||||
// disconnect and kill the server process
|
||||
void server_stop(struct server *server, const char *serial);
|
||||
void server_stop(struct server *server);
|
||||
|
||||
// close and release sockets
|
||||
void server_destroy(struct server *server);
|
||||
|
@ -1,9 +1,11 @@
|
||||
#include "../../command.h"
|
||||
#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;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#include "../../net.h"
|
||||
#include "net.h"
|
||||
|
||||
# include <unistd.h>
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
#include "../../command.h"
|
||||
#include "command.h"
|
||||
|
||||
#include "../../log.h"
|
||||
#include "../../strutil.h"
|
||||
#include "config.h"
|
||||
#include "log.h"
|
||||
#include "strutil.h"
|
||||
|
||||
HANDLE cmd_execute(const char *path, const char *const argv[]) {
|
||||
STARTUPINFO si;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#include "../../net.h"
|
||||
#include "net.h"
|
||||
|
||||
#include "../../log.h"
|
||||
#include "log.h"
|
||||
|
||||
SDL_bool net_init(void) {
|
||||
WSADATA wsa;
|
||||
|
@ -1,4 +1,5 @@
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "controlevent.h"
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "controlevent.h"
|
||||
|
||||
@ -35,16 +36,36 @@ static void test_serialize_text_event() {
|
||||
|
||||
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
|
||||
int size = control_event_serialize(&event, buf);
|
||||
assert(size == 15);
|
||||
assert(size == 16);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
0x01, // CONTROL_EVENT_TYPE_KEYCODE
|
||||
0x0d, // text length
|
||||
0x00, 0x0d, // text length
|
||||
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_long_text_event() {
|
||||
struct control_event event;
|
||||
event.type = CONTROL_EVENT_TYPE_TEXT;
|
||||
char text[TEXT_MAX_LENGTH];
|
||||
memset(text, 'a', sizeof(text));
|
||||
event.text_event.text = text;
|
||||
|
||||
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
|
||||
int size = control_event_serialize(&event, buf);
|
||||
assert(size == 3 + sizeof(text));
|
||||
|
||||
unsigned char expected[3 + TEXT_MAX_LENGTH];
|
||||
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE
|
||||
expected[1] = 0x01;
|
||||
expected[2] = 0x2c; // text length (16 bits)
|
||||
memset(&expected[3], 'a', TEXT_MAX_LENGTH);
|
||||
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_mouse_event() {
|
||||
struct control_event event = {
|
||||
.type = CONTROL_EVENT_TYPE_MOUSE,
|
||||
@ -114,6 +135,7 @@ static void test_serialize_scroll_event() {
|
||||
int main() {
|
||||
test_serialize_keycode_event();
|
||||
test_serialize_text_event();
|
||||
test_serialize_long_text_event();
|
||||
test_serialize_mouse_event();
|
||||
test_serialize_scroll_event();
|
||||
return 0;
|
||||
|
BIN
assets/screenshot-debian-600.jpg
Normal file
BIN
assets/screenshot-debian-600.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
@ -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
20
cross_win32.txt
Normal 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
20
cross_win64.txt
Normal 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'
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
@ -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')
|
||||
|
@ -1,5 +1,8 @@
|
||||
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')
|
||||
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
||||
|
4
prebuilt-deps/.gitignore
vendored
Normal file
4
prebuilt-deps/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/.gitignore
|
||||
!/Makefile
|
||||
!/prepare-dep
|
40
prebuilt-deps/Makefile
Normal file
40
prebuilt-deps/Makefile
Normal 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
58
prebuilt-deps/prepare-dep
Executable 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"
|
@ -6,8 +6,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 3
|
||||
versionName "1.2"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -11,7 +11,7 @@ public final class ControlEvent {
|
||||
public static final int TYPE_SCROLL = 3;
|
||||
public static final int TYPE_COMMAND = 4;
|
||||
|
||||
public static final int COMMAND_SCREEN_ON = 0;
|
||||
public static final int COMMAND_BACK_OR_SCREEN_ON = 0;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
|
@ -13,12 +13,12 @@ public class ControlEventReader {
|
||||
private static final int SCROLL_PAYLOAD_LENGTH = 16;
|
||||
private static final int COMMAND_PAYLOAD_LENGTH = 1;
|
||||
|
||||
private static final int MAX_TEXT_LENGTH = 32;
|
||||
private static final int RAW_BUFFER_SIZE = 128;
|
||||
public static final int TEXT_MAX_LENGTH = 300;
|
||||
private static final int RAW_BUFFER_SIZE = 1024;
|
||||
|
||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
private final byte[] textBuffer = new byte[MAX_TEXT_LENGTH];
|
||||
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH];
|
||||
|
||||
public ControlEventReader() {
|
||||
// invariant: the buffer is always in "get" mode
|
||||
@ -94,7 +94,7 @@ public class ControlEventReader {
|
||||
if (buffer.remaining() < 1) {
|
||||
return null;
|
||||
}
|
||||
int len = toUnsigned(buffer.get());
|
||||
int len = toUnsigned(buffer.getShort());
|
||||
if (buffer.remaining() < len) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.net.LocalServerSocket;
|
||||
import android.net.LocalSocket;
|
||||
import android.net.LocalSocketAddress;
|
||||
|
||||
@ -33,8 +34,24 @@ public final class DesktopConnection implements Closeable {
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device) throws IOException {
|
||||
LocalSocket socket = connect(SOCKET_NAME);
|
||||
private static LocalSocket listenAndAccept(String abstractName) throws IOException {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(abstractName);
|
||||
try {
|
||||
return localServerSocket.accept();
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
||||
LocalSocket socket;
|
||||
if (tunnelForward) {
|
||||
socket = listenAndAccept(SOCKET_NAME);
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
socket.getOutputStream().write(0);
|
||||
} else {
|
||||
socket = connect(SOCKET_NAME);
|
||||
}
|
||||
|
||||
DesktopConnection connection = new DesktopConnection(socket);
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
|
@ -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;
|
||||
@ -18,14 +19,32 @@ public final class Device {
|
||||
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
private boolean rotated;
|
||||
|
||||
public Device(Options options) {
|
||||
screenInfo = computeScreenInfo(options.getMaxSize());
|
||||
options.setCrop(new Rect(0, 0, 1280, 1440));
|
||||
|
||||
// TODO crop the crop to the device size!!!
|
||||
|
||||
final int maxSize = options.getMaxSize();
|
||||
final Rect crop = options.getCrop();
|
||||
|
||||
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
|
||||
rotated = (displayInfo.getRotation() & 1) != 0;
|
||||
|
||||
screenInfo = ScreenInfo.create(displayInfo.getSize(), maxSize, crop, rotated);
|
||||
registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) throws RemoteException {
|
||||
public void onRotationChanged(int rotation) {
|
||||
boolean rotated = (rotation & 1) != 0;
|
||||
synchronized (Device.this) {
|
||||
screenInfo = screenInfo.withRotation(rotation);
|
||||
// Do not call getDisplayInfo(), the resulting rotation may be inconsistent with
|
||||
// the rotation parameter (race condition).
|
||||
// Instead, compute the new size from the (rotated) old size.
|
||||
Size oldSize = screenInfo.getDeviceSize();
|
||||
Size newSize = rotated != Device.this.rotated ? oldSize.rotate() : oldSize;
|
||||
screenInfo = ScreenInfo.create(newSize, maxSize, crop, rotated);
|
||||
Device.this.rotated = rotated;
|
||||
|
||||
// notify
|
||||
if (rotationListener != null) {
|
||||
@ -40,40 +59,9 @@ public final class Device {
|
||||
return screenInfo;
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private ScreenInfo computeScreenInfo(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();
|
||||
int h = deviceSize.getHeight();
|
||||
if (maxSize > 0) {
|
||||
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
||||
throw new AssertionError("Max size must be a multiple of 8");
|
||||
}
|
||||
boolean portrait = h > w;
|
||||
int major = portrait ? h : w;
|
||||
int minor = portrait ? w : h;
|
||||
if (major > maxSize) {
|
||||
int minorExact = minor * maxSize / major;
|
||||
// +4 to round the value to the nearest multiple of 8
|
||||
minor = (minorExact + 4) & ~7;
|
||||
major = maxSize;
|
||||
}
|
||||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
Size videoSize = new Size(w, h);
|
||||
return new ScreenInfo(deviceSize, videoSize, rotated);
|
||||
}
|
||||
|
||||
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,11 +70,11 @@ 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();
|
||||
return new Point(scaledX, scaledY);
|
||||
int deviceX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
|
||||
int deviceY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
|
||||
return new Point(deviceX, deviceY);
|
||||
}
|
||||
|
||||
public static String getDeviceName() {
|
||||
|
@ -16,5 +16,16 @@ public final class DisplayInfo {
|
||||
public int getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public DisplayInfo withRotation(int rotation) {
|
||||
if (rotation == this.rotation) {
|
||||
return this;
|
||||
}
|
||||
Size newSize = size;
|
||||
if ((rotation & 1) != (this.rotation & 1)) {
|
||||
newSize = size.rotate();
|
||||
}
|
||||
return new DisplayInfo(newSize, rotation);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,10 +39,6 @@ public class EventController {
|
||||
coords.orientation = 0;
|
||||
coords.pressure = 1;
|
||||
coords.size = 1;
|
||||
coords.toolMajor = 1;
|
||||
coords.toolMinor = 1;
|
||||
coords.touchMajor = 1;
|
||||
coords.touchMinor = 1;
|
||||
}
|
||||
|
||||
private void setPointerCoords(Point point) {
|
||||
@ -93,14 +89,12 @@ public class EventController {
|
||||
return injectKeyEvent(action, keycode, 0, metaState);
|
||||
}
|
||||
|
||||
private boolean injectText(String text) {
|
||||
return injectText(text, true);
|
||||
}
|
||||
|
||||
private boolean injectText(String text, boolean decomposeOnFailure) {
|
||||
KeyEvent[] events = charMap.getEvents(text.toCharArray());
|
||||
private boolean injectChar(char c) {
|
||||
String decomposed = KeyComposition.decompose(c);
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
|
||||
KeyEvent[] events = charMap.getEvents(chars);
|
||||
if (events == null) {
|
||||
return decomposeOnFailure ? injectDecomposition(text) : false;
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!injectEvent(event)) {
|
||||
@ -110,10 +104,9 @@ public class EventController {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean injectDecomposition(String text) {
|
||||
private boolean injectText(String text) {
|
||||
for (char c : text.toCharArray()) {
|
||||
String composedText = KeyComposition.decompose(c);
|
||||
if (composedText == null || !injectText(composedText, false)) {
|
||||
if (!injectChar(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -170,10 +163,15 @@ public class EventController {
|
||||
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||
}
|
||||
|
||||
private boolean pressBackOrTurnScreenOn() {
|
||||
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
||||
return injectKeycode(keycode);
|
||||
}
|
||||
|
||||
private boolean executeCommand(int action) {
|
||||
switch (action) {
|
||||
case ControlEvent.COMMAND_SCREEN_ON:
|
||||
return turnScreenOn();
|
||||
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
|
||||
return pressBackOrTurnScreenOn();
|
||||
default:
|
||||
Ln.w("Unsupported command: " + action);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import java.util.Map;
|
||||
* This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])}
|
||||
* KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
|
||||
* <p>
|
||||
* See <a href="https://source.android.com/devices/input/key-character-map-files#key-declarations">diacritical dead key characters</a>.
|
||||
* See <a href="https://source.android.com/devices/input/key-character-map-files#behaviors">diacritical dead key characters</a>.
|
||||
*/
|
||||
public final class KeyComposition {
|
||||
|
||||
|
@ -1,8 +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;
|
||||
@ -19,4 +23,20 @@ public class Options {
|
||||
public void setBitRate(int bitRate) {
|
||||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
public boolean isTunnelForward() {
|
||||
return tunnelForward;
|
||||
}
|
||||
|
||||
public void setTunnelForward(boolean tunnelForward) {
|
||||
this.tunnelForward = tunnelForward;
|
||||
}
|
||||
|
||||
public Rect getCrop() {
|
||||
return crop;
|
||||
}
|
||||
|
||||
public void setCrop(Rect crop) {
|
||||
this.crop = crop;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private static final int DEFAULT_BIT_RATE = 4_000_000; // bits per second
|
||||
private static final int DEFAULT_FRAME_RATE = 60; // fps
|
||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||
|
||||
@ -40,16 +39,12 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
|
||||
}
|
||||
|
||||
public ScreenEncoder() {
|
||||
this(DEFAULT_BIT_RATE, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
rotationChanged.set(true);
|
||||
}
|
||||
|
||||
public boolean checkRotationChanged() {
|
||||
public boolean consumeRotationChange() {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
@ -61,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);
|
||||
@ -87,11 +82,11 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
byte[] buf = new byte[bitRate / 8]; // may contain up to 1 second of video
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
while (!checkRotationChanged() && !eof) {
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
try {
|
||||
if (checkRotationChanged()) {
|
||||
if (consumeRotationChange()) {
|
||||
// must restart encoding with new size
|
||||
break;
|
||||
}
|
||||
|
@ -1,14 +1,60 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
public final class ScreenInfo {
|
||||
private final Size deviceSize;
|
||||
private final Size videoSize;
|
||||
private final boolean rotated;
|
||||
private final Rect crop;
|
||||
|
||||
public ScreenInfo(Size deviceSize, Size videoSize, boolean rotated) {
|
||||
private ScreenInfo(Size deviceSize, Size videoSize, Rect crop, boolean rotated) {
|
||||
this.deviceSize = deviceSize;
|
||||
this.videoSize = videoSize;
|
||||
this.rotated = rotated;
|
||||
this.crop = crop;
|
||||
}
|
||||
|
||||
public static ScreenInfo create(Size deviceSize, int maxSize, Rect crop, boolean rotated) {
|
||||
Size inputSize;
|
||||
if (crop == null) {
|
||||
inputSize = deviceSize;
|
||||
} else {
|
||||
if (rotated) {
|
||||
// the crop (provided by the user) is expressed in the natural orientation
|
||||
// the device size (provided by the system) already takes the rotation into account
|
||||
crop = rotateCrop(crop, deviceSize);
|
||||
}
|
||||
inputSize = new Size(crop.width(), crop.height());
|
||||
}
|
||||
Size videoSize = computeVideoSize(inputSize, maxSize);
|
||||
return new ScreenInfo(deviceSize, videoSize, crop, rotated);
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Size computeVideoSize(Size inputSize, 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)
|
||||
int w = inputSize.getWidth() & ~7; // in case it's not a multiple of 8
|
||||
int h = inputSize.getHeight() & ~7;
|
||||
if (maxSize > 0) {
|
||||
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
||||
throw new AssertionError("Max size must be a multiple of 8");
|
||||
}
|
||||
boolean portrait = h > w;
|
||||
int major = portrait ? h : w;
|
||||
int minor = portrait ? w : h;
|
||||
if (major > maxSize) {
|
||||
int minorExact = minor * maxSize / major;
|
||||
// +4 to round the value to the nearest multiple of 8
|
||||
minor = (minorExact + 4) & ~7;
|
||||
major = maxSize;
|
||||
}
|
||||
w = portrait ? minor : major;
|
||||
h = portrait ? major : minor;
|
||||
}
|
||||
return new Size(w, h);
|
||||
}
|
||||
|
||||
public Size getDeviceSize() {
|
||||
@ -19,11 +65,17 @@ public final class ScreenInfo {
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
public ScreenInfo withRotation(int rotation) {
|
||||
boolean newRotated = (rotation & 1) != 0;
|
||||
if (rotated == newRotated) {
|
||||
return this;
|
||||
}
|
||||
return new ScreenInfo(deviceSize.rotate(), videoSize.rotate(), newRotated);
|
||||
public Rect getCrop() {
|
||||
return crop;
|
||||
}
|
||||
|
||||
public Rect getContentRect() {
|
||||
Rect crop = getCrop();
|
||||
return crop != null ? crop : deviceSize.toRect();
|
||||
}
|
||||
|
||||
private static Rect rotateCrop(Rect crop, Size rotatedSize) {
|
||||
int w = rotatedSize.getHeight(); // the size is already rotated
|
||||
return new Rect(crop.top, w - crop.right, crop.bottom, w - crop.left);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ public final class Server {
|
||||
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
final Device device = new Device(options);
|
||||
try (DesktopConnection connection = DesktopConnection.open(device)) {
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
|
||||
|
||||
// asynchronous
|
||||
@ -55,6 +56,13 @@ public final class Server {
|
||||
int bitRate = Integer.parseInt(args[1]);
|
||||
options.setBitRate(bitRate);
|
||||
|
||||
if (args.length < 3) {
|
||||
return options;
|
||||
}
|
||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||
boolean tunnelForward = Boolean.parseBoolean(args[2]);
|
||||
options.setTunnelForward(tunnelForward);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class ControlEventReaderTest {
|
||||
|
||||
@ -43,8 +45,8 @@ public class ControlEventReaderTest {
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlEvent.TYPE_TEXT);
|
||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeByte(text.length);
|
||||
dos.write("testé".getBytes(StandardCharsets.UTF_8));
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
@ -54,6 +56,26 @@ public class ControlEventReaderTest {
|
||||
Assert.assertEquals("testé", event.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseLongTextEvent() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlEvent.TYPE_TEXT);
|
||||
byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH];
|
||||
Arrays.fill(text, (byte) 'a');
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
ControlEvent event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
|
||||
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseMouseEvent() throws IOException {
|
||||
ControlEventReader reader = new ControlEventReader();
|
||||
|
Reference in New Issue
Block a user