Compare commits

...

34 Commits
hidpi ... arch

Author SHA1 Message Date
Romain Vimont
e8510a8cc3 Add links to AUR packages in README 2018-03-12 16:00:12 +01:00
Romain Vimont
f9a63ec272 Reverse horizontal scrolling behavior
The SDL mouse wheel event seems inconsistent between horizontal and
vertical scrolling.

> Movements to the left generate negative x values and to the right
> generate positive x values. Movements down (scroll backward) generate
> negative y values and up (scroll forward) generate positive y values.

<https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>

Reverse the horizontal.

Fixes <https://github.com/Genymobile/scrcpy/issues/49>.
2018-03-12 09:37:46 +01:00
Romain Vimont
f6d0893316 Merge branch 'fedora_install' (pull request #29)
Document how to install on Fedora
2018-03-11 22:07:12 +01:00
Romain Vimont
ed65cd72fd Use one subsection by distribution in README
Now that instructions are given for both Debian/Ubuntu and Fedora, use
subsections.
2018-03-11 22:06:15 +01:00
Michael Gangolf
70599998eb Document how to install on Fedora 2018-03-11 22:06:15 +01:00
Romain Vimont
e87cd175cc Improve dependencies in README
Document server and client dependencies separately, to avoid unneeded
packages installation when building using the prebuilt server.

Also remove "zip", since it's only used for building a portable version
(which is not documented in README).
2018-03-11 22:05:41 +01:00
Romain Vimont
14b15ceb06 Add a FAQ for common issues 2018-03-11 10:40:46 +01:00
Romain Vimont
b9bb4ff740 Merge branch 'sdushantha' (pull request #28)
Improve README syntax highlighting.
2018-03-10 16:31:17 +01:00
Romain Vimont
cc4a015256 Add empty lines around code blocks
And fix spaces (do not randomly use non-breaking spaces for
indentation).
2018-03-10 16:24:03 +01:00
Siddharth Dushantha
8476b4aab8 removed "$" and changed Mac OS ---> MacOS 2018-03-10 15:34:59 +01:00
Siddharth Dushantha
a1491862e4 added "$" in front of terminal commands 2018-03-10 09:12:47 +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
19 changed files with 707 additions and 166 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 ;-)

84
FAQ.md Normal file
View File

@@ -0,0 +1,84 @@
# 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, I have no output in the console
When run in `cmd.exe`, the application does not print anything. Even `scrcpy
--help` have no output. We don't know why yet.
However, if you run the very same `scrcpy.exe` from
[MSYS2](https://www.msys2.org/) (`mingw64`), then it correctly prints output.
### On Windows, when I start the application, nothing happens
The previous problem does not help to get a clue about the cause.
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
### The application does not work over `adb connect`
If the device is connected using `adb connect`, then you'll get this error:
error: more than one device/emulator
ERROR: "adb reverse" returned with value 1
We plan to support it in future versions. See [issue 5].
[issue 5]: https://github.com/Genymobile/scrcpy/issues/5
### Mouse clicks do not work
On many LG devices, [mouse clicks do not work][issue 18]. We're [working][pr27]
on it.
[issue 18]: https://github.com/Genymobile/scrcpy/issues/18
[pr27]: https://github.com/Genymobile/scrcpy/pull/27
### 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 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.

339
README.md
View File

@@ -1,141 +1,302 @@
# 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 _MacOS_.
![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.
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, 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/
```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
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"
```
## Build
#### Mac OS
The project is divided into two parts:
- the server, running on the device (in `server/`);
- the client, running on the computer (in `app/`).
Use [Homebrew] to install the packages:
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.
[Homebrew]: https://brew.sh/
The client is a C project using [SDL] and [FFmpeg], built with [Meson]/[Ninja].
```bash
# runtime dependencies
brew install sdl2 ffmpeg
The root directory contains a `Makefile` to build both parts.
# client build dependencies
brew install gcc pkg-config meson
```
[sdl]: https://www.libsdl.org/
[ffmpeg]: https://www.ffmpeg.org/
[meson]: http://mesonbuild.com/
[ninja]: https://ninja-build.org/
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`:
```bash
export PATH="$JAVA_HOME/bin:$PATH"
```
### Build requirements
### Common steps
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
Then, build `scrcpy`:
sudo apt install make gcc openjdk-8-jdk pkg-config meson zip \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev libsdl2-net-dev
```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.0.jar`][direct-scrcpy-server].
_(SHA-256: b573b06a6072476b85b6308e3ad189f2665ad5be4f8ca3a6b9ec81d64df20558)_
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.0/scrcpy-server-v1.0.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 run without installing:
./scrcpy -p 1234
```bash
./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` |
| click on `BACK` | `Ctrl`+`b` |
| 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` |
## 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.
## 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

@@ -36,10 +36,11 @@ 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);
memcpy(&buf[2], event->text_event.text, len);
return 2 + len;
}
case CONTROL_EVENT_TYPE_MOUSE:
@@ -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

@@ -10,7 +10,7 @@
#define CONTROL_EVENT_QUEUE_SIZE 64
#define SERIALIZED_EVENT_MAX_SIZE 33
#define TEXT_MAX_LENGTH 31
#define TEXT_MAX_LENGTH 256
enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE,
@@ -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

@@ -172,7 +172,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;

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);
@@ -68,12 +79,10 @@ static inline void action_volume_down(struct controller *controller) {
}
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,
},
};
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action = CONTROL_EVENT_COMMAND_SCREEN_ON;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on");
}
@@ -91,6 +100,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 +137,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 +149,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 +181,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;
@@ -204,7 +241,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

@@ -82,6 +82,9 @@ static void usage(const char *arg0) {
" Right-click\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

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

@@ -71,6 +71,7 @@ static socket_t listen_on_port(Uint16 port) {
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;
@@ -95,10 +96,10 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po
}
// 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.
// 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) {

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

@@ -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 TEXT_MAX_LENGTH = 256;
private static final int RAW_BUFFER_SIZE = 128;
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

View File

@@ -93,14 +93,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 +108,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;
}
}

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

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