Compare commits

...

31 Commits

Author SHA1 Message Date
Romain Vimont
1038bad385 Make it work over tcpip
"adb reverse" currently does not work over tcpip (i.e. on a device
connected by "adb connect"):
<https://issuetracker.google.com/issues/37066218>

To work around the problem, if the call to "adb reverse" fails, then
fallback to "adb forward", and reverse the client/server roles.

Keep the "adb reverse" mode as the default because it does not involve
connection retries: when using "adb forward", the client must try to
connect successively until the server listens.

Due to the tunnel, every connect() will succeed, so the client must
attempt to read() to detect a connection failure. For this purpose, when
using the "adb forward" mode, the server initially writes a dummy byte,
read by the client.

Fixes <https://github.com/Genymobile/scrcpy/issues/5>.
2018-03-12 14:10:32 +01:00
Romain Vimont
2b3ed5bcdb Store serial in server instance
The serial is needed for many server actions, but this is an
implementation detail, so the caller should not have to provide it on
every call.

Instead, store the serial in the server instance on server_start().

This paves the way to implement the "adb forward" fallback properly.
2018-03-12 14:10:32 +01:00
Romain Vimont
9e328ef98b Always use the best render scale quality available
Because why not.

See <https://wiki.libsdl.org/SDL_HINT_RENDER_SCALE_QUALITY>.
2018-03-12 14:10:12 +01:00
Romain Vimont
c075ad0a1e Fix mouse clicks on LG devices
Use default values (0) for some fields of PointerCoords so that mouse
clicks work correctly on LG devices.

Fixes <https://github.com/Genymobile/scrcpy/issues/18>.
2018-03-11 14:11:43 +01:00
Romain Vimont
dac7196bd6 Support screens with dimensions not divisible by 8
The codec only supports dimensions which are multiple of 8.

Thus, when --max-size is specified, the value is always rounded down to
the nearest multiple of 8.

However, it was wrongly assumed that the physical size is always a
multiple of 8. To support such devices, also round down the physical
screen dimensions.

Fixes <https://github.com/Genymobile/scrcpy/issues/39>.
2018-03-11 14:09:25 +01:00
Romain Vimont
c87d94ee27 Map middle-click to HOME
Middle-click is useless in practice. Use it for HOME.
2018-03-10 00:44:19 +01:00
Romain Vimont
675704c71c Map right-click to BACK if screen is on
Right-click was used to turn the screen on. It did nothing when the
screen was already on.

Instead, in that case, press BACK (like Vysor).

Suggested by: <https://www.reddit.com/r/Android/comments/834zmr/introducing_scrcpy_an_app_to_display_and_control/dvfueft/>
2018-03-10 00:16:29 +01:00
Romain Vimont
9396ea6d42 Fix text input event segfault
The text input control_event was initially designed for mapping
SDL_TextInputEvent, limited to 32 characters.

For simplicity, the copy/paste feature was implemented using the same
control_event: it just sends the text to paste.

However, the pasted text might have a length breaking some assumptions:
 - on the client, the event max-size was smaller than the text
   max-length,
 - on the server, the raw buffer storing the events was smaller than the
   max event size.

Fix these inconsistencies, and encode the length on 2 bytes, to accept
more than 256 characters.

Fixes <https://github.com/Genymobile/scrcpy/issues/10>.
2018-03-09 22:30:10 +01:00
Romain Vimont
f9562f537a Unref the packet on error
Do not leak the packet data on error.
2018-03-08 21:36:04 +01:00
Romain Vimont
a34fbd23e9 Do not leak the packet data
Oops! The content of the packets were never freed.
2018-03-08 20:46:02 +01:00
Romain Vimont
e8b8a570e7 Document ./run script usage
Indicate how to run the app from the build directory in README. It's
convenient during development.
2018-03-08 14:53:18 +01:00
Romain Vimont
0e9a76c0c4 Add link to blog article in README 2018-03-08 12:31:26 +01:00
Romain Vimont
f9f305d19d Update release checksums in README 2018-03-08 11:34:54 +01:00
Romain Vimont
727d1ef1e2 Add developer documentation
And update README.
2018-03-08 10:27:40 +01:00
Romain Vimont
c2ac6fe7bd Upgrade version to 1.0 2018-03-08 09:21:06 +01:00
Romain Vimont
e2a7abcd53 Implement clipboard paste
Paste computer clipboard to the device on Ctrl+v.

The other direction (pasting the device clipboard to the computer) is
not implemented. It would require a communication channel from the
device to the computer, other than the socket used by the video stream.
2018-03-07 18:07:02 +01:00
Romain Vimont
e4d64e8752 Initialize struct field by field
Initializing with braces initializes the other fields to 0, which is not
necessary.
2018-03-07 18:07:02 +01:00
Romain Vimont
fffeedffda Expose High DPI support configuration flag
The High DPI support is enabled by default, so that the renderer use the
full definition of High DPI screens.

However, there are still mouse coordinates problems on some MacOS having
High DPI support (but not all), so expose a way to disable it.
2018-03-07 18:07:02 +01:00
Romain Vimont
82b4acee73 Do not fail on EAGAIN
A call to avcodec_receive_frame() may return AVERROR(EAGAIN) if more
input is required. This is not an error, do not fail.
2018-03-07 18:07:02 +01:00
Romain Vimont
633b18d786 Provide a better URL to document key decomposition
Directly link to the relevant subsection.
2018-03-07 18:07:02 +01:00
Romain Vimont
ab780ce26d Avoid useless variables initialization
Initialize variables only when necessary.
2018-03-07 18:07:02 +01:00
Romain Vimont
84ad6633a6 Move the new avcodec implementation before the old
The API to decode the video frames is different depending on the
libavcodec version.

Move the new API usage to the #if-block.
2018-03-07 18:07:01 +01:00
Romain Vimont
1b0cea61a5 Do not use return code for thread run function
The decoder sometimes returned a non-zero value on error, but not on
every path.

Since we never use the value, always return 0 at the end (like in the
controller).
2018-03-07 18:07:01 +01:00
Romain Vimont
42f6341a14 Revert "Enable high dpi support"
Just enabling this flag breaks mouse location values.

This reverts commit 38b56f552e.
2018-03-07 18:06:43 +01:00
Romain Vimont
acd2dc3183 Shutdown sockets before closing
The server socket does not release the port it was listening for if we
just close it: we must also shutdown it.
2018-03-07 18:04:39 +01:00
Romain Vimont
db396f2138 Fix scroll wheel mouse position
SDL_MouseWheelEvent does not provide the mouse location, so we used
SDL_GetMouseState() to retrieve it.

Unfortunately, SDL_GetMouseState() returns a position expressed in the
window coordinate system while the position filled in SDL events are
expressed in the renderer coordinate system. As a consequence, the
scroll was not applied at the right position on the device.

Therefore, convert the coordinate system.

See <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>.
2018-03-07 18:04:39 +01:00
Romain Vimont
e6feb991db Fix comment typo
Replace "at network level" by "at the network level".
2018-03-07 18:04:38 +01:00
Romain Vimont
e3f5d3b49b Double the default bitrate
Set the default video bitrate to 8Mbps. This greatly increase quality on
fast motion, without negative side effects.
2018-03-07 18:04:38 +01:00
Romain Vimont
a7979e4e74 Rename rotation detection method name
The old name checkRotationChanged() did not suggest that the flag was
reset.
2018-03-07 18:04:38 +01:00
Romain Vimont
38b56f552e Enable high dpi support
Use high DPI if available.

Note that on Mac OS X, setting this flag is not sufficient:

> On Apple's OS X you must set the NSHighResolutionCapable Info.plist
> property to YES, otherwise you will not receive a High DPI OpenGL
> display.

<https://wiki.libsdl.org/SDL_CreateWindow#flags>
2018-03-07 18:04:38 +01:00
Romain Vimont
8ace3d1781 Update README
Explain how to build, install and run the application.
2018-03-07 18:04:38 +01:00
30 changed files with 831 additions and 234 deletions

238
DEVELOP.md Normal file
View 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 ;-)

281
README.md
View File

@@ -1,141 +1,244 @@
# ScrCpy
# scrcpy
This project displays screens of Android devices plugged on USB in live.
This application provides display and control of Android devices connected on
USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
and _Mac OS_.
![screenshot](assets/screenshot-debian-600.jpg)
## 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] (recent enough so that `adb reverse` is implemented, it works
with 1.0.36). It is available in the [Android SDK platform
tools][platform-tools], on packaged in your distribution (`android-adb-tools`).
On Windows, just download the [platform-tools][platform-tools-windows] and
extract the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
Make sure you [enabled adb debugging][enable-adb] on your device(s).
[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
The client requires _FFmpeg_ and _LibSDL2_.
## 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 (here, for Debian):
sudo apt install ffmpeg libsdl2-2.0.0 libsdl2-net-2.0.0
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0.0
# build dependencies
sudo apt install make gcc openjdk-8-jdk pkg-config meson zip \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
#### Windows
From [MSYS2]:
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
pacman -S mingw-w64-x86_64-SDL2
pacman -S mingw-w64-x86_64-SDL2_net
pacman -S mingw-w64-x86_64-ffmpeg
- [`scrcpy-windows-with-deps-v1.0.zip`][direct-windows-with-deps].
_(SHA-256: bc4bf32600e8548cdce490f94bed5dcba0006cdd38aff95748972e5d9877dd62)_
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.0/scrcpy-windows-with-deps-v1.0.zip
_(It's just a portable version including _dll_ copied from MSYS2.)_
Instead, you may want to build it manually. You need [MSYS2] to build the
project. From an MSYS2 terminal, install the required packages:
[MSYS2]: http://www.msys2.org/
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
#### MacOS
# 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 \
zip
TODO
## Build
The project is divided into two parts:
- the server, running on the device (in `server/`);
- the client, running on the computer (in `app/`).
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.
The client is a C project using [SDL] and [FFmpeg], built with [Meson]/[Ninja].
The root directory contains a `Makefile` to build both parts.
[sdl]: https://www.libsdl.org/
[ffmpeg]: https://www.ffmpeg.org/
[meson]: http://mesonbuild.com/
[ninja]: https://ninja-build.org/
### Build requirements
Install the [Android SDK], the JDK 8 (`openjdk-8-jdk`), and the packages
described below.
[Android SDK]: https://developer.android.com/studio/index.html
#### Linux
sudo apt install make gcc openjdk-8-jdk pkg-config meson zip \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev libsdl2-net-dev
#### Windows
Install these packages:
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
Java 8 is not available in MSYS2, so install it manually and make it available
from the `PATH`:
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`:
export PATH="$JAVA_HOME/bin:$PATH"
### Build
#### Mac OS
Make sure your `ANDROID_HOME` variable is set to your Android SDK directory:
Use [Homebrew] to install the packages:
[Homebrew]: https://brew.sh/
# runtime dependencies
brew install sdl2 ffmpeg
# build dependencies
brew install gcc pkg-config meson zip
Java (>= 7) is not available in Homebrew, so if you plan to build the server,
install it manually and make it available from the `PATH`:
export PATH="$JAVA_HOME/bin:$PATH"
### Common steps
Install the [Android SDK] (_Android Studio_), and set `ANDROID_HOME` to
its directory. For example:
[Android SDK]: https://developer.android.com/studio/index.html
export ANDROID_HOME=~/android/sdk
From the project root directory, execute:
Then, build `scrcpy`:
make build
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
To run the build:
You can test it from here:
make run
ninja run
It is also pass arguments to `scrcpy` via `make`:
Or you can install it on the system:
make run ARGS="-p 1234"
sudo ninja install # without sudo on Windows
The purpose of this command is to execute `scrcpy` during the development.
This installs two files:
- `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server.jar`
Just remove them to "uninstall" the application.
### Test
#### Prebuilt server
To execute unit tests:
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:
make test
- [`scrcpy-server-v1.0.jar`][direct-scrcpy-server].
_(SHA-256: b573b06a6072476b85b6308e3ad189f2665ad5be4f8ca3a6b9ec81d64df20558)_
The server-side tests require JUnit 4:
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.0/scrcpy-server-v1.0.jar
sudo apt install junit4
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:
### Generate a release
From the project root directory, execute:
make release
This will generate the application in `dist/scrcpy/`.
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:
scrcpy
It accepts command-line arguments, listed by:
scrcpy --help
For example, to decrease video bitrate to 2Mbps (default is 8Mbps):
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):
scrcpy -m 1024
If several devices are listed in `adb devices`, you must specify the _serial_:
./scrcpy 0123456789abcdef
scrcpy -s 0123456789abcdef
To change the default port (useful to launch several `scrcpy` simultaneously):
To run without installing:
./scrcpy -p 1234
./run x [options]
Other options are available, check `scrcpy --help`.
(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` |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` \| _Right-click¹_ |
| click on `APP_SWITCH` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_DOWN` | `Ctrl`+`-` |
| 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` |
_¹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
## 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.
## Article
- [Introducing scrcpy](https://blog.rom1v.com/2018/03/introducing-scrcpy/)

View File

@@ -42,7 +42,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.0')
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
@@ -73,13 +73,16 @@ 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'))
configure_file(configuration: conf, output: 'config.h')
executable('scrcpy', src, dependencies: dependencies, install: true)

View File

@@ -45,6 +45,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

View File

@@ -39,6 +39,7 @@ 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);

View File

@@ -36,11 +36,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 +62,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 +84,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) {

View File

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

View File

@@ -65,7 +65,9 @@ static int run_controller(void *data) {
}
struct control_event event;
while (control_event_queue_take(&controller->queue, &event)) {
if (!process_event(controller, &event)) {
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
goto end;
}

View File

@@ -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;
}
@@ -100,7 +94,22 @@ static int run_decoder(void *data) {
while (!av_read_frame(format_ctx, &packet) && !avio_ctx->eof_reached) {
// 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,8 @@ 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);
}
LOGD("End of frames");
@@ -142,7 +140,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) {

View File

@@ -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,
@@ -22,14 +36,11 @@ static SDL_bool is_ctrl_down(void) {
static void send_keycode(struct controller *controller, enum android_keycode keycode, 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.action = AKEY_EVENT_ACTION_DOWN;
control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
@@ -67,13 +78,12 @@ static inline void action_volume_down(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, "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,
},
};
// 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,6 +101,27 @@ static void switch_fps_counter_state(struct frames *frames) {
mutex_unlock(frames->mutex);
}
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;
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) {
if (is_ctrl_down()) {
@@ -107,8 +138,11 @@ void input_manager_process_text_input(struct input_manager *input_manager,
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 = 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,23 +150,24 @@ 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) {
SDL_bool repeat = event->repeat;
// only consider keydown events, and ignore repeated events
if (repeat || event->type != SDL_KEYDOWN) {
return;
}
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (shift) {
// currently, there is no shortcut implying SHIFT
return;
}
SDL_Keycode keycode = event->keysym.sym;
switch (keycode) {
case SDLK_h:
action_home(input_manager->controller);
@@ -147,6 +182,9 @@ void input_manager_process_key(struct input_manager *input_manager,
case SDLK_p:
action_power(input_manager->controller);
return;
case SDLK_v:
clipboard_paste(input_manager->controller);
return;
case SDLK_f:
screen_switch_fullscreen(input_manager->screen);
return;
@@ -188,9 +226,15 @@ 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);
return;
}
};
struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
@@ -204,7 +248,7 @@ 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)) {

View File

@@ -61,10 +61,12 @@ static void usage(const char *arg0) {
"\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"
@@ -79,9 +81,12 @@ static void usage(const char *arg0) {
" 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",

View File

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

View File

@@ -21,6 +21,7 @@
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);

View File

@@ -103,9 +103,9 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// 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, SERVER_CONNECT_TIMEOUT_MS);
if (device_socket == INVALID_SOCKET) {
server_stop(&server, serial);
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
@@ -117,13 +117,13 @@ 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;
}
@@ -134,7 +134,7 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// start the decoder
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server, serial);
server_stop(&server);
goto finally_destroy_frames;
}
@@ -165,7 +165,7 @@ 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_frames:
frames_destroy(&frames);

View File

@@ -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");
}
@@ -140,8 +140,12 @@ SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, s
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;

View File

@@ -4,6 +4,7 @@
#include <inttypes.h>
#include <stdint.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h"
#include "log.h"
@@ -37,17 +38,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 +89,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 +147,84 @@ 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 (!push_server(serial)) {
return SDL_FALSE;
}
server->server_copied_to_device = SDL_TRUE;
if (!enable_tunnel(serial, local_port)) {
if (!enable_tunnel(server)) {
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);
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);
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, Uint32 timeout_ms) {
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
Uint32 attempts = 10;
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 +234,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 +251,5 @@ void server_destroy(struct server *server) {
if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket);
}
SDL_free((void *) server->serial);
}

View File

@@ -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, Uint32 timeout_ms);
// 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);

View File

@@ -35,16 +35,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 +134,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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -3,3 +3,4 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser
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')

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
@@ -33,8 +34,20 @@ 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);
return localServerSocket.accept();
}
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();

View File

@@ -50,8 +50,8 @@ public final class Device {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
int w = deviceSize.getWidth();
int h = deviceSize.getHeight();
int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8
int h = deviceSize.getHeight() & ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
public class Options {
private int maxSize;
private int bitRate;
private boolean tunnelForward;
public int getMaxSize() {
return maxSize;
@@ -19,4 +20,12 @@ public class Options {
public void setBitRate(int bitRate) {
this.bitRate = bitRate;
}
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
}

View File

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

View File

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

View File

@@ -3,14 +3,15 @@ package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
public class ControlEventReaderTest {
@@ -43,8 +44,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 +55,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();