Compare commits

..

10 Commits

Author SHA1 Message Date
Romain Vimont
ae87885ffa hidpiscale 2018-03-05 15:14:09 +01:00
Romain Vimont
65021416b3 Add developer documentation
And update README.
2018-03-05 15:10:37 +01:00
Romain Vimont
305e76ee6e Revert "Enable high dpi support"
Just enabling this flag breaks mouse location values.

This reverts commit 64efe2c07d.
2018-03-05 15:10:37 +01:00
Romain Vimont
ef0029d10e 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-05 15:10:37 +01:00
Romain Vimont
36c68c4802 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-05 14:59:02 +01:00
Romain Vimont
a82f665114 Fix comment typo
Replace "at network level" by "at the network level".
2018-03-02 15:43:01 +01:00
Romain Vimont
f88cc91a51 Double the default bitrate
Set the default video bitrate to 8Mbps. This greatly increase quality on
fast motion, without negative side effects.
2018-03-02 15:43:01 +01:00
Romain Vimont
38e4b3e39a Rename rotation detection method name
The old name checkRotationChanged() did not suggest that the flag was
reset.
2018-03-01 17:16:02 +01:00
Romain Vimont
64efe2c07d 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-01 13:25:15 +01:00
Romain Vimont
93e9fb496c Update README
Explain how to build, install and run the application.
2018-03-01 12:17:56 +01:00
98 changed files with 1767 additions and 4929 deletions

251
BUILD.md
View File

@@ -1,251 +0,0 @@
# Build scrcpy
Here are the instructions to build _scrcpy_ (client and server).
You may want to build only the client: the server binary, which will be pushed
to the Android device, does not depend on your system and architecture. In that
case, use the [prebuilt server] (so you will not need Java or the Android SDK).
[prebuilt server]: #prebuilt-server
## Requirements
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
On Windows, 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`
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
[adb]: https://developer.android.com/studio/command-line/adb.html
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
## System-specific steps
### Linux
Install the required packages from your package manager.
#### Debian/Ubuntu
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0.0
# client build dependencies
sudo apt install make gcc pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
# server build dependencies
sudo apt install openjdk-8-jdk
```
On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install
it from `pip3`:
```bash
sudo apt install python3-pip
pip3 install meson
```
#### 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
```
### Windows
#### Cross-compile from Linux
This is the preferred method (and the way the release is built).
From _Debian_, install _mingw_:
```bash
sudo apt install mingw-w64 mingw-w64-tools
```
You also need the JDK to build the server:
```bash
sudo apt install openjdk-8-jdk
```
Then generate the releases:
```bash
make -f Makefile.CrossWindows
```
It will generate win32 and win64 releases into `dist/`.
#### In MSYS2
From Windows, you need [MSYS2] to build the project. From an MSYS2 terminal,
install the required packages:
[MSYS2]: http://www.msys2.org/
```bash
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
# 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
```
For a 32 bits version, replace `x86_64` by `i686`:
```bash
# runtime dependencies
pacman -S mingw-w64-i686-SDL2 \
mingw-w64-i686-ffmpeg
# client build dependencies
pacman -S mingw-w64-i686-make \
mingw-w64-i686-gcc \
mingw-w64-i686-pkg-config \
mingw-w64-i686-meson
```
Java (>= 7) is not available in MSYS2, so if you plan to build the server,
install it manually and make it available from the `PATH`:
```bash
export PATH="$JAVA_HOME/bin:$PATH"
```
### Mac OS
Install the packages with [Homebrew]:
[Homebrew]: https://brew.sh/
```bash
# runtime dependencies
brew install sdl2 ffmpeg
# client build dependencies
brew install pkg-config meson
```
Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it avaliable from the `PATH`:
```bash
brew tap caskroom/versions
brew cask install java8
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
export PATH="$JAVA_HOME/bin:$PATH"
```
### Docker
See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
## Common steps
If you want to build the server, 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
```
If you don't want to build the server, use the [prebuilt server].
Clone the project:
```bash
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
```
Then, build:
```bash
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
```
_Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja
install` must be run as root)._
[ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a
### Run
To run without installing:
```bash
./run x [options]
```
### Install
After a successful build, you can install _scrcpy_ 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.
You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
_(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
```bash
meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar
cd x
ninja
sudo ninja install
```

View File

@@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String...
args)`][main] method), compiled against the Android framework, and executed as args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device. `shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100 [main]: server/src/main/java/com/genymobile/scrcpy/Server.java#L61
To run such a Java application, the classes must be [_dexed_][dex] (typically, 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 to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@@ -57,7 +57,7 @@ build system, the server is built to an (unsigned) APK (renamed to
### Hidden methods ### Hidden methods
Although compiled against the Android framework, [hidden] methods and classes are Albeit compiled against the Android framework, [hidden] methods and classes are
not directly accessible (and they may differ from one Android version to not directly accessible (and they may differ from one Android version to
another). another).
@@ -65,8 +65,8 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl]. components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178 [hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers [wrappers]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view [aidl]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/aidl/android/view
### Threading ### Threading
@@ -89,9 +89,9 @@ 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 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). stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70 [surface]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
On device [rotation], the codec, surface and display are reinitialized, and a On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced. new video stream is produced.
@@ -105,9 +105,8 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 [rotation]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
[repeat]: [repeat]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
@@ -125,11 +124,11 @@ 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 the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]). [`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66 [`EventController`]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.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 [`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.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@@ -154,7 +153,7 @@ Note that the client-server roles are expressed at the application level:
- the server _serves_ video stream and handle requests from the client, - the server _serves_ video stream and handle requests from the client,
- the client _controls_ the device through the server. - the client _controls_ the device through the server.
However, the roles are reversed at the network level: However, the roles are inverted at the network level:
- the client opens a server socket and listen on a port before starting the - the client opens a server socket and listen on a port before starting the
server, server,
@@ -163,9 +162,6 @@ However, the roles are reversed at the network level:
This role inversion guarantees that the connection will not fail due to race This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling. conditions, and avoids polling.
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
reverse`. See commit [1038bad] and [issue #5].)_
Once the server is connected, it sends the device information (name and initial 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 screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available. the first frame is available.
@@ -173,8 +169,6 @@ the first frame is available.
To minimize startup time, SDL initialization is performed while listening for To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]). the connection from the server (see commit [90a46b4]).
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e [90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
@@ -183,25 +177,17 @@ the connection from the server (see commit [90a46b4]).
The client uses 3 threads: The client uses 3 threads:
- the **main** thread, executing the SDL event loop, - the **main** thread, executing the SDL event loop,
- the **stream** thread, receiving the video and used for decoding and - the **decoder** thread, decoding video frames,
recording,
- the **controller** thread, sending _control events_ to the server. - the **controller** thread, sending _control events_ to the server.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window).
### 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.
### Stream There are two [frames] simultaneously in memory:
The video [stream] is received from the socket (connected to the server on the
device) in a separate thread.
If a [decoder] is present (i.e. `--no-display` is not set), then 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][video_buffer] simultaneously in memory:
- the **decoding** frame, written by the decoder from the decoder thread, - the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread. - the **rendering** frame, rendered in a texture from the main thread.
@@ -209,23 +195,9 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one. to decode a new frame while the main thread renders the last one.
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw [decoder]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/decoder.c
H.264 packet to the output video file. [frames]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/frames.h
[stream]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/recorder.h
```
+----------+ +----------+
---> | decoder | ---> | screen |
+---------+ / +----------+ +----------+
socket ---> | stream | ----
+---------+ \ +----------+
---> | recorder |
+----------+
```
### Controller ### Controller
@@ -239,10 +211,10 @@ 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 controller takes events from the queue, that it serializes and sends to the
client. client.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h [controller]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h [controlevent]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/controlevent.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h [inputmanager]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/inputmanager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h [convert]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/convert.h
### UI and event loop ### UI and event loop
@@ -253,10 +225,9 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager]. delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c [scrcpy]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/scrcpy.c
[event loop]: [event loop]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/scrcpy.c#L38
https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c#L187 [screen]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/screen.h
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/screen.h
## Hack ## Hack

56
FAQ.md
View File

@@ -1,56 +0,0 @@
# Frequently Asked Questions
## Common issues
The application is very young, it is not unlikely that you encounter problems
with it.
Here are the common reported problems and their status.
### On Windows, my device is not detected
The most common is your device not being detected by `adb`, or is unauthorized.
Check everything is ok by calling:
adb devices
Windows may need some [drivers] to detect your device.
[drivers]: https://developer.android.com/studio/run/oem-usb.html
If you still encounter problems, please see [issue 9].
[issue 9]: https://github.com/Genymobile/scrcpy/issues/9
### Mouse clicks do not work
On some devices, you may need to enable an option to allow [simulating input].
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
### Mouse clicks at wrong location
On MacOS, with HiDPI support and multiple screens, input location are wrongly
scaled. See [issue 15].
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
A workaround is to build with HiDPI support disabled:
```bash
meson x --buildtype release -Dhidpi_support=false
```
However, the video will be displayed at lower resolution.
### KWin compositor crashes
On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
As a workaround, [disable "Block compositing"][kwin].
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613

62
Makefile Normal file
View File

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

View File

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

397
README.md
View File

@@ -1,8 +1,8 @@
# scrcpy (v1.8) # scrcpy
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
It works on _GNU/Linux_, _Windows_ and _MacOS_. and _Mac OS_.
![screenshot](assets/screenshot-debian-600.jpg) ![screenshot](assets/screenshot-debian-600.jpg)
@@ -11,287 +11,178 @@ It works on _GNU/Linux_, _Windows_ and _MacOS_.
The Android part requires at least API 21 (Android 5.0). The Android part requires at least API 21 (Android 5.0).
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). 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 [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
On some devices, you also need to enable [an additional option][control] to The client requires _FFmpeg_ and _LibSDL2_.
control it using keyboard and mouse.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
## Get the app ## Build and install
### System-specific steps
#### Linux
Install the required packages from your package manager (here, for Debian):
# 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
### Linux #### Windows
On Linux, you typically need to [build the app manually][BUILD]. Don't worry, For Windows, for simplicity, a prebuilt package with all the dependencies
it's not that hard. (including `adb`) is available: TODO.
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. Instead, you may want to build it manually. You need [MSYS2] to build the
project. From an MSYS2 terminal, install the required packages:
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [MSYS2]: http://www.msys2.org/
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. # runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild # build dependencies
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy pacman -S mingw-w64-x86_64-make \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-meson \
zip
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"
### Windows #### Mac OS
For Windows, for simplicity, prebuilt archives with all the dependencies Use [Homebrew] to install the packages:
(including `adb`) are available:
- [`scrcpy-win32-v1.8.zip`][direct-win32]
_(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
- [`scrcpy-win64-v1.8.zip`][direct-win64]
_(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip
You can also [build the app manually][BUILD].
### Mac OS
The application is available in [Homebrew]. Just install it:
[Homebrew]: https://brew.sh/ [Homebrew]: https://brew.sh/
```bash # runtime dependencies
brew install scrcpy brew install sdl2 ffmpeg
```
You need `adb`, accessible from your `PATH`. If you don't have it yet: # build dependencies
brew install gcc pkg-config meson zip
```bash Java (>= 7) is not available in Homebrew, so if you plan to build the server,
brew cask install android-platform-tools install it manually and make it available from the `PATH`:
```
You can also [build the app manually][BUILD]. 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
Then, build `scrcpy`:
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
You can test it from here:
ninja run
Or you can install it on the system:
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.
#### Prebuilt server
Since the server binary, that will be pushed to the Android device, does not
depend on your system and architecture, you may want to use the prebuilt binary
instead: [`scrcpy-server.jar`](TODO).
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:
meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar
cd x
ninja
sudo ninja install
## Run ## Run
Plug an Android device, and execute: _At runtime, `adb` must be accessible from your `PATH`._
```bash If everything is ok, just plug an Android device, and execute:
scrcpy
``` scrcpy
It accepts command-line arguments, listed by: It accepts command-line arguments, listed by:
```bash scrcpy --help
scrcpy --help
```
## Features For example, to decrease video bitrate to 2Mbps (default is 8Mbps):
scrcpy -b 2M
### Reduce size 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):
Sometimes, it is useful to mirror an Android device at a lower definition to scrcpy -m 1024
increase performances.
To limit both width and height to some value (e.g. 1024):
```bash
scrcpy --max-size 1024
scrcpy -m 1024 # short version
```
The other dimension is computed to that the device aspect-ratio is preserved.
That way, a device in 1920×1080 will be mirrored at 1024×576.
### Change bit-rate
The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps):
```bash
scrcpy --bit-rate 2M
scrcpy -b 2M # short version
```
### Crop
The device screen may be cropped to mirror only part of the screen.
This is useful for example to mirror only 1 eye of the Oculus Go:
```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
scrcpy -c 1224:1440:0:0 # short version
```
If `--max-size` is also specified, resizing is applied after cropping.
### Wireless
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP:
1. Connect the device to the same Wi-Fi as your computer.
2. Get your device IP address (in Settings → About phone → Status).
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
4. Unplug your device.
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
6. Run `scrcpy` as usual.
It may be useful to decrease the bit-rate and the definition:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # short version
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
### Record screen
It is possible to record the screen while mirroring:
```bash
scrcpy --record file.mp4
scrcpy -r file.mkv
```
To disable mirroring while recording:
```bash
scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
# Ctrl+C does not terminate properly on Windows, so disconnect the device
```
"Skipped frames" are recorded, even if they are not displayed in real time (for
performance reasons). Frames are _timestamped_ on the device, so [packet delay
variation] does not impact the recorded file.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
### Multi-devices
If several devices are listed in `adb devices`, you must specify the _serial_: If several devices are listed in `adb devices`, you must specify the _serial_:
```bash scrcpy -s 0123456789abcdef
scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # short version
```
You can start several instances of _scrcpy_ for several devices.
### Fullscreen
The app may be started directly in fullscreen:
```bash
scrcpy --fullscreen
scrcpy -f # short version
```
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
### Always on top
The window of app can always be above others by:
```bash
scrcpy --always-on-top
scrcpy -T # short version
```
### Show touches
For presentations, it may be useful to show physical touches (on the physical
device).
Android provides this feature in _Developers options_.
_Scrcpy_ provides an option to enable this feature on start and disable on exit:
```bash
scrcpy --show-touches
scrcpy -t
```
Note that it only shows _physical_ touches (with the finger on the device).
### Install APK
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
window.
There is no visual feedback, a log is printed to the console.
### Push file to device
To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
_scrcpy_ window.
There is no visual feedback, a log is printed to the console.
### Read-only
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash
scrcpy --no-control
scrcpy -n
```
### Forward audio
Audio is not forwarded by _scrcpy_.
There is a limited solution using [AOA], implemented in the [`audio`] branch. If
you are interested, see [issue 14].
[AOA]: https://source.android.com/devices/accessories/aoa2
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts ## Shortcuts
| Action | Shortcut | | Action | Shortcut |
| -------------------------------------- |:---------------------------- | | ------------------------------------- | -------------:|
| switch fullscreen mode | `Ctrl`+`f` | | switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | | resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | | resize window to remove black borders | `Ctrl`+`x` |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | | click on `HOME` | `Ctrl`+`h` |
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | | click on `BACK` | `Ctrl`+`b` |
| click on `APP_SWITCH` | `Ctrl`+`s` | | click on `APP_SWITCH` | `Ctrl`+`m` |
| click on `MENU` | `Ctrl`+`m` | | click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_UP` | `Ctrl`+`` _(up)_ (`Cmd`+`↑` on MacOS) | | click on `VOLUME_DOWN` | `Ctrl`+`-` |
| click on `VOLUME_DOWN` | `Ctrl`+`` _(down)_ (`Cmd`+`↓` on MacOS) | | click on `POWER` | `Ctrl`+`p` |
| click on `POWER` | `Ctrl`+`p` | | turn screen on | _Right-click_ |
| turn screen on | _Right-click²_ | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| expand notification panel | `Ctrl`+`n` |
| collapse notification panel | `Ctrl`+`Shift`+`n` |
| paste computer clipboard to device | `Ctrl`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
## Why _scrcpy_? ## Why _scrcpy_?
@@ -304,18 +195,6 @@ A colleague challenged me to find a name as unpronounceable as [gnirehtet].
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html [`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
## How to build?
See [BUILD].
[BUILD]: BUILD.md
## Common issues
See the [FAQ](FAQ.md).
## Developers ## Developers
Read the [developers page]. Read the [developers page].
@@ -338,11 +217,3 @@ Read the [developers page].
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
## Articles
- [Introducing scrcpy][article-intro]
- [Scrcpy now works wirelessly][article-tcpip]
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/

View File

@@ -1,74 +1,30 @@
src = [ src = [
'src/main.c', 'src/main.c',
'src/command.c', 'src/command.c',
'src/control_event.c', 'src/controlevent.c',
'src/controller.c', 'src/controller.c',
'src/convert.c', 'src/convert.c',
'src/decoder.c', 'src/decoder.c',
'src/device.c', 'src/device.c',
'src/file_handler.c', 'src/fpscounter.c',
'src/fps_counter.c', 'src/frames.c',
'src/input_manager.c', 'src/hidpi.c',
'src/lock_util.c', 'src/inputmanager.c',
'src/lockutil.c',
'src/net.c', 'src/net.c',
'src/recorder.c',
'src/scrcpy.c', 'src/scrcpy.c',
'src/screen.c', 'src/screen.c',
'src/server.c', 'src/server.c',
'src/str_util.c', 'src/strutil.c',
'src/tiny_xpm.c', 'src/tinyxpm.c',
'src/stream.c',
'src/video_buffer.c',
] ]
if not get_option('crossbuild_windows') dependencies = [
dependency('libavformat'),
# native build dependency('libavcodec'),
dependencies = [ dependency('libavutil'),
dependency('libavformat'), dependency('sdl2'),
dependency('libavcodec'), ]
dependency('libavutil'),
dependency('sdl2'),
]
else
# cross-compile mingw32 build (from Linux to Windows)
cc = meson.get_compiler('c')
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin'
sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib'
sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include'
sdl2 = declare_dependency(
dependencies: [
cc.find_library('SDL2', dirs: sdl2_bin_dir),
cc.find_library('SDL2main', dirs: sdl2_lib_dir),
],
include_directories: include_directories(sdl2_include_dir)
)
prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared')
prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev')
ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin'
ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include'
ffmpeg = declare_dependency(
dependencies: [
cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir),
cc.find_library('avformat-58', dirs: ffmpeg_bin_dir),
cc.find_library('avutil-56', dirs: ffmpeg_bin_dir),
],
include_directories: include_directories(ffmpeg_include_dir)
)
dependencies = [
ffmpeg,
sdl2,
cc.find_library('mingw32')
]
endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
@@ -87,7 +43,7 @@ conf = configuration_data()
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug') conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
# the version, updated on release # the version, updated on release
conf.set_quoted('SCRCPY_VERSION', meson.project_version()) conf.set_quoted('SCRCPY_VERSION', '0.1')
# the prefix used during configuration (meson --prefix=PREFIX) # the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix')) conf.set_quoted('PREFIX', get_option('prefix'))
@@ -125,35 +81,21 @@ conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# SKIP_FRAMES improves latency at the cost of framerate # SKIP_FRAMES improves latency at the cost of framerate
conf.set('SKIP_FRAMES', get_option('skip_frames')) conf.set('SKIP_FRAMES', get_option('skip_frames'))
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
configure_file(configuration: conf, output: 'config.h') configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src') executable('scrcpy', src, dependencies: dependencies, install: true)
if get_option('windows_noconsole')
c_args = [ '-mwindows' ]
link_args = [ '-mwindows' ]
else
c_args = []
link_args = []
endif
executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: c_args, link_args: link_args)
### TESTS ### TESTS
tests = [ tests = [
['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/control_event.c']], ['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/controlevent.c']],
['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/control_event.c']], ['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/controlevent.c']],
['test_strutil', ['tests/test_strutil.c', 'src/str_util.c']], ['test_strutil', ['tests/test_strutil.c', 'src/strutil.c']],
] ]
src_dir = include_directories('src')
foreach t : tests foreach t : tests
exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies) exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies)
test(t[0], exe) test(t[0], exe)

View File

@@ -777,6 +777,9 @@ enum android_input_source {
AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK, AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK,
/** rotary encoder */ /** rotary encoder */
AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE, AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE,
/** any */
AINPUT_SOURCE_ANY = 0xffffff00,
}; };
/** /**

View File

@@ -1,33 +0,0 @@
#ifndef BUFFER_UTIL_H
#define BUFFER_UTIL_H
#include <stdbool.h>
#include <stdint.h>
static inline void
buffer_write16be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
buf[1] = value;
}
static inline void
buffer_write32be(uint8_t *buf, uint32_t value) {
buf[0] = value >> 24;
buf[1] = value >> 16;
buf[2] = value >> 8;
buf[3] = value;
}
static inline uint32_t
buffer_read32be(uint8_t *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
}
static inline
uint64_t buffer_read64be(uint8_t *buf) {
uint32_t msb = buffer_read32be(buf);
uint32_t lsb = buffer_read32be(&buf[4]);
return ((uint64_t) msb << 32) | lsb;
}
#endif

View File

@@ -4,14 +4,13 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "common.h"
#include "log.h" #include "log.h"
#include "str_util.h"
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
static const char *adb_command; static const char *adb_command;
static inline const char * static inline const char *get_adb_command() {
get_adb_command(void) {
if (!adb_command) { if (!adb_command) {
adb_command = getenv("ADB"); adb_command = getenv("ADB");
if (!adb_command) if (!adb_command)
@@ -20,27 +19,9 @@ get_adb_command(void) {
return adb_command; return adb_command;
} }
static void process_t adb_execute(const char *serial, const char *const adb_cmd[], int len) {
show_adb_err_msg(enum process_result err) {
switch (err) {
case PROCESS_ERROR_GENERIC:
LOGE("Failed to execute adb");
break;
case PROCESS_ERROR_MISSING_BINARY:
LOGE("'adb' command not found (make it accessible from your PATH "
"or define its full path in the ADB environment variable)");
break;
case PROCESS_SUCCESS:
/* do nothing */
break;
}
}
process_t
adb_execute(const char *serial, const char *const adb_cmd[], int len) {
const char *cmd[len + 4]; const char *cmd[len + 4];
int i; int i;
process_t process;
cmd[0] = get_adb_command(); cmd[0] = get_adb_command();
if (serial) { if (serial) {
cmd[1] = "-s"; cmd[1] = "-s";
@@ -52,17 +33,10 @@ adb_execute(const char *serial, const char *const adb_cmd[], int len) {
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
cmd[len + i] = NULL; cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd[0], cmd, &process); return cmd_execute(cmd[0], cmd);
if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r);
return PROCESS_NONE;
}
return process;
} }
process_t process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) {
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
char local[4 + 5 + 1]; // tcp:PORT char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port); sprintf(local, "tcp:%" PRIu16, local_port);
@@ -71,17 +45,7 @@ adb_forward(const char *serial, uint16_t local_port,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
} }
process_t process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
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 local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port); sprintf(local, "tcp:%" PRIu16, local_port);
@@ -90,67 +54,27 @@ adb_reverse(const char *serial, const char *device_socket_name,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
} }
process_t process_t adb_reverse_remove(const char *serial, const char *device_socket_name) {
adb_reverse_remove(const char *serial, const char *device_socket_name) {
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
const char *const adb_cmd[] = {"reverse", "--remove", remote}; const char *const adb_cmd[] = {"reverse", "--remove", remote};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
} }
process_t process_t adb_push(const char *serial, const char *local, const char *remote) {
adb_push(const char *serial, const char *local, const char *remote) {
#ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
local = strquote(local);
if (!local) {
return PROCESS_NONE;
}
remote = strquote(remote);
if (!remote) {
free((void *) local);
return PROCESS_NONE;
}
#endif
const char *const adb_cmd[] = {"push", local, remote}; const char *const adb_cmd[] = {"push", local, remote};
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) remote);
free((void *) local);
#endif
return proc;
} }
process_t process_t adb_remove_path(const char *serial, const char *path) {
adb_install(const char *serial, const char *local) { const char *const adb_cmd[] = {"shell", "rm", path};
#ifdef __WINDOWS__ return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
local = strquote(local);
if (!local) {
return PROCESS_NONE;
}
#endif
const char *const adb_cmd[] = {"install", "-r", local};
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) local);
#endif
return proc;
} }
bool SDL_bool process_check_success(process_t proc, const char *name) {
process_check_success(process_t proc, const char *name) {
if (proc == PROCESS_NONE) { if (proc == PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name); LOGE("Could not execute \"%s\"", name);
return false; return SDL_FALSE;
} }
exit_code_t exit_code; exit_code_t exit_code;
if (!cmd_simple_wait(proc, &exit_code)) { if (!cmd_simple_wait(proc, &exit_code)) {
@@ -159,7 +83,7 @@ process_check_success(process_t proc, const char *name) {
} else { } else {
LOGE("\"%s\" exited unexpectedly", name); LOGE("\"%s\" exited unexpectedly", name);
} }
return false; return SDL_FALSE;
} }
return true; return SDL_TRUE;
} }

View File

@@ -1,79 +1,51 @@
#ifndef COMMAND_H #ifndef COMMAND_H
#define COMMAND_H #define COMMAND_H
#include <stdbool.h>
#include <inttypes.h> #include <inttypes.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_platform.h>
#ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h>
# define PRIexitcode "lu"
// <https://stackoverflow.com/a/44383330/1987178> // <https://stackoverflow.com/a/44383330/1987178>
#ifdef _WIN32
# ifdef _WIN64 # ifdef _WIN64
# define PRIsizet PRIu64 # define PRIsizet PRIu64
# define PRIexitcode "lu"
# else # else
# define PRIsizet PRIu32 # define PRIsizet PRIu32
# define PRIexitcode "u"
# endif # endif
#else
# define PRIsizet "zu"
# define PRIexitcode "d"
#endif
#ifdef __WINDOWS__
# include <winsock2.h> // not needed here, but must never be included AFTER windows.h
# include <windows.h>
# define PROCESS_NONE NULL # define PROCESS_NONE NULL
typedef HANDLE process_t; typedef HANDLE process_t;
typedef DWORD exit_code_t; typedef DWORD exit_code_t;
#else #else
# include <sys/types.h> # include <sys/types.h>
# define PRIsizet "zu"
# define PRIexitcode "d"
# define PROCESS_NONE -1 # define PROCESS_NONE -1
typedef pid_t process_t; typedef pid_t process_t;
typedef int exit_code_t; typedef int exit_code_t;
#endif #endif
# define NO_EXIT_CODE -1 # define NO_EXIT_CODE -1
enum process_result { process_t cmd_execute(const char *path, const char *const argv[]);
PROCESS_SUCCESS, SDL_bool cmd_terminate(process_t pid);
PROCESS_ERROR_GENERIC, SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
PROCESS_ERROR_MISSING_BINARY,
};
enum process_result process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
cmd_execute(const char *path, const char *const argv[], process_t *process); process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
bool process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
cmd_terminate(process_t pid); process_t adb_push(const char *serial, const char *local, const char *remote);
process_t adb_remove_path(const char *serial, const char *path);
bool
cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t
adb_execute(const char *serial, const char *const adb_cmd[], int len);
process_t
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name);
process_t
adb_forward_remove(const char *serial, uint16_t local_port);
process_t
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
process_t
adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t
adb_push(const char *serial, const char *local, const char *remote);
process_t
adb_install(const char *serial, const char *local);
// convenience function to wait for a successful process execution // convenience function to wait for a successful process execution
// automatically log process errors with the provided process name // automatically log process errors with the provided process name
bool SDL_bool process_check_success(process_t process, const char *name);
process_check_success(process_t process, const char *name);
#endif #endif

View File

@@ -1,26 +1,24 @@
#ifndef COMMON_H #ifndef COMMON_H
#define COMMON_H #define COMMON_H
#include <stdint.h> #include <SDL2/SDL_stdinc.h>
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
#define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y)
struct size { struct size {
uint16_t width; Uint16 width;
uint16_t height; Uint16 height;
}; };
struct point { struct point {
int32_t x; Uint16 x;
int32_t y; Uint16 y;
}; };
struct position { struct position {
// The video screen size may be different from the real device screen size, // The video screen size may be different from the real device screen size,
// so store to which size the absolute position apply, to scale it // so store to which size the absolute position apply, to scale it accordingly.
// accordingly.
struct size screen_size; struct size screen_size;
struct point point; struct point point;
}; };

View File

@@ -1,46 +0,0 @@
#ifndef COMPAT_H
#define COMPAT_H
#include <libavformat/version.h>
#include <SDL2/SDL_version.h>
// In ffmpeg/doc/APIchanges:
// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h
// Add AVStream.codecpar, deprecate AVStream.codec.
#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \
|| (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0))
# define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
#endif
// In ffmpeg/doc/APIchanges:
// 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
// Deprecate use of av_register_input_format(), av_register_output_format(),
// av_register_all(), av_iformat_next(), av_oformat_next().
// Add av_demuxer_iterate(), and av_muxer_iterate().
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
# define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
#else
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#endif
// In ffmpeg/doc/APIchanges:
// 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h
// Add a new audio/video encoding and decoding API with decoupled input
// and output -- avcodec_send_packet(), avcodec_receive_frame(),
// avcodec_send_frame() and avcodec_receive_packet().
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
# define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
#endif
#if SDL_VERSION_ATLEAST(2, 0, 5)
// <https://wiki.libsdl.org/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH>
# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
// <https://wiki.libsdl.org/SDL_GetDisplayUsableBounds>
# define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS
// <https://wiki.libsdl.org/SDL_WindowFlags>
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
#endif
#endif

View File

@@ -1,110 +0,0 @@
#include "control_event.h"
#include <string.h>
#include "buffer_util.h"
#include "lock_util.h"
#include "log.h"
static void
write_position(uint8_t *buf, const struct position *position) {
buffer_write32be(&buf[0], position->point.x);
buffer_write32be(&buf[4], position->point.y);
buffer_write16be(&buf[8], position->screen_size.width);
buffer_write16be(&buf[10], position->screen_size.height);
}
int
control_event_serialize(const struct control_event *event, unsigned char *buf) {
buf[0] = event->type;
switch (event->type) {
case CONTROL_EVENT_TYPE_KEYCODE:
buf[1] = event->keycode_event.action;
buffer_write32be(&buf[2], event->keycode_event.keycode);
buffer_write32be(&buf[6], event->keycode_event.metastate);
return 10;
case CONTROL_EVENT_TYPE_TEXT: {
// write length (2 bytes) + string (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;
}
buffer_write16be(&buf[1], (uint16_t) len);
memcpy(&buf[3], event->text_event.text, len);
return 3 + len;
}
case CONTROL_EVENT_TYPE_MOUSE:
buf[1] = event->mouse_event.action;
buffer_write32be(&buf[2], event->mouse_event.buttons);
write_position(&buf[6], &event->mouse_event.position);
return 18;
case CONTROL_EVENT_TYPE_SCROLL:
write_position(&buf[1], &event->scroll_event.position);
buffer_write32be(&buf[13], (uint32_t) event->scroll_event.hscroll);
buffer_write32be(&buf[17], (uint32_t) event->scroll_event.vscroll);
return 21;
case CONTROL_EVENT_TYPE_COMMAND:
buf[1] = event->command_event.action;
return 2;
default:
LOGW("Unknown event type: %u", (unsigned) event->type);
return 0;
}
}
void
control_event_destroy(struct control_event *event) {
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
SDL_free(event->text_event.text);
}
}
bool
control_event_queue_is_empty(const struct control_event_queue *queue) {
return queue->head == queue->tail;
}
bool
control_event_queue_is_full(const struct control_event_queue *queue) {
return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail;
}
bool
control_event_queue_init(struct control_event_queue *queue) {
queue->head = 0;
queue->tail = 0;
// the current implementation may not fail
return true;
}
void
control_event_queue_destroy(struct control_event_queue *queue) {
int i = queue->tail;
while (i != queue->head) {
control_event_destroy(&queue->data[i]);
i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE;
}
}
bool
control_event_queue_push(struct control_event_queue *queue,
const struct control_event *event) {
if (control_event_queue_is_full(queue)) {
return false;
}
queue->data[queue->head] = *event;
queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE;
return true;
}
bool
control_event_queue_take(struct control_event_queue *queue,
struct control_event *event) {
if (control_event_queue_is_empty(queue)) {
return false;
}
*event = queue->data[queue->tail];
queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE;
return true;
}

99
app/src/controlevent.c Normal file
View File

@@ -0,0 +1,99 @@
#include "controlevent.h"
#include <SDL2/SDL_stdinc.h>
#include "lockutil.h"
#include "log.h"
static inline void write16(Uint8 *buf, Uint16 value) {
buf[0] = value >> 8;
buf[1] = value;
}
static inline void write32(Uint8 *buf, Uint32 value) {
buf[0] = value >> 24;
buf[1] = value >> 16;
buf[2] = value >> 8;
buf[3] = value;
}
static void write_position(Uint8 *buf, const struct position *position) {
write16(&buf[0], position->point.x);
write16(&buf[2], position->point.y);
write16(&buf[4], position->screen_size.width);
write16(&buf[6], position->screen_size.height);
}
int control_event_serialize(const struct control_event *event, unsigned char *buf) {
buf[0] = event->type;
switch (event->type) {
case CONTROL_EVENT_TYPE_KEYCODE:
buf[1] = event->keycode_event.action;
write32(&buf[2], event->keycode_event.keycode);
write32(&buf[6], event->keycode_event.metastate);
return 10;
case CONTROL_EVENT_TYPE_TEXT: {
// write length (1 byte) + date (non nul-terminated)
size_t len = strlen(event->text_event.text);
if (len > TEXT_MAX_LENGTH) {
len = TEXT_MAX_LENGTH;
}
buf[1] = (Uint8) len;
memcpy(&buf[2], &event->text_event.text, len);
return 2 + len;
}
case CONTROL_EVENT_TYPE_MOUSE:
buf[1] = event->mouse_event.action;
write32(&buf[2], event->mouse_event.buttons);
write_position(&buf[6], &event->mouse_event.position);
return 14;
case CONTROL_EVENT_TYPE_SCROLL:
write_position(&buf[1], &event->scroll_event.position);
write32(&buf[9], (Uint32) event->scroll_event.hscroll);
write32(&buf[13], (Uint32) event->scroll_event.vscroll);
return 17;
case CONTROL_EVENT_TYPE_COMMAND:
buf[1] = event->command_event.action;
return 2;
default:
LOGW("Unknown event type: %u", (unsigned) event->type);
return 0;
}
}
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
return queue->head == queue->tail;
}
SDL_bool control_event_queue_is_full(const struct control_event_queue *queue) {
return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail;
}
SDL_bool control_event_queue_init(struct control_event_queue *queue) {
queue->head = 0;
queue->tail = 0;
// the current implementation may not fail
return SDL_TRUE;
}
void control_event_queue_destroy(struct control_event_queue *queue) {
// nothing to do in the current implementation
}
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {
if (control_event_queue_is_full(queue)) {
return SDL_FALSE;
}
queue->data[queue->head] = *event;
queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE;
return SDL_TRUE;
}
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event) {
if (control_event_queue_is_empty(queue)) {
return SDL_FALSE;
}
*event = queue->data[queue->tail];
queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE;
return SDL_TRUE;
}

View File

@@ -1,17 +1,16 @@
#ifndef CONTROLEVENT_H #ifndef CONTROLEVENT_H
#define CONTROLEVENT_H #define CONTROLEVENT_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include "android/input.h" #include "android/input.h"
#include "android/keycodes.h" #include "android/keycodes.h"
#include "common.h" #include "common.h"
#define CONTROL_EVENT_QUEUE_SIZE 64 #define CONTROL_EVENT_QUEUE_SIZE 64
#define TEXT_MAX_LENGTH 300 #define SERIALIZED_EVENT_MAX_SIZE 33
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH #define TEXT_MAX_LENGTH 31
enum control_event_type { enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE, CONTROL_EVENT_TYPE_KEYCODE,
@@ -21,11 +20,7 @@ enum control_event_type {
CONTROL_EVENT_TYPE_COMMAND, CONTROL_EVENT_TYPE_COMMAND,
}; };
enum control_event_command { #define CONTROL_EVENT_COMMAND_SCREEN_ON 0
CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON,
CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL,
CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL,
};
struct control_event { struct control_event {
enum control_event_type type; enum control_event_type type;
@@ -36,7 +31,7 @@ struct control_event {
enum android_metastate metastate; enum android_metastate metastate;
} keycode_event; } keycode_event;
struct { struct {
char *text; // owned, to be freed by SDL_free() char text[TEXT_MAX_LENGTH + 1]; // nul-terminated string
} text_event; } text_event;
struct { struct {
enum android_motionevent_action action; enum android_motionevent_action action;
@@ -45,11 +40,11 @@ struct control_event {
} mouse_event; } mouse_event;
struct { struct {
struct position position; struct position position;
int32_t hscroll; Sint32 hscroll;
int32_t vscroll; Sint32 vscroll;
} scroll_event; } scroll_event;
struct { struct {
enum control_event_command action; int action;
} command_event; } command_event;
}; };
}; };
@@ -61,31 +56,16 @@ struct control_event_queue {
}; };
// buf size must be at least SERIALIZED_EVENT_MAX_SIZE // buf size must be at least SERIALIZED_EVENT_MAX_SIZE
int int control_event_serialize(const struct control_event *event, unsigned char *buf);
control_event_serialize(const struct control_event *event, unsigned char *buf);
bool SDL_bool control_event_queue_init(struct control_event_queue *queue);
control_event_queue_init(struct control_event_queue *queue); void control_event_queue_destroy(struct control_event_queue *queue);
void SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue);
control_event_queue_destroy(struct control_event_queue *queue); SDL_bool control_event_queue_is_full(const struct control_event_queue *queue);
bool
control_event_queue_is_empty(const struct control_event_queue *queue);
bool
control_event_queue_is_full(const struct control_event_queue *queue);
// event is copied, the queue does not use the event after the function returns // event is copied, the queue does not use the event after the function returns
bool SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event);
control_event_queue_push(struct control_event_queue *queue, SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event);
const struct control_event *event);
bool
control_event_queue_take(struct control_event_queue *queue,
struct control_event *event);
void
control_event_destroy(struct control_event *event);
#endif #endif

View File

@@ -1,45 +1,38 @@
#include "controller.h" #include "controller.h"
#include <SDL2/SDL_assert.h> #include "lockutil.h"
#include "config.h"
#include "lock_util.h"
#include "log.h" #include "log.h"
bool SDL_bool controller_init(struct controller *controller, socket_t video_socket) {
controller_init(struct controller *controller, socket_t video_socket) {
if (!control_event_queue_init(&controller->queue)) { if (!control_event_queue_init(&controller->queue)) {
return false; return SDL_FALSE;
} }
if (!(controller->mutex = SDL_CreateMutex())) { if (!(controller->mutex = SDL_CreateMutex())) {
return false; return SDL_FALSE;
} }
if (!(controller->event_cond = SDL_CreateCond())) { if (!(controller->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(controller->mutex); SDL_DestroyMutex(controller->mutex);
return false; return SDL_FALSE;
} }
controller->video_socket = video_socket; controller->video_socket = video_socket;
controller->stopped = false; controller->stopped = SDL_FALSE;
return true; return SDL_TRUE;
} }
void void controller_destroy(struct controller *controller) {
controller_destroy(struct controller *controller) {
SDL_DestroyCond(controller->event_cond); SDL_DestroyCond(controller->event_cond);
SDL_DestroyMutex(controller->mutex); SDL_DestroyMutex(controller->mutex);
control_event_queue_destroy(&controller->queue); control_event_queue_destroy(&controller->queue);
} }
bool SDL_bool controller_push_event(struct controller *controller, const struct control_event *event) {
controller_push_event(struct controller *controller, SDL_bool res;
const struct control_event *event) {
bool res;
mutex_lock(controller->mutex); mutex_lock(controller->mutex);
bool was_empty = control_event_queue_is_empty(&controller->queue); SDL_bool was_empty = control_event_queue_is_empty(&controller->queue);
res = control_event_queue_push(&controller->queue, event); res = control_event_queue_push(&controller->queue, event);
if (was_empty) { if (was_empty) {
cond_signal(controller->event_cond); cond_signal(controller->event_cond);
@@ -48,72 +41,60 @@ controller_push_event(struct controller *controller,
return res; return res;
} }
static bool static SDL_bool process_event(struct controller *controller, const struct control_event *event) {
process_event(struct controller *controller,
const struct control_event *event) {
unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE]; unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE];
int length = control_event_serialize(event, serialized_event); int length = control_event_serialize(event, serialized_event);
if (!length) { if (!length) {
return false; return SDL_FALSE;
} }
int w = net_send_all(controller->video_socket, serialized_event, length); int w = net_send_all(controller->video_socket, serialized_event, length);
return w == length; return w == length;
} }
static int static int run_controller(void *data) {
run_controller(void *data) {
struct controller *controller = data; struct controller *controller = data;
mutex_lock(controller->mutex);
for (;;) { for (;;) {
mutex_lock(controller->mutex); while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
while (!controller->stopped
&& control_event_queue_is_empty(&controller->queue)) {
cond_wait(controller->event_cond, controller->mutex); cond_wait(controller->event_cond, controller->mutex);
} }
if (controller->stopped) { if (controller->stopped) {
// stop immediately, do not process further events // stop immediately, do not process further events
mutex_unlock(controller->mutex);
break; break;
} }
struct control_event event; struct control_event event;
bool non_empty = control_event_queue_take(&controller->queue, while (control_event_queue_take(&controller->queue, &event)) {
&event); if (!process_event(controller, &event)) {
SDL_assert(non_empty); LOGD("Cannot write event to socket");
mutex_unlock(controller->mutex); goto end;
}
bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
break;
} }
} }
end:
mutex_unlock(controller->mutex);
return 0; return 0;
} }
bool SDL_bool controller_start(struct controller *controller) {
controller_start(struct controller *controller) {
LOGD("Starting controller thread"); LOGD("Starting controller thread");
controller->thread = SDL_CreateThread(run_controller, "controller", controller->thread = SDL_CreateThread(run_controller, "controller", controller);
controller);
if (!controller->thread) { if (!controller->thread) {
LOGC("Could not start controller thread"); LOGC("Could not start controller thread");
return false; return SDL_FALSE;
} }
return true; return SDL_TRUE;
} }
void void controller_stop(struct controller *controller) {
controller_stop(struct controller *controller) {
mutex_lock(controller->mutex); mutex_lock(controller->mutex);
controller->stopped = true; controller->stopped = SDL_TRUE;
cond_signal(controller->event_cond); cond_signal(controller->event_cond);
mutex_unlock(controller->mutex); mutex_unlock(controller->mutex);
} }
void void controller_join(struct controller *controller) {
controller_join(struct controller *controller) {
SDL_WaitThread(controller->thread, NULL); SDL_WaitThread(controller->thread, NULL);
} }

View File

@@ -1,10 +1,10 @@
#ifndef CONTROL_H #ifndef CONTROL_H
#define CONTROL_H #define CONTROL_H
#include "control_event.h" #include "controlevent.h"
#include <stdbool.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "net.h" #include "net.h"
@@ -14,28 +14,18 @@ struct controller {
SDL_Thread *thread; SDL_Thread *thread;
SDL_mutex *mutex; SDL_mutex *mutex;
SDL_cond *event_cond; SDL_cond *event_cond;
bool stopped; SDL_bool stopped;
struct control_event_queue queue; struct control_event_queue queue;
}; };
bool SDL_bool controller_init(struct controller *controller, socket_t video_socket);
controller_init(struct controller *controller, socket_t video_socket); void controller_destroy(struct controller *controller);
void SDL_bool controller_start(struct controller *controller);
controller_destroy(struct controller *controller); void controller_stop(struct controller *controller);
void controller_join(struct controller *controller);
bool
controller_start(struct controller *controller);
void
controller_stop(struct controller *controller);
void
controller_join(struct controller *controller);
// expose simple API to hide control_event_queue // expose simple API to hide control_event_queue
bool SDL_bool controller_push_event(struct controller *controller, const struct control_event *event);
controller_push_event(struct controller *controller,
const struct control_event *event);
#endif #endif

View File

@@ -1,10 +1,9 @@
#include "convert.h" #include "convert.h"
#define MAP(FROM, TO) case FROM: *to = TO; return true #define MAP(FROM, TO) case FROM: *to = TO; return SDL_TRUE
#define FAIL default: return false #define FAIL default: return SDL_FALSE
static bool static SDL_bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP);
@@ -12,8 +11,7 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
} }
} }
static enum android_metastate static enum android_metastate autocomplete_metastate(enum android_metastate metastate) {
autocomplete_metastate(enum android_metastate metastate) {
// fill dependant flags // fill dependant flags
if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
metastate |= AMETA_SHIFT_ON; metastate |= AMETA_SHIFT_ON;
@@ -32,8 +30,7 @@ autocomplete_metastate(enum android_metastate metastate) {
} }
static enum android_metastate static enum android_metastate convert_meta_state(SDL_Keymod mod) {
convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0; enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) { if (mod & KMOD_LSHIFT) {
metastate |= AMETA_SHIFT_LEFT_ON; metastate |= AMETA_SHIFT_LEFT_ON;
@@ -73,11 +70,9 @@ convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate); return autocomplete_metastate(metastate);
} }
static bool static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to) {
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
switch (from) { switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER); MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL); MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB); MAP(SDLK_TAB, AKEYCODE_TAB);
@@ -90,45 +85,11 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP); MAP(SDLK_UP, AKEYCODE_DPAD_UP);
}
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return false;
}
// if ALT and META are not pressed, also handle letters and space
switch (from) {
MAP(SDLK_a, AKEYCODE_A);
MAP(SDLK_b, AKEYCODE_B);
MAP(SDLK_c, AKEYCODE_C);
MAP(SDLK_d, AKEYCODE_D);
MAP(SDLK_e, AKEYCODE_E);
MAP(SDLK_f, AKEYCODE_F);
MAP(SDLK_g, AKEYCODE_G);
MAP(SDLK_h, AKEYCODE_H);
MAP(SDLK_i, AKEYCODE_I);
MAP(SDLK_j, AKEYCODE_J);
MAP(SDLK_k, AKEYCODE_K);
MAP(SDLK_l, AKEYCODE_L);
MAP(SDLK_m, AKEYCODE_M);
MAP(SDLK_n, AKEYCODE_N);
MAP(SDLK_o, AKEYCODE_O);
MAP(SDLK_p, AKEYCODE_P);
MAP(SDLK_q, AKEYCODE_Q);
MAP(SDLK_r, AKEYCODE_R);
MAP(SDLK_s, AKEYCODE_S);
MAP(SDLK_t, AKEYCODE_T);
MAP(SDLK_u, AKEYCODE_U);
MAP(SDLK_v, AKEYCODE_V);
MAP(SDLK_w, AKEYCODE_W);
MAP(SDLK_x, AKEYCODE_X);
MAP(SDLK_y, AKEYCODE_Y);
MAP(SDLK_z, AKEYCODE_Z);
MAP(SDLK_SPACE, AKEYCODE_SPACE);
FAIL; FAIL;
} }
} }
static bool static SDL_bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) { switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
@@ -136,8 +97,7 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
} }
} }
static enum android_motionevent_buttons static enum android_motionevent_buttons convert_mouse_buttons(Uint32 state) {
convert_mouse_buttons(uint32_t state) {
enum android_motionevent_buttons buttons = 0; enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) { if (state & SDL_BUTTON_LMASK) {
buttons |= AMOTION_EVENT_BUTTON_PRIMARY; buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
@@ -157,71 +117,73 @@ convert_mouse_buttons(uint32_t state) {
return buttons; return buttons;
} }
bool SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, struct control_event *to) {
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_KEYCODE; to->type = CONTROL_EVENT_TYPE_KEYCODE;
if (!convert_keycode_action(from->type, &to->keycode_event.action)) { if (!convert_keycode_action(from->type, &to->keycode_event.action)) {
return false; return SDL_FALSE;
} }
uint16_t mod = from->keysym.mod; if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode)) {
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) { return SDL_FALSE;
return false;
} }
to->keycode_event.metastate = convert_meta_state(mod); to->keycode_event.metastate = convert_meta_state(from->keysym.mod);
return true; return SDL_TRUE;
} }
bool SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, struct size screen_size,
struct size screen_size, struct hidpi_scale *hidpi_scale,
struct control_event *to) { struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE; to->type = CONTROL_EVENT_TYPE_MOUSE;
if (!convert_mouse_action(from->type, &to->mouse_event.action)) { if (!convert_mouse_action(from->type, &to->mouse_event.action)) {
return false; return SDL_FALSE;
} }
to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
to->mouse_event.position.screen_size = screen_size; to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
return true; Sint32 x = from->x;
Sint32 y = from->y;
hidpi_unscale_coordinates(hidpi_scale, &x, &y);
to->mouse_event.position.point.x = (Uint16) x;
to->mouse_event.position.point.y = (Uint16) y;
return SDL_TRUE;
} }
bool SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, struct size screen_size,
struct size screen_size, struct hidpi_scale *hidpi_scale,
struct control_event *to) { struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE; to->type = CONTROL_EVENT_TYPE_MOUSE;
to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE; to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->mouse_event.buttons = convert_mouse_buttons(from->state); to->mouse_event.buttons = convert_mouse_buttons(from->state);
to->mouse_event.position.screen_size = screen_size; to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
return true; Sint32 x = from->x;
Sint32 y = from->y;
hidpi_unscale_coordinates(hidpi_scale, &x, &y);
to->mouse_event.position.point.x = (Uint16) x;
to->mouse_event.position.point.y = (Uint16) y;
return SDL_TRUE;
} }
bool SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, struct position position,
struct position position, struct control_event *to) {
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_SCROLL; to->type = CONTROL_EVENT_TYPE_SCROLL;
to->scroll_event.position = position; to->scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1; int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling to->scroll_event.hscroll = mul * from->x;
// 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; to->scroll_event.vscroll = mul * from->y;
return true; return SDL_TRUE;
} }

View File

@@ -1,10 +1,12 @@
#ifndef CONVERT_H #ifndef CONVERT_H
#define CONVERT_H #define CONVERT_H
#include <stdbool.h> #include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include "control_event.h" #include "common.h"
#include "controlevent.h"
#include "hidpi.h"
struct complete_mouse_motion_event { struct complete_mouse_motion_event {
SDL_MouseMotionEvent *mouse_motion_event; SDL_MouseMotionEvent *mouse_motion_event;
@@ -16,26 +18,23 @@ struct complete_mouse_wheel_event {
struct point position; struct point position;
}; };
bool SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, struct control_event *to);
struct control_event *to); SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct hidpi_scale *hidpi_scale,
struct control_event *to);
bool // the video size may be different from the real device size, so we need the size
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, // to which the absolute position apply, to scale it accordingly
struct size screen_size, SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct control_event *to); struct size screen_size,
struct hidpi_scale *hidpi_scale,
// the video size may be different from the real device size, so we need the struct control_event *to);
// size to which the absolute position apply, to scale it accordingly
bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_event *to);
// on Android, a scroll event requires the current mouse position // on Android, a scroll event requires the current mouse position
bool SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, struct position position,
struct position position, struct control_event *to);
struct control_event *to);
#endif #endif

View File

@@ -1,29 +1,28 @@
#include "decoder.h" #include "decoder.h"
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include <unistd.h> #include <unistd.h>
#include "compat.h"
#include "config.h" #include "config.h"
#include "buffer_util.h"
#include "events.h" #include "events.h"
#include "lock_util.h" #include "frames.h"
#include "lockutil.h"
#include "log.h" #include "log.h"
#include "recorder.h"
#include "video_buffer.h" #define BUFSIZE 0x10000
static int read_packet(void *opaque, uint8_t *buf, int buf_size) {
struct decoder *decoder = opaque;
return net_recv(decoder->video_socket, buf, buf_size);
}
// set the decoded frame as ready for rendering, and notify // set the decoded frame as ready for rendering, and notify
static void static void push_frame(struct decoder *decoder) {
push_frame(struct decoder *decoder) { SDL_bool previous_frame_consumed = frames_offer_decoded_frame(decoder->frames);
bool previous_frame_skipped; if (!previous_frame_consumed) {
video_buffer_offer_decoded_frame(decoder->video_buffer,
&previous_frame_skipped);
if (previous_frame_skipped) {
// the previous EVENT_NEW_FRAME will consume this frame // the previous EVENT_NEW_FRAME will consume this frame
return; return;
} }
@@ -33,71 +32,140 @@ push_frame(struct decoder *decoder) {
SDL_PushEvent(&new_frame_event); SDL_PushEvent(&new_frame_event);
} }
void static void notify_stopped(void) {
decoder_init(struct decoder *decoder, struct video_buffer *vb) { SDL_Event stop_event;
decoder->video_buffer = vb; stop_event.type = EVENT_DECODER_STOPPED;
SDL_PushEvent(&stop_event);
} }
bool static int run_decoder(void *data) {
decoder_open(struct decoder *decoder, const AVCodec *codec) { struct decoder *decoder = data;
decoder->codec_ctx = avcodec_alloc_context3(codec); int ret = 0;
if (!decoder->codec_ctx) {
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
return -1;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGC("Could not allocate decoder context"); LOGC("Could not allocate decoder context");
return false; return -1;
} }
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Could not open codec"); LOGE("Could not open H.264 codec");
avcodec_free_context(&decoder->codec_ctx); ret = -1;
return false; goto run_finally_free_codec_ctx;
} }
return true; AVFormatContext *format_ctx = avformat_alloc_context();
} if (!format_ctx) {
LOGC("Could not allocate format context");
ret = -1;
goto run_finally_close_codec;
}
void unsigned char *buffer = av_malloc(BUFSIZE);
decoder_close(struct decoder *decoder) { if (!buffer) {
avcodec_close(decoder->codec_ctx); LOGC("Could not allocate buffer");
avcodec_free_context(&decoder->codec_ctx); ret = -1;
} goto run_finally_free_format_ctx;
}
bool AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL);
decoder_push(struct decoder *decoder, const AVPacket *packet) { if (!avio_ctx) {
LOGC("Could not allocate avio context");
// 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;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
ret = -1;
goto run_finally_free_avio_ctx;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet) && !avio_ctx->eof_reached) {
// the new decoding/encoding API has been introduced by: // the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726> // <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 37, 0)
int ret; while (packet.size > 0) {
if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { int got_picture;
LOGE("Could not send video packet: %d", ret); int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet);
return false; if (len < 0) {
} LOGE("Could not decode video packet: %d", len);
ret = avcodec_receive_frame(decoder->codec_ctx, goto run_quit;
decoder->video_buffer->decoding_frame); }
if (!ret) { if (got_picture) {
// a frame was received push_frame(decoder);
push_frame(decoder); }
} else if (ret != AVERROR(EAGAIN)) { packet.size -= len;
LOGE("Could not receive video frame: %d", ret); packet.data += len;
return false; }
}
#else #else
int got_picture; int ret;
int len = avcodec_decode_video2(decoder->codec_ctx, if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
decoder->video_buffer->decoding_frame, LOGE("Could not send video packet: %d", ret);
&got_picture, goto run_quit;
packet); }
if (len < 0) { if ((ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame)) < 0) {
LOGE("Could not decode video packet: %d", len); LOGE("Could not receive video frame: %d", ret);
return false; goto run_quit;
} }
if (got_picture) {
push_frame(decoder); push_frame(decoder);
}
#endif #endif
return true; }
LOGD("End of frames");
run_quit:
avformat_close_input(&format_ctx);
run_finally_free_avio_ctx:
av_freep(&avio_ctx);
run_finally_free_format_ctx:
avformat_free_context(format_ctx);
run_finally_close_codec:
avcodec_close(codec_ctx);
run_finally_free_codec_ctx:
avcodec_free_context(&codec_ctx);
notify_stopped();
return ret;
} }
void void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) {
decoder_interrupt(struct decoder *decoder) { decoder->frames = frames;
video_buffer_interrupt(decoder->video_buffer); decoder->video_socket = video_socket;
}
SDL_bool decoder_start(struct decoder *decoder) {
LOGD("Starting decoder thread");
decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder);
if (!decoder->thread) {
LOGC("Could not start decoder thread");
return SDL_FALSE;
}
return SDL_TRUE;
}
void decoder_stop(struct decoder *decoder) {
frames_stop(decoder->frames);
}
void decoder_join(struct decoder *decoder) {
SDL_WaitThread(decoder->thread, NULL);
} }

View File

@@ -1,29 +1,23 @@
#ifndef DECODER_H #ifndef DECODER_H
#define DECODER_H #define DECODER_H
#include <stdbool.h> #include <SDL2/SDL_stdinc.h>
#include <libavformat/avformat.h> #include <SDL2/SDL_thread.h>
struct video_buffer; #include "net.h"
struct frames;
struct decoder { struct decoder {
struct video_buffer *video_buffer; struct frames *frames;
AVCodecContext *codec_ctx; socket_t video_socket;
SDL_Thread *thread;
SDL_mutex *mutex;
}; };
void void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket);
decoder_init(struct decoder *decoder, struct video_buffer *vb); SDL_bool decoder_start(struct decoder *decoder);
void decoder_stop(struct decoder *decoder);
bool void decoder_join(struct decoder *decoder);
decoder_open(struct decoder *decoder, const AVCodec *codec);
void
decoder_close(struct decoder *decoder);
bool
decoder_push(struct decoder *decoder, const AVPacket *packet);
void
decoder_interrupt(struct decoder *decoder);
#endif #endif

View File

@@ -1,22 +1,18 @@
#include "device.h" #include "device.h"
#include "log.h" #include "log.h"
bool SDL_bool device_read_info(socket_t device_socket, char *device_name, struct size *size) {
device_read_info(socket_t device_socket, char *device_name, struct size *size) {
unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4];
int r = net_recv_all(device_socket, buf, sizeof(buf)); int r = net_recv_all(device_socket, buf, sizeof(buf));
if (r < DEVICE_NAME_FIELD_LENGTH + 4) { if (r < DEVICE_NAME_FIELD_LENGTH + 4) {
LOGE("Could not retrieve device information"); LOGE("Could not retrieve device information");
return false; return SDL_FALSE;
} }
// in case the client sends garbage buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; // in case the client sends garbage
buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; // strcpy is safe here, since name contains at least DEVICE_NAME_FIELD_LENGTH bytes
// strcpy is safe here, since name contains at least // and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
// DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
strcpy(device_name, (char *) buf); strcpy(device_name, (char *) buf);
size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 1];
| buf[DEVICE_NAME_FIELD_LENGTH + 1]; size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 3];
size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) return SDL_TRUE;
| buf[DEVICE_NAME_FIELD_LENGTH + 3];
return true;
} }

View File

@@ -1,16 +1,14 @@
#ifndef DEVICE_H #ifndef DEVICE_H
#define DEVICE_H #define DEVICE_H
#include <stdbool.h> #include <SDL2/SDL_stdinc.h>
#include "common.h" #include "common.h"
#include "net.h" #include "net.h"
#define DEVICE_NAME_FIELD_LENGTH 64 #define DEVICE_NAME_FIELD_LENGTH 64
#define DEVICE_SDCARD_PATH "/sdcard/"
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes // name must be at least DEVICE_NAME_FIELD_LENGTH bytes
bool SDL_bool device_read_info(socket_t device_socket, char *name, struct size *frame_size);
device_read_info(socket_t device_socket, char *name, struct size *frame_size);
#endif #endif

View File

@@ -1,3 +1,3 @@
#define EVENT_NEW_SESSION SDL_USEREVENT #define EVENT_NEW_SESSION SDL_USEREVENT
#define EVENT_NEW_FRAME (SDL_USEREVENT + 1) #define EVENT_NEW_FRAME (SDL_USEREVENT + 1)
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2) #define EVENT_DECODER_STOPPED (SDL_USEREVENT + 2)

View File

@@ -1,251 +0,0 @@
#include "file_handler.h"
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "command.h"
#include "device.h"
#include "lock_util.h"
#include "log.h"
struct request {
file_handler_action_t action;
const char *file;
};
static struct request *
request_new(file_handler_action_t action, const char *file) {
struct request *req = SDL_malloc(sizeof(*req));
if (!req) {
return NULL;
}
req->action = action;
req->file = file;
return req;
}
static void
request_free(struct request *req) {
if (!req) {
return;
}
SDL_free((void *) req->file);
SDL_free((void *) req);
}
static bool
request_queue_is_empty(const struct request_queue *queue) {
return queue->head == queue->tail;
}
static bool
request_queue_is_full(const struct request_queue *queue) {
return (queue->head + 1) % REQUEST_QUEUE_SIZE == queue->tail;
}
static bool
request_queue_init(struct request_queue *queue) {
queue->head = 0;
queue->tail = 0;
return true;
}
static void
request_queue_destroy(struct request_queue *queue) {
int i = queue->tail;
while (i != queue->head) {
request_free(queue->reqs[i]);
i = (i + 1) % REQUEST_QUEUE_SIZE;
}
}
static bool
request_queue_push(struct request_queue *queue, struct request *req) {
if (request_queue_is_full(queue)) {
return false;
}
queue->reqs[queue->head] = req;
queue->head = (queue->head + 1) % REQUEST_QUEUE_SIZE;
return true;
}
static bool
request_queue_take(struct request_queue *queue, struct request **req) {
if (request_queue_is_empty(queue)) {
return false;
}
// transfer ownership
*req = queue->reqs[queue->tail];
queue->tail = (queue->tail + 1) % REQUEST_QUEUE_SIZE;
return true;
}
bool
file_handler_init(struct file_handler *file_handler, const char *serial) {
if (!request_queue_init(&file_handler->queue)) {
return false;
}
if (!(file_handler->mutex = SDL_CreateMutex())) {
return false;
}
if (!(file_handler->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(file_handler->mutex);
return false;
}
if (serial) {
file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) {
LOGW("Cannot strdup serial");
SDL_DestroyMutex(file_handler->mutex);
return false;
}
} else {
file_handler->serial = NULL;
}
// lazy initialization
file_handler->initialized = false;
file_handler->stopped = false;
file_handler->current_process = PROCESS_NONE;
return true;
}
void
file_handler_destroy(struct file_handler *file_handler) {
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex);
request_queue_destroy(&file_handler->queue);
SDL_free((void *) file_handler->serial);
}
static process_t
install_apk(const char *serial, const char *file) {
return adb_install(serial, file);
}
static process_t
push_file(const char *serial, const char *file) {
return adb_push(serial, file, DEVICE_SDCARD_PATH);
}
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
const char *file) {
bool res;
// start file_handler if it's used for the first time
if (!file_handler->initialized) {
if (!file_handler_start(file_handler)) {
return false;
}
file_handler->initialized = true;
}
LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push",
file);
struct request *req = request_new(action, file);
if (!req) {
LOGE("Could not create request");
return false;
}
mutex_lock(file_handler->mutex);
bool was_empty = request_queue_is_empty(&file_handler->queue);
res = request_queue_push(&file_handler->queue, req);
if (was_empty) {
cond_signal(file_handler->event_cond);
}
mutex_unlock(file_handler->mutex);
return res;
}
static int
run_file_handler(void *data) {
struct file_handler *file_handler = data;
for (;;) {
mutex_lock(file_handler->mutex);
file_handler->current_process = PROCESS_NONE;
while (!file_handler->stopped
&& request_queue_is_empty(&file_handler->queue)) {
cond_wait(file_handler->event_cond, file_handler->mutex);
}
if (file_handler->stopped) {
// stop immediately, do not process further events
mutex_unlock(file_handler->mutex);
break;
}
struct request *req;
bool non_empty = request_queue_take(&file_handler->queue, &req);
SDL_assert(non_empty);
process_t process;
if (req->action == ACTION_INSTALL_APK) {
LOGI("Installing %s...", req->file);
process = install_apk(file_handler->serial, req->file);
} else {
LOGI("Pushing %s...", req->file);
process = push_file(file_handler->serial, req->file);
}
file_handler->current_process = process;
mutex_unlock(file_handler->mutex);
if (req->action == ACTION_INSTALL_APK) {
if (process_check_success(process, "adb install")) {
LOGI("%s successfully installed", req->file);
} else {
LOGE("Failed to install %s", req->file);
}
} else {
if (process_check_success(process, "adb push")) {
LOGI("%s successfully pushed to /sdcard/", req->file);
} else {
LOGE("Failed to push %s to /sdcard/", req->file);
}
}
request_free(req);
}
return 0;
}
bool
file_handler_start(struct file_handler *file_handler) {
LOGD("Starting file_handler thread");
file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler",
file_handler);
if (!file_handler->thread) {
LOGC("Could not start file_handler thread");
return false;
}
return true;
}
void
file_handler_stop(struct file_handler *file_handler) {
mutex_lock(file_handler->mutex);
file_handler->stopped = true;
cond_signal(file_handler->event_cond);
if (file_handler->current_process != PROCESS_NONE) {
if (!cmd_terminate(file_handler->current_process)) {
LOGW("Cannot terminate install process");
}
cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE;
}
mutex_unlock(file_handler->mutex);
}
void
file_handler_join(struct file_handler *file_handler) {
SDL_WaitThread(file_handler->thread, NULL);
}

View File

@@ -1,54 +0,0 @@
#ifndef FILE_HANDLER_H
#define FILE_HANDLER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "command.h"
#define REQUEST_QUEUE_SIZE 16
typedef enum {
ACTION_INSTALL_APK,
ACTION_PUSH_FILE,
} file_handler_action_t;
struct request_queue {
struct request *reqs[REQUEST_QUEUE_SIZE];
int tail;
int head;
};
struct file_handler {
const char *serial;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *event_cond;
bool stopped;
bool initialized;
process_t current_process;
struct request_queue queue;
};
bool
file_handler_init(struct file_handler *file_handler, const char *serial);
void
file_handler_destroy(struct file_handler *file_handler);
bool
file_handler_start(struct file_handler *file_handler);
void
file_handler_stop(struct file_handler *file_handler);
void
file_handler_join(struct file_handler *file_handler);
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
const char *file);
#endif

View File

@@ -1,35 +0,0 @@
#ifndef FPSCOUNTER_H
#define FPSCOUNTER_H
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
struct fps_counter {
bool started;
uint32_t slice_start; // initialized by SDL_GetTicks()
int nr_rendered;
#ifdef SKIP_FRAMES
int nr_skipped;
#endif
};
void
fps_counter_init(struct fps_counter *counter);
void
fps_counter_start(struct fps_counter *counter);
void
fps_counter_stop(struct fps_counter *counter);
void
fps_counter_add_rendered_frame(struct fps_counter *counter);
#ifdef SKIP_FRAMES
void
fps_counter_add_skipped_frame(struct fps_counter *counter);
#endif
#endif

View File

@@ -1,19 +1,17 @@
#include "fps_counter.h" #include "fpscounter.h"
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include "log.h" #include "log.h"
void void fps_counter_init(struct fps_counter *counter) {
fps_counter_init(struct fps_counter *counter) { counter->started = SDL_FALSE;
counter->started = false;
// no need to initialize the other fields, they are meaningful only when // no need to initialize the other fields, they are meaningful only when
// started is true // started is true
} }
void void fps_counter_start(struct fps_counter *counter) {
fps_counter_start(struct fps_counter *counter) { counter->started = SDL_TRUE;
counter->started = true;
counter->slice_start = SDL_GetTicks(); counter->slice_start = SDL_GetTicks();
counter->nr_rendered = 0; counter->nr_rendered = 0;
#ifdef SKIP_FRAMES #ifdef SKIP_FRAMES
@@ -21,17 +19,14 @@ fps_counter_start(struct fps_counter *counter) {
#endif #endif
} }
void void fps_counter_stop(struct fps_counter *counter) {
fps_counter_stop(struct fps_counter *counter) { counter->started = SDL_FALSE;
counter->started = false;
} }
static void static void display_fps(struct fps_counter *counter) {
display_fps(struct fps_counter *counter) {
#ifdef SKIP_FRAMES #ifdef SKIP_FRAMES
if (counter->nr_skipped) { if (counter->nr_skipped) {
LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, counter->nr_skipped);
counter->nr_skipped);
} else { } else {
#endif #endif
LOGI("%d fps", counter->nr_rendered); LOGI("%d fps", counter->nr_rendered);
@@ -40,13 +35,12 @@ display_fps(struct fps_counter *counter) {
#endif #endif
} }
static void static void check_expired(struct fps_counter *counter) {
check_expired(struct fps_counter *counter) { Uint32 now = SDL_GetTicks();
uint32_t now = SDL_GetTicks();
if (now - counter->slice_start >= 1000) { if (now - counter->slice_start >= 1000) {
display_fps(counter); display_fps(counter);
// add a multiple of one second // add a multiple of one second
uint32_t elapsed_slices = (now - counter->slice_start) / 1000; Uint32 elapsed_slices = (now - counter->slice_start) / 1000;
counter->slice_start += 1000 * elapsed_slices; counter->slice_start += 1000 * elapsed_slices;
counter->nr_rendered = 0; counter->nr_rendered = 0;
#ifdef SKIP_FRAMES #ifdef SKIP_FRAMES
@@ -55,15 +49,13 @@ check_expired(struct fps_counter *counter) {
} }
} }
void void fps_counter_add_rendered_frame(struct fps_counter *counter) {
fps_counter_add_rendered_frame(struct fps_counter *counter) {
check_expired(counter); check_expired(counter);
++counter->nr_rendered; ++counter->nr_rendered;
} }
#ifdef SKIP_FRAMES #ifdef SKIP_FRAMES
void void fps_counter_add_skipped_frame(struct fps_counter *counter) {
fps_counter_add_skipped_frame(struct fps_counter *counter) {
check_expired(counter); check_expired(counter);
++counter->nr_skipped; ++counter->nr_skipped;
} }

26
app/src/fpscounter.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef FPSCOUNTER_H
#define FPSCOUNTER_H
#include <SDL2/SDL_stdinc.h>
#include "config.h"
struct fps_counter {
SDL_bool started;
Uint32 slice_start; // initialized by SDL_GetTicks()
int nr_rendered;
#ifdef SKIP_FRAMES
int nr_skipped;
#endif
};
void fps_counter_init(struct fps_counter *counter);
void fps_counter_start(struct fps_counter *counter);
void fps_counter_stop(struct fps_counter *counter);
void fps_counter_add_rendered_frame(struct fps_counter *counter);
#ifdef SKIP_FRAMES
void fps_counter_add_skipped_frame(struct fps_counter *counter);
#endif
#endif

110
app/src/frames.c Normal file
View File

@@ -0,0 +1,110 @@
#include "frames.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "lockutil.h"
#include "log.h"
SDL_bool frames_init(struct frames *frames) {
if (!(frames->decoding_frame = av_frame_alloc())) {
goto error_0;
}
if (!(frames->rendering_frame = av_frame_alloc())) {
goto error_1;
}
if (!(frames->mutex = SDL_CreateMutex())) {
goto error_2;
}
#ifndef SKIP_FRAMES
if (!(frames->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(frames->mutex);
goto error_2;
}
frames->stopped = SDL_FALSE;
#endif
// there is initially no rendering frame, so consider it has already been
// consumed
frames->rendering_frame_consumed = SDL_TRUE;
fps_counter_init(&frames->fps_counter);
return SDL_TRUE;
error_2:
av_frame_free(&frames->rendering_frame);
error_1:
av_frame_free(&frames->decoding_frame);
error_0:
return SDL_FALSE;
}
void frames_destroy(struct frames *frames) {
#ifndef SKIP_FRAMES
SDL_DestroyCond(frames->rendering_frame_consumed_cond);
#endif
SDL_DestroyMutex(frames->mutex);
av_frame_free(&frames->rendering_frame);
av_frame_free(&frames->decoding_frame);
}
static void frames_swap(struct frames *frames) {
AVFrame *tmp = frames->decoding_frame;
frames->decoding_frame = frames->rendering_frame;
frames->rendering_frame = tmp;
}
SDL_bool frames_offer_decoded_frame(struct frames *frames) {
mutex_lock(frames->mutex);
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then the decoder must wait for the current
// frame to be consumed
while (!frames->rendering_frame_consumed && !frames->stopped) {
cond_wait(frames->rendering_frame_consumed_cond, frames->mutex);
}
#else
if (frames->fps_counter.started && !frames->rendering_frame_consumed) {
fps_counter_add_skipped_frame(&frames->fps_counter);
}
#endif
frames_swap(frames);
SDL_bool previous_frame_consumed = frames->rendering_frame_consumed;
frames->rendering_frame_consumed = SDL_FALSE;
mutex_unlock(frames->mutex);
return previous_frame_consumed;
}
const AVFrame *frames_consume_rendered_frame(struct frames *frames) {
SDL_assert(!frames->rendering_frame_consumed);
frames->rendering_frame_consumed = SDL_TRUE;
if (frames->fps_counter.started) {
fps_counter_add_rendered_frame(&frames->fps_counter);
}
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then notify the decoder the current frame is
// consumed, so that it may push a new one
cond_signal(frames->rendering_frame_consumed_cond);
#endif
return frames->rendering_frame;
}
void frames_stop(struct frames *frames) {
#ifdef SKIP_FRAMES
(void) frames; // unused
#else
mutex_lock(frames->mutex);
frames->stopped = SDL_TRUE;
mutex_unlock(frames->mutex);
// wake up blocking wait
cond_signal(frames->rendering_frame_consumed_cond);
#endif
}

42
app/src/frames.h Normal file
View File

@@ -0,0 +1,42 @@
#ifndef FRAMES_H
#define FRAMES_H
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include "config.h"
#include "fpscounter.h"
// forward declarations
typedef struct AVFrame AVFrame;
struct frames {
AVFrame *decoding_frame;
AVFrame *rendering_frame;
SDL_mutex *mutex;
#ifndef SKIP_FRAMES
SDL_bool stopped;
SDL_cond *rendering_frame_consumed_cond;
#endif
SDL_bool rendering_frame_consumed;
struct fps_counter fps_counter;
};
SDL_bool frames_init(struct frames *frames);
void frames_destroy(struct frames *frames);
// set the decoder frame as ready for rendering
// this function locks frames->mutex during its execution
// returns true if the previous frame had been consumed
SDL_bool frames_offer_decoded_frame(struct frames *frames);
// mark the rendering frame as consumed and return it
// MUST be called with frames->mutex locked!!!
// the caller is expected to render the returned frame to some texture before
// unlocking frames->mutex
const AVFrame *frames_consume_rendered_frame(struct frames *frames);
// wake up and avoid any blocking call
void frames_stop(struct frames *frames);
#endif

16
app/src/hidpi.c Normal file
View File

@@ -0,0 +1,16 @@
#include "hidpi.h"
void hidpi_get_scale(struct screen *screen, struct hidpi_scale *scale) {
SDL_GL_GetDrawableSize(screen->window, &scale->horizontal.num, &scale->vertical.num);
SDL_GetWindowSize(screen->window, &scale->horizontal.div, &scale->vertical.div);
}
void hidpi_unscale_coordinates(struct hidpi_scale *scale, Sint32 *x, Sint32 *y) {
// to unscale, we devide by the ratio (so num and div are reversed)
if (scale->horizontal.num) {
*x = ((Sint64) *x) * scale->horizontal.div / scale->horizontal.num;
}
if (scale->vertical.num) {
*y = ((Sint64) *y) * scale->vertical.div / scale->vertical.num;
}
}

24
app/src/hidpi.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef HIDPI_H
#define HIDPI_H
#include "common.h"
#include "screen.h"
// rational number p/q
struct rational {
int num;
int div;
};
struct hidpi_scale {
struct rational horizontal; // drawable.width / window.width
struct rational vertical; // drawable.height / window.height
};
void hidpi_get_scale(struct screen *screen, struct hidpi_scale *hidpi_scale);
// mouse location need to be "unscaled" if hidpi is enabled
// <https://nlguillemot.wordpress.com/2016/12/11/high-dpi-rendering/>
void hidpi_unscale_coordinates(struct hidpi_scale *hidpi_scale, Sint32 *x, Sint32 *y);
#endif

View File

@@ -1,393 +0,0 @@
#include "input_manager.h"
#include <SDL2/SDL_assert.h>
#include "convert.h"
#include "lock_util.h"
#include "log.h"
// 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);
return (struct point) {
.x = x,
.y = y,
};
}
static const int ACTION_DOWN = 1;
static const int ACTION_UP = 1 << 1;
static void
send_keycode(struct controller *controller, enum android_keycode keycode,
int actions, const char *name) {
// send DOWN event
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0;
if (actions & ACTION_DOWN) {
control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
return;
}
}
if (actions & ACTION_UP) {
control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (UP)", name);
}
}
}
static inline void
action_home(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_HOME, actions, "HOME");
}
static inline void
action_back(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_BACK, actions, "BACK");
}
static inline void
action_app_switch(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH");
}
static inline void
action_power(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_POWER, actions, "POWER");
}
static inline void
action_volume_up(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP");
}
static inline void
action_volume_down(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN");
}
static inline void
action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
}
// turn the screen on if it was off, press BACK otherwise
static void
press_back_or_turn_screen_on(struct controller *controller) {
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action =
CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on");
}
}
static void
expand_notification_panel(struct controller *controller) {
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action =
CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot expand notification panel");
}
}
static void
collapse_notification_panel(struct controller *controller) {
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action =
CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot collapse notification panel");
}
}
static void
switch_fps_counter_state(struct video_buffer *vb) {
mutex_lock(vb->mutex);
if (vb->fps_counter.started) {
LOGI("FPS counter stopped");
fps_counter_stop(&vb->fps_counter);
} else {
LOGI("FPS counter started");
fps_counter_start(&vb->fps_counter);
}
mutex_unlock(vb->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) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
SDL_assert(event->text[1] == '\0');
// letters and space are handled as raw key event
return;
}
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
control_event.text_event.text = SDL_strdup(event->text);
if (!control_event.text_event.text) {
LOGW("Cannot strdup input text");
return;
}
if (!controller_push_event(input_manager->controller, &control_event)) {
SDL_free(control_event.text_event.text);
LOGW("Cannot send text event");
}
}
void
input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event,
bool control) {
bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
if (alt) {
// no shortcut involves Alt or Meta, and they should not be forwarded
// to the device
return;
}
// capture all Ctrl events
if (ctrl | meta) {
SDL_Keycode keycode = event->keysym.sym;
int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) {
case SDLK_h:
if (control && ctrl && !meta && !shift && !repeat) {
action_home(input_manager->controller, action);
}
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
if (control && ctrl && !meta && !shift && !repeat) {
action_back(input_manager->controller, action);
}
return;
case SDLK_s:
if (control && ctrl && !meta && !shift && !repeat) {
action_app_switch(input_manager->controller, action);
}
return;
case SDLK_m:
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(input_manager->controller, action);
}
return;
case SDLK_p:
if (control && ctrl && !meta && !shift && !repeat) {
action_power(input_manager->controller, action);
}
return;
case SDLK_DOWN:
#ifdef __APPLE__
if (control && !ctrl && meta && !shift) {
#else
if (control && ctrl && !meta && !shift) {
#endif
// forward repeated events
action_volume_down(input_manager->controller, action);
}
return;
case SDLK_UP:
#ifdef __APPLE__
if (control && !ctrl && meta && !shift) {
#else
if (control && ctrl && !meta && !shift) {
#endif
// forward repeated events
action_volume_up(input_manager->controller, action);
}
return;
case SDLK_v:
if (control && ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
clipboard_paste(input_manager->controller);
}
return;
case SDLK_f:
if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
screen_switch_fullscreen(input_manager->screen);
}
return;
case SDLK_x:
if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
screen_resize_to_fit(input_manager->screen);
}
return;
case SDLK_g:
if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
screen_resize_to_pixel_perfect(input_manager->screen);
}
return;
case SDLK_i:
if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
switch_fps_counter_state(input_manager->video_buffer);
}
return;
case SDLK_n:
if (control && ctrl && !meta
&& !repeat && event->type == SDL_KEYDOWN) {
if (shift) {
collapse_notification_panel(input_manager->controller);
} else {
expand_notification_panel(input_manager->controller);
}
}
return;
}
return;
}
if (!control) {
return;
}
struct control_event control_event;
if (input_key_from_sdl_to_android(event, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send control event");
}
}
}
void
input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
// do not send motion events when no button is pressed
return;
}
struct control_event control_event;
if (mouse_motion_from_sdl_to_android(event,
input_manager->screen->frame_size,
&control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse motion event");
}
}
}
static bool
is_outside_device_screen(struct input_manager *input_manager, int x, int y)
{
return x < 0 || x >= input_manager->screen->frame_size.width ||
y < 0 || y >= input_manager->screen->frame_size.height;
}
void
input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event,
bool control) {
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
return;
}
if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside = is_outside_device_screen(input_manager,
event->x,
event->y);
if (outside) {
screen_resize_to_fit(input_manager->screen);
return;
}
}
// otherwise, send the click event to the device
}
if (!control) {
return;
}
struct control_event control_event;
if (mouse_button_from_sdl_to_android(event,
input_manager->screen->frame_size,
&control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse button event");
}
}
}
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(input_manager->screen),
};
struct control_event control_event;
if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse wheel event");
}
}
}

View File

@@ -1,40 +0,0 @@
#ifndef INPUTMANAGER_H
#define INPUTMANAGER_H
#include <stdbool.h>
#include "common.h"
#include "controller.h"
#include "fps_counter.h"
#include "video_buffer.h"
#include "screen.h"
struct input_manager {
struct controller *controller;
struct video_buffer *video_buffer;
struct screen *screen;
};
void
input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event);
void
input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event,
bool control);
void
input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event);
void
input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event,
bool control);
void
input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event);
#endif

247
app/src/inputmanager.c Normal file
View File

@@ -0,0 +1,247 @@
#include "inputmanager.h"
#include "convert.h"
#include "hidpi.h"
#include "lockutil.h"
#include "log.h"
// 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 mx;
int my;
SDL_GetMouseState(&mx, &my);
convert_to_renderer_coordinates(screen->renderer, &mx, &my);
struct hidpi_scale hidpi_scale;
hidpi_get_scale(screen, &hidpi_scale);
// SDL sometimes uses "int", sometimes "Sint32"
Sint32 x = mx;
Sint32 y = my;
hidpi_unscale_coordinates(&hidpi_scale, &x, &y);
SDL_assert_release(x >= 0 && x < 0x10000 && y >= 0 && y < 0x10000);
return (struct point) {
.x = (Uint16) x,
.y = (Uint16) y,
};
}
static SDL_bool is_ctrl_down(void) {
const Uint8 *state = SDL_GetKeyboardState(NULL);
return state[SDL_SCANCODE_LCTRL] || state[SDL_SCANCODE_RCTRL];
}
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,
},
};
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
return;
}
// send UP event
control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (UP)", name);
}
}
static inline void action_home(struct controller *controller) {
send_keycode(controller, AKEYCODE_HOME, "HOME");
}
static inline void action_back(struct controller *controller) {
send_keycode(controller, AKEYCODE_BACK, "BACK");
}
static inline void action_app_switch(struct controller *controller) {
send_keycode(controller, AKEYCODE_APP_SWITCH, "APP_SWITCH");
}
static inline void action_power(struct controller *controller) {
send_keycode(controller, AKEYCODE_POWER, "POWER");
}
static inline void action_volume_up(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_UP, "VOLUME_UP");
}
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,
},
};
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on");
}
}
static void switch_fps_counter_state(struct frames *frames) {
mutex_lock(frames->mutex);
if (frames->fps_counter.started) {
LOGI("FPS counter stopped");
fps_counter_stop(&frames->fps_counter);
} else {
LOGI("FPS counter started");
fps_counter_start(&frames->fps_counter);
}
mutex_unlock(frames->mutex);
}
void input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event) {
if (is_ctrl_down()) {
switch (event->text[0]) {
case '+':
action_volume_up(input_manager->controller);
break;
case '-':
action_volume_down(input_manager->controller);
break;
}
return;
}
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
strncpy(control_event.text_event.text, event->text, TEXT_MAX_LENGTH);
control_event.text_event.text[TEXT_MAX_LENGTH] = '\0';
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send text event");
}
}
void input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event) {
SDL_Keycode keycode = event->keysym.sym;
SDL_bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
SDL_bool repeat = event->repeat;
// capture all Ctrl events
if (ctrl) {
// only consider keydown events, and ignore repeated events
if (repeat || event->type != SDL_KEYDOWN) {
return;
}
if (shift) {
// currently, there is no shortcut implying SHIFT
return;
}
switch (keycode) {
case SDLK_h:
action_home(input_manager->controller);
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
action_back(input_manager->controller);
return;
case SDLK_m:
action_app_switch(input_manager->controller);
return;
case SDLK_p:
action_power(input_manager->controller);
return;
case SDLK_f:
screen_switch_fullscreen(input_manager->screen);
return;
case SDLK_x:
screen_resize_to_fit(input_manager->screen);
return;
case SDLK_g:
screen_resize_to_pixel_perfect(input_manager->screen);
return;
case SDLK_i:
switch_fps_counter_state(input_manager->frames);
return;
}
return;
}
struct control_event control_event;
if (input_key_from_sdl_to_android(event, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send control event");
}
}
}
void input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
// do not send motion events when no button is pressed
return;
}
struct hidpi_scale hidpi_scale;
hidpi_get_scale(input_manager->screen, &hidpi_scale);
struct control_event control_event;
if (mouse_motion_from_sdl_to_android(event, input_manager->screen->frame_size, &hidpi_scale, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse motion event");
}
}
}
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;
};
struct hidpi_scale hidpi_scale;
hidpi_get_scale(input_manager->screen, &hidpi_scale);
struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &hidpi_scale, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse button event");
}
}
}
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(input_manager->screen),
};
struct control_event control_event;
if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send wheel button event");
}
}
}

27
app/src/inputmanager.h Normal file
View File

@@ -0,0 +1,27 @@
#ifndef INPUTMANAGER_H
#define INPUTMANAGER_H
#include "common.h"
#include "controller.h"
#include "fpscounter.h"
#include "frames.h"
#include "screen.h"
struct input_manager {
struct controller *controller;
struct frames *frames;
struct screen *screen;
};
void input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event);
void input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event);
void input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event);
void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event);
void input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event);
#endif

View File

@@ -1,20 +0,0 @@
#ifndef LOCKUTIL_H
#define LOCKUTIL_H
// forward declarations
typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
void
mutex_lock(SDL_mutex *mutex);
void
mutex_unlock(SDL_mutex *mutex);
void
cond_wait(SDL_cond *cond, SDL_mutex *mutex);
void
cond_signal(SDL_cond *cond);
#endif

View File

@@ -1,35 +1,30 @@
#include <lock_util.h>
#include <stdlib.h> #include <stdlib.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include "log.h" #include "log.h"
void void mutex_lock(SDL_mutex *mutex) {
mutex_lock(SDL_mutex *mutex) {
if (SDL_LockMutex(mutex)) { if (SDL_LockMutex(mutex)) {
LOGC("Could not lock mutex"); LOGC("Could not lock mutex");
abort(); abort();
} }
} }
void void mutex_unlock(SDL_mutex *mutex) {
mutex_unlock(SDL_mutex *mutex) {
if (SDL_UnlockMutex(mutex)) { if (SDL_UnlockMutex(mutex)) {
LOGC("Could not unlock mutex"); LOGC("Could not unlock mutex");
abort(); abort();
} }
} }
void void cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
if (SDL_CondWait(cond, mutex)) { if (SDL_CondWait(cond, mutex)) {
LOGC("Could not wait on condition"); LOGC("Could not wait on condition");
abort(); abort();
} }
} }
void void cond_signal(SDL_cond *cond) {
cond_signal(SDL_cond *cond) {
if (SDL_CondSignal(cond)) { if (SDL_CondSignal(cond)) {
LOGC("Could not signal a condition"); LOGC("Could not signal a condition");
abort(); abort();

13
app/src/lockutil.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef LOCKUTIL_H
#define LOCKUTIL_H
// forward declarations
typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
void mutex_lock(SDL_mutex *mutex);
void mutex_unlock(SDL_mutex *mutex);
void cond_wait(SDL_cond *cond, SDL_mutex *mutex);
void cond_signal(SDL_cond *cond);
#endif

View File

@@ -3,7 +3,6 @@
#include <SDL2/SDL_log.h> #include <SDL2/SDL_log.h>
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)

View File

@@ -1,32 +1,20 @@
#include "scrcpy.h" #include "scrcpy.h"
#include <getopt.h> #include <getopt.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "compat.h"
#include "config.h" #include "config.h"
#include "log.h" #include "log.h"
#include "recorder.h"
struct args { struct args {
const char *serial; const char *serial;
const char *crop; SDL_bool help;
const char *record_filename; SDL_bool version;
enum recorder_format record_format; Uint16 port;
bool fullscreen; Uint16 max_size;
bool no_control; Uint32 bit_rate;
bool no_display;
bool help;
bool version;
bool show_touches;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool always_on_top;
}; };
static void usage(const char *arg0) { static void usage(const char *arg0) {
@@ -40,18 +28,6 @@ static void usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" -c, --crop width:height:x:y\n"
" Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n"
"\n"
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
" -F, --record-format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" -h, --help\n" " -h, --help\n"
" Print this help.\n" " Print this help.\n"
"\n" "\n"
@@ -61,33 +37,14 @@ static void usage(const char *arg0) {
" is preserved.\n" " is preserved.\n"
" Default is %d%s.\n" " Default is %d%s.\n"
"\n" "\n"
" -n, --no-control\n"
" Disable device control (mirror the device in read-only).\n"
"\n"
" -N, --no-display\n"
" Do not display device (only when screen recording is\n"
" enabled).\n"
"\n"
" -p, --port port\n" " -p, --port port\n"
" Set the TCP port the client listens on.\n" " Set the TCP port the client listens on.\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" -r, --record file.mp4\n"
" Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n"
"\n"
" -s, --serial\n" " -s, --serial\n"
" The device serial number. Mandatory only if several devices\n" " The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n" " are connected to adb.\n"
"\n" "\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -T, --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -v, --version\n" " -v, --version\n"
" Print the version of scrcpy.\n" " Print the version of scrcpy.\n"
"\n" "\n"
@@ -100,51 +57,33 @@ static void usage(const char *arg0) {
" resize window to 1:1 (pixel-perfect)\n" " resize window to 1:1 (pixel-perfect)\n"
"\n" "\n"
" Ctrl+x\n" " Ctrl+x\n"
" Double-click on black borders\n"
" resize window to remove black borders\n" " resize window to remove black borders\n"
"\n" "\n"
" Ctrl+h\n" " Ctrl+h\n"
" Home\n" " Home\n"
" Middle-click\n"
" click on HOME\n" " click on HOME\n"
"\n" "\n"
" Ctrl+b\n" " Ctrl+b\n"
" Ctrl+Backspace\n" " Ctrl+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n" " click on BACK\n"
"\n" "\n"
" Ctrl+s\n" " Ctrl+m\n"
" click on APP_SWITCH\n" " click on APP_SWITCH\n"
"\n" "\n"
" Ctrl+m\n" " Ctrl+'+'\n"
" click on MENU\n"
"\n"
" Ctrl+Up\n"
" click on VOLUME_UP\n" " click on VOLUME_UP\n"
"\n" "\n"
" Ctrl+Down\n" " Ctrl+'-'\n"
" click on VOLUME_DOWN\n" " click on VOLUME_DOWN\n"
"\n" "\n"
" Ctrl+p\n" " Ctrl+p\n"
" click on POWER (turn screen on/off)\n" " click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click (when screen is off)\n" " Right-click\n"
" turn screen on\n" " turn screen on\n"
"\n" "\n"
" Ctrl+n\n"
" expand notification panel\n"
"\n"
" Ctrl+Shift+n\n"
" collapse notification panel\n"
"\n"
" Ctrl+v\n"
" paste computer clipboard to device\n"
"\n"
" Ctrl+i\n" " Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n" " enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
" install APK from computer\n"
"\n", "\n",
arg0, arg0,
DEFAULT_BIT_RATE, DEFAULT_BIT_RATE,
@@ -152,37 +91,28 @@ static void usage(const char *arg0) {
DEFAULT_LOCAL_PORT); DEFAULT_LOCAL_PORT);
} }
static void static void print_version(void) {
print_version(void) { fprintf(stderr, "scrcpy v%s\n\n", SCRCPY_VERSION);
fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION);
fprintf(stderr, "dependencies:\n"); fprintf(stderr, "dependencies:\n");
fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL);
SDL_PATCHLEVEL); fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, LIBAVCODEC_VERSION_MICRO);
fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO);
LIBAVCODEC_VERSION_MINOR, fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO);
LIBAVCODEC_VERSION_MICRO);
fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR,
LIBAVFORMAT_VERSION_MINOR,
LIBAVFORMAT_VERSION_MICRO);
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
LIBAVUTIL_VERSION_MINOR,
LIBAVUTIL_VERSION_MICRO);
} }
static bool static SDL_bool parse_bit_rate(char *optarg, Uint32 *bit_rate) {
parse_bit_rate(char *optarg, uint32_t *bit_rate) {
char *endptr; char *endptr;
if (*optarg == '\0') { if (*optarg == '\0') {
LOGE("Bit-rate parameter is empty"); LOGE("Bit-rate parameter is empty");
return false; return SDL_FALSE;
} }
long value = strtol(optarg, &endptr, 0); long value = strtol(optarg, &endptr, 0);
int mul = 1; int mul = 1;
if (*endptr != '\0') { if (*endptr != '\0') {
if (optarg == endptr) { if (optarg == endptr) {
LOGE("Invalid bit-rate: %s", optarg); LOGE("Invalid bit-rate: %s", optarg);
return false; return SDL_FALSE;
} }
if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') {
mul = 1000000; mul = 1000000;
@@ -190,224 +120,123 @@ parse_bit_rate(char *optarg, uint32_t *bit_rate) {
mul = 1000; mul = 1000;
} else { } else {
LOGE("Invalid bit-rate unit: %s", optarg); LOGE("Invalid bit-rate unit: %s", optarg);
return false; return SDL_FALSE;
} }
} }
if (value < 0 || ((uint32_t) -1) / mul < value) { if (value < 0 || ((Uint32) -1) / mul < value) {
LOGE("Bitrate must be positive and less than 2^32: %s", optarg); LOGE("Bitrate must be positive and less than 2^32: %s", optarg);
return false; return SDL_FALSE;
} }
*bit_rate = (uint32_t) value * mul; *bit_rate = (Uint32) value * mul;
return true; return SDL_TRUE;
} }
static bool static SDL_bool parse_max_size(char *optarg, Uint16 *max_size) {
parse_max_size(char *optarg, uint16_t *max_size) {
char *endptr; char *endptr;
if (*optarg == '\0') { if (*optarg == '\0') {
LOGE("Max size parameter is empty"); LOGE("Max size parameter is empty");
return false; return SDL_FALSE;
} }
long value = strtol(optarg, &endptr, 0); long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') { if (*endptr != '\0') {
LOGE("Invalid max size: %s", optarg); LOGE("Invalid max size: %s", optarg);
return false; return SDL_FALSE;
} }
if (value & ~0xffff) { if (value & ~0xffff) {
LOGE("Max size must be between 0 and 65535: %ld", value); LOGE("Max size must be between 0 and 65535: %ld", value);
return false; return SDL_FALSE;
} }
*max_size = (uint16_t) value; *max_size = (Uint16) value;
return true; return SDL_TRUE;
} }
static bool static SDL_bool parse_port(char *optarg, Uint16 *port) {
parse_port(char *optarg, uint16_t *port) {
char *endptr; char *endptr;
if (*optarg == '\0') { if (*optarg == '\0') {
LOGE("Invalid port parameter is empty"); LOGE("Invalid port parameter is empty");
return false; return SDL_FALSE;
} }
long value = strtol(optarg, &endptr, 0); long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') { if (*endptr != '\0') {
LOGE("Invalid port: %s", optarg); LOGE("Invalid port: %s", optarg);
return false; return SDL_FALSE;
} }
if (value & ~0xffff) { if (value & ~0xffff) {
LOGE("Port out of range: %ld", value); LOGE("Port out of range: %ld", value);
return false; return SDL_FALSE;
} }
*port = (uint16_t) value; *port = (Uint16) value;
return true; return SDL_TRUE;
} }
static bool static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) {
*format = RECORDER_FORMAT_MP4;
return true;
}
if (!strcmp(optarg, "mkv")) {
*format = RECORDER_FORMAT_MKV;
return true;
}
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
}
static enum recorder_format
guess_record_format(const char *filename) {
size_t len = strlen(filename);
if (len < 4) {
return 0;
}
const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) {
return RECORDER_FORMAT_MP4;
}
if (!strcmp(ext, ".mkv")) {
return RECORDER_FORMAT_MKV;
}
return 0;
}
static bool
parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, 'T'}, {"bit-rate", required_argument, NULL, 'b'},
{"bit-rate", required_argument, NULL, 'b'}, {"help", no_argument, NULL, 'h'},
{"crop", required_argument, NULL, 'c'}, {"max-size", required_argument, NULL, 'm'},
{"fullscreen", no_argument, NULL, 'f'}, {"port", required_argument, NULL, 'p'},
{"help", no_argument, NULL, 'h'}, {"serial", required_argument, NULL, 's'},
{"max-size", required_argument, NULL, 'm'}, {"version", no_argument, NULL, 'v'},
{"no-control", no_argument, NULL, 'n'}, {NULL, 0, NULL, 0 },
{"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
}; };
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options, while ((c = getopt_long(argc, argv, "b:hm:p:s:v", long_options, NULL)) != -1) {
NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b': {
if (!parse_bit_rate(optarg, &args->bit_rate)) { if (!parse_bit_rate(optarg, &args->bit_rate)) {
return false; return SDL_FALSE;
} }
break; break;
case 'c': }
args->crop = optarg; case 'h': {
args->help = SDL_TRUE;
break; break;
case 'f': }
args->fullscreen = true; case 'm': {
break;
case 'F':
if (!parse_record_format(optarg, &args->record_format)) {
return false;
}
break;
case 'h':
args->help = true;
break;
case 'm':
if (!parse_max_size(optarg, &args->max_size)) { if (!parse_max_size(optarg, &args->max_size)) {
return false; return SDL_FALSE;
} }
break; break;
case 'n': }
args->no_control = true; case 'p': {
break;
case 'N':
args->no_display = true;
break;
case 'p':
if (!parse_port(optarg, &args->port)) { if (!parse_port(optarg, &args->port)) {
return false; return SDL_FALSE;
} }
break; break;
case 'r': }
args->record_filename = optarg; case 's': {
break;
case 's':
args->serial = optarg; args->serial = optarg;
break; break;
case 't': }
args->show_touches = true; case 'v': {
break; args->version = SDL_TRUE;
case 'T':
args->always_on_top = true;
break;
case 'v':
args->version = true;
break; break;
}
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return SDL_FALSE;
} }
} }
if (args->no_display && !args->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
if (args->no_display && args->fullscreen) {
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
return false;
}
int index = optind; int index = optind;
if (index < argc) { if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]); LOGE("Unexpected additional argument: %s", argv[index]);
return false; return SDL_FALSE;
} }
return SDL_TRUE;
if (args->record_format && !args->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (args->record_filename && !args->record_format) {
args->record_format = guess_record_format(args->record_filename);
if (!args->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)",
args->record_filename);
return false;
}
}
return true;
} }
int int main(int argc, char *argv[]) {
main(int argc, char *argv[]) {
#ifdef __WINDOWS__
// disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL);
setbuf(stderr, NULL);
#endif
struct args args = { struct args args = {
.serial = NULL, .serial = NULL,
.crop = NULL, .help = SDL_FALSE,
.record_filename = NULL, .version = SDL_FALSE,
.record_format = 0,
.help = false,
.version = false,
.show_touches = false,
.port = DEFAULT_LOCAL_PORT, .port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE, .max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE, .bit_rate = DEFAULT_BIT_RATE,
.always_on_top = false,
.no_control = false,
.no_display = false,
}; };
if (!parse_args(&args, argc, argv)) { if (!parse_args(&args, argc, argv)) {
return 1; return 1;
@@ -423,9 +252,7 @@ main(int argc, char *argv[]) {
return 0; return 0;
} }
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all(); av_register_all();
#endif
if (avformat_network_init()) { if (avformat_network_init()) {
return 1; return 1;
@@ -435,29 +262,9 @@ main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif #endif
struct scrcpy_options options = { int res = scrcpy(args.serial, args.port, args.max_size, args.bit_rate) ? 0 : 1;
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.record_format = args.record_format,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
.always_on_top = args.always_on_top,
.no_control = args.no_control,
.no_display = args.no_display,
};
int res = scrcpy(&options) ? 0 : 1;
avformat_network_deinit(); // ignore failure avformat_network_deinit(); // ignore failure
#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE)
if (res != 0) {
fprintf(stderr, "Press any key to continue...\n");
getchar();
}
#endif
return res; return res;
} }

View File

@@ -18,29 +18,7 @@
typedef struct in_addr IN_ADDR; typedef struct in_addr IN_ADDR;
#endif #endif
socket_t socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
net_connect(uint32_t addr, uint16_t 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_t addr, uint16_t port, int backlog) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0); socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) { if (sock == INVALID_SOCKET) {
perror("socket"); perror("socket");
@@ -48,8 +26,7 @@ net_listen(uint32_t addr, uint16_t port, int backlog) {
} }
int reuse = 1; int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) {
sizeof(reuse)) == -1) {
perror("setsockopt(SO_REUSEADDR)"); perror("setsockopt(SO_REUSEADDR)");
} }
@@ -71,43 +48,37 @@ net_listen(uint32_t addr, uint16_t port, int backlog) {
return sock; return sock;
} }
socket_t socket_t net_accept(socket_t server_socket) {
net_accept(socket_t server_socket) {
SOCKADDR_IN csin; SOCKADDR_IN csin;
socklen_t sinsize = sizeof(csin); socklen_t sinsize = sizeof(csin);
return accept(server_socket, (SOCKADDR *) &csin, &sinsize); return accept(server_socket, (SOCKADDR *) &csin, &sinsize);
} }
ssize_t ssize_t net_recv(socket_t socket, void *buf, size_t len) {
net_recv(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, 0); return recv(socket, buf, len, 0);
} }
ssize_t ssize_t net_recv_all(socket_t socket, void *buf, size_t len) {
net_recv_all(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, MSG_WAITALL); return recv(socket, buf, len, MSG_WAITALL);
} }
ssize_t ssize_t net_send(socket_t socket, void *buf, size_t len) {
net_send(socket_t socket, const void *buf, size_t len) {
return send(socket, buf, len, 0); return send(socket, buf, len, 0);
} }
ssize_t ssize_t net_send_all(socket_t socket, void *buf, size_t len) {
net_send_all(socket_t socket, const void *buf, size_t len) { ssize_t w;
ssize_t w = 0;
while (len > 0) { while (len > 0) {
w = send(socket, buf, len, 0); w = send(socket, buf, len, 0);
if (w == -1) { if (w == -1) {
return -1; return -1;
} }
len -= w; len -= w;
buf = (char *) buf + w; buf += w;
} }
return w; return w;
} }
bool SDL_bool net_shutdown(socket_t socket, int how) {
net_shutdown(socket_t socket, int how) {
return !shutdown(socket, how); return !shutdown(socket, how);
} }

View File

@@ -1,15 +1,16 @@
#ifndef NET_H #ifndef NET_H
#define NET_H #define NET_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_platform.h> #include <SDL2/SDL_platform.h>
#include <SDL2/SDL_stdinc.h>
#ifdef __WINDOWS__ #ifdef __WINDOWS__
# include <winsock2.h> # include <winsock2.h>
#define SHUT_RD SD_RECEIVE #define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND #define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH #define SHUT_RDWR SD_BOTH
typedef SIZE_T size_t;
typedef SSIZE_T ssize_t;
typedef SOCKET socket_t; typedef SOCKET socket_t;
#else #else
# include <sys/socket.h> # include <sys/socket.h>
@@ -17,39 +18,19 @@
typedef int socket_t; typedef int socket_t;
#endif #endif
bool SDL_bool net_init(void);
net_init(void); void net_cleanup(void);
void socket_t net_listen(Uint32 addr, Uint16 port, int backlog);
net_cleanup(void); socket_t net_accept(socket_t server_socket);
socket_t
net_connect(uint32_t addr, uint16_t port);
socket_t
net_listen(uint32_t addr, uint16_t port, int backlog);
socket_t
net_accept(socket_t server_socket);
// the _all versions wait/retry until len bytes have been written/read // the _all versions wait/retry until len bytes have been written/read
ssize_t ssize_t net_recv(socket_t socket, void *buf, size_t len);
net_recv(socket_t socket, void *buf, size_t len); ssize_t net_recv_all(socket_t socket, void *buf, size_t len);
ssize_t net_send(socket_t socket, void *buf, size_t len);
ssize_t ssize_t net_send_all(socket_t socket, void *buf, size_t len);
net_recv_all(socket_t socket, void *buf, size_t len);
ssize_t
net_send(socket_t socket, const void *buf, size_t len);
ssize_t
net_send_all(socket_t socket, const void *buf, size_t len);
// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) // how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
bool SDL_bool net_shutdown(socket_t socket, int how);
net_shutdown(socket_t socket, int how); SDL_bool net_close(socket_t socket);
bool
net_close(socket_t socket);
#endif #endif

View File

@@ -1,181 +0,0 @@
#include "recorder.h"
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include "compat.h"
#include "config.h"
#include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat *
find_muxer(const char *name) {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
void *opaque = NULL;
#endif
const AVOutputFormat *oformat = NULL;
do {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
oformat = av_muxer_iterate(&opaque);
#else
oformat = av_oformat_next(oformat);
#endif
// until null or with name "mp4"
} while (oformat && strcmp(oformat->name, name));
return oformat;
}
bool
recorder_init(struct recorder *recorder,
const char *filename,
enum recorder_format format,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Cannot strdup filename");
return false;
}
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
return true;
}
void
recorder_destroy(struct recorder *recorder) {
SDL_free(recorder->filename);
}
static const char *
recorder_get_format_name(enum recorder_format format) {
switch (format) {
case RECORDER_FORMAT_MP4: return "mp4";
case RECORDER_FORMAT_MKV: return "matroska";
default: return NULL;
}
}
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
const char *format_name = recorder_get_format_name(recorder->format);
SDL_assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
return false;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOGE("Could not allocate output context");
return false;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
// still expects a pointer-to-non-const (it has not be updated accordingly)
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
recorder->ctx->oformat = (AVOutputFormat *) format;
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
avformat_free_context(recorder->ctx);
return false;
}
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = recorder->declared_frame_size.width;
ostream->codecpar->height = recorder->declared_frame_size.height;
#else
ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codec->codec_id = input_codec->id;
ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
ostream->codec->width = recorder->declared_frame_size.width;
ostream->codec->height = recorder->declared_frame_size.height;
#endif
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
avformat_free_context(recorder->ctx);
return false;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
}
void
recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
}
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
static bool
recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Cannot allocate extradata");
return false;
}
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
#else
ostream->codec->extradata = extradata;
ostream->codec->extradata_size = packet->size;
#endif
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
SDL_free(extradata);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return false;
}
return true;
}
static void
recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
bool
recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
bool ok = recorder_write_header(recorder, packet);
if (!ok) {
return false;
}
recorder->header_written = true;
}
recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
}

View File

@@ -1,38 +0,0 @@
#ifndef RECORDER_H
#define RECORDER_H
#include <stdbool.h>
#include <libavformat/avformat.h>
#include "common.h"
enum recorder_format {
RECORDER_FORMAT_MP4 = 1,
RECORDER_FORMAT_MKV,
};
struct recorder {
char *filename;
enum recorder_format format;
AVFormatContext *ctx;
struct size declared_frame_size;
bool header_written;
};
bool
recorder_init(struct recorder *recoder, const char *filename,
enum recorder_format format, struct size declared_frame_size);
void
recorder_destroy(struct recorder *recorder);
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec);
void
recorder_close(struct recorder *recorder);
bool
recorder_write(struct recorder *recorder, AVPacket *packet);
#endif

View File

@@ -13,411 +13,163 @@
#include "decoder.h" #include "decoder.h"
#include "device.h" #include "device.h"
#include "events.h" #include "events.h"
#include "file_handler.h" #include "frames.h"
#include "fps_counter.h" #include "fpscounter.h"
#include "input_manager.h" #include "inputmanager.h"
#include "log.h" #include "log.h"
#include "lock_util.h" #include "lockutil.h"
#include "net.h" #include "net.h"
#include "recorder.h"
#include "screen.h" #include "screen.h"
#include "server.h" #include "server.h"
#include "stream.h" #include "tinyxpm.h"
#include "tiny_xpm.h"
#include "video_buffer.h"
static struct server server = SERVER_INITIALIZER; static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER;
static struct video_buffer video_buffer; static struct frames frames;
static struct stream stream;
static struct decoder decoder; static struct decoder decoder;
static struct recorder recorder;
static struct controller controller; static struct controller controller;
static struct file_handler file_handler;
static struct input_manager input_manager = { static struct input_manager input_manager = {
.controller = &controller, .controller = &controller,
.video_buffer = &video_buffer, .frames = &frames,
.screen = &screen, .screen = &screen,
}; };
// init SDL and set appropriate hints static void event_loop(void) {
static bool SDL_Event event;
sdl_init_and_configure(bool display) { while (SDL_WaitEvent(&event)) {
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; switch (event.type) {
if (SDL_Init(flags)) { case EVENT_DECODER_STOPPED:
LOGC("Could not initialize SDL: %s", SDL_GetError()); LOGD("Video decoder stopped");
return false; return;
} case SDL_QUIT:
LOGD("User requested to quit");
atexit(SDL_Quit); return;
case EVENT_NEW_FRAME:
if (!display) { if (!screen.has_frame) {
return true; screen.has_frame = SDL_TRUE;
} // this is the very first frame, show the window
screen_show_window(&screen);
// Use the best available scale quality }
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { if (!screen_update_frame(&screen, &frames)) {
LOGW("Could not enable bilinear filtering"); return;
} }
break;
#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH case SDL_WINDOWEVENT:
// Handle a click to gain focus as any other click switch (event.window.event) {
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
LOGW("Could not enable mouse focus clickthrough");
}
#endif
// Do not disable the screensaver when scrcpy is running
SDL_EnableScreenSaver();
return true;
}
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
#ifdef CONTINUOUS_RESIZING_WORKAROUND
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
// not triggered. As a workaround, handle them in an event handler.
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static int
event_watcher(void *data, SDL_Event *event) {
if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
// called from another thread, not very safe, but it's a workaround!
screen_render(&screen);
}
return 0;
}
#endif
static bool
is_apk(const char *file) {
const char *ext = strrchr(file, '.');
return ext && !strcmp(ext, ".apk");
}
enum event_result {
EVENT_RESULT_CONTINUE,
EVENT_RESULT_STOPPED_BY_USER,
EVENT_RESULT_STOPPED_BY_EOS,
};
static enum event_result
handle_event(SDL_Event *event, bool control) {
switch (event->type) {
case EVENT_STREAM_STOPPED:
LOGD("Video stream stopped");
return EVENT_RESULT_STOPPED_BY_EOS;
case SDL_QUIT:
LOGD("User requested to quit");
return EVENT_RESULT_STOPPED_BY_USER;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = true;
// this is the very first frame, show the window
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &video_buffer)) {
return false;
}
break;
case SDL_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED: case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen); screen_render(&screen);
break; break;
} }
break; break;
case SDL_TEXTINPUT: case SDL_TEXTINPUT: {
if (!control) { input_manager_process_text_input(&input_manager, &event.text);
break; break;
} }
input_manager_process_text_input(&input_manager, &event->text); case SDL_KEYDOWN:
break; case SDL_KEYUP:
case SDL_KEYDOWN: input_manager_process_key(&input_manager, &event.key);
case SDL_KEYUP: break;
// some key events do not interact with the device, so process the case SDL_MOUSEMOTION:
// event even if control is disabled input_manager_process_mouse_motion(&input_manager, &event.motion);
input_manager_process_key(&input_manager, &event->key, control); break;
break; case SDL_MOUSEWHEEL: {
case SDL_MOUSEMOTION: input_manager_process_mouse_wheel(&input_manager, &event.wheel);
if (!control) {
break; break;
} }
input_manager_process_mouse_motion(&input_manager, &event->motion); case SDL_MOUSEBUTTONDOWN:
break; case SDL_MOUSEBUTTONUP: {
case SDL_MOUSEWHEEL: input_manager_process_mouse_button(&input_manager, &event.button);
if (!control) {
break; break;
} }
input_manager_process_mouse_wheel(&input_manager, &event->wheel);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process
// the event even if control is disabled
input_manager_process_mouse_button(&input_manager, &event->button,
control);
break;
case SDL_DROPFILE: {
if (!control) {
break;
}
file_handler_action_t action;
if (is_apk(event->drop.file)) {
action = ACTION_INSTALL_APK;
} else {
action = ACTION_PUSH_FILE;
}
file_handler_request(&file_handler, action, event->drop.file);
break;
} }
} }
return EVENT_RESULT_CONTINUE;
} }
static bool SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate) {
event_loop(bool display, bool control) { if (!server_start(&server, serial, local_port, max_size, bit_rate)) {
#ifdef CONTINUOUS_RESIZING_WORKAROUND return SDL_FALSE;
if (display) {
SDL_AddEventWatch(event_watcher, NULL);
}
#endif
SDL_Event event;
while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, control);
switch (result) {
case EVENT_RESULT_STOPPED_BY_USER:
return true;
case EVENT_RESULT_STOPPED_BY_EOS:
return false;
case EVENT_RESULT_CONTINUE:
break;
}
}
return false;
}
static process_t
set_show_touches_enabled(const char *serial, bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void
wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
return SDL_LOG_PRIORITY_CRITICAL;
case AV_LOG_ERROR:
return SDL_LOG_PRIORITY_ERROR;
case AV_LOG_WARNING:
return SDL_LOG_PRIORITY_WARN;
case AV_LOG_INFO:
return SDL_LOG_PRIORITY_INFO;
}
// do not forward others, which are too verbose
return 0;
}
static void
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) {
return;
}
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
if (!local_fmt) {
LOGC("Cannot allocate string");
return;
}
// strcpy is safe here, the destination is large enough
strcpy(local_fmt, "[FFmpeg] ");
strcpy(local_fmt + 9, fmt);
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
SDL_free(local_fmt);
}
bool
scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
if (!server_start(&server, options->serial, options->port,
options->max_size, options->bit_rate, options->crop,
record)) {
return false;
} }
process_t proc_show_touches = PROCESS_NONE; SDL_bool ret = SDL_TRUE;
bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, true);
show_touches_waited = false;
}
bool ret = true; if (!sdl_init_and_configure()) {
ret = SDL_FALSE;
bool display = !options->no_display;
bool control = !options->no_control;
if (!sdl_init_and_configure(display)) {
ret = false;
goto finally_destroy_server; goto finally_destroy_server;
} }
socket_t device_socket = server_connect_to(&server); // SDL initialization replace the signal handler for SIGTERM, so Ctrl+C is
// managed by the event loop. This blocking call blocks the event loop, so
// timeout the connection not to block indefinitely in case of SIGTERM.
#define SERVER_CONNECT_TIMEOUT_MS 2000
socket_t device_socket = server_connect_to(&server, serial, SERVER_CONNECT_TIMEOUT_MS);
if (device_socket == INVALID_SOCKET) { if (device_socket == INVALID_SOCKET) {
server_stop(&server); server_stop(&server, serial);
ret = false; ret = SDL_FALSE;
goto finally_destroy_server; goto finally_destroy_server;
} }
char device_name[DEVICE_NAME_FIELD_LENGTH]; char device_name[DEVICE_NAME_FIELD_LENGTH];
struct size frame_size; struct size frame_size;
// screenrecord does not send frames when the screen content does not // screenrecord does not send frames when the screen content does not change
// change therefore, we transmit the screen size before the video stream, // therefore, we transmit the screen size before the video stream, to be able
// to be able to init the window immediately // to init the window immediately
if (!device_read_info(device_socket, device_name, &frame_size)) { if (!device_read_info(device_socket, device_name, &frame_size)) {
server_stop(&server); server_stop(&server, serial);
ret = false; ret = SDL_FALSE;
goto finally_destroy_server; goto finally_destroy_server;
} }
struct decoder *dec = NULL; if (!frames_init(&frames)) {
if (display) { server_stop(&server, serial);
if (!video_buffer_init(&video_buffer)) { ret = SDL_FALSE;
server_stop(&server); goto finally_destroy_server;
ret = false;
goto finally_destroy_server;
}
if (control && !file_handler_init(&file_handler, server.serial)) {
ret = false;
server_stop(&server);
goto finally_destroy_video_buffer;
}
decoder_init(&decoder, &video_buffer);
dec = &decoder;
} }
struct recorder *rec = NULL; decoder_init(&decoder, &frames, device_socket);
if (record) {
if (!recorder_init(&recorder,
options->record_filename,
options->record_format,
frame_size)) {
ret = false;
server_stop(&server);
goto finally_destroy_file_handler;
}
rec = &recorder;
}
av_log_set_callback(av_log_callback);
stream_init(&stream, device_socket, dec, rec);
// now we consumed the header values, the socket receives the video stream // now we consumed the header values, the socket receives the video stream
// start the stream // start the decoder
if (!stream_start(&stream)) { if (!decoder_start(&decoder)) {
ret = false; ret = SDL_FALSE;
server_stop(&server); server_stop(&server, serial);
goto finally_destroy_recorder; goto finally_destroy_frames;
} }
if (display) { if (!controller_init(&controller, device_socket)) {
if (control) { ret = SDL_FALSE;
if (!controller_init(&controller, device_socket)) { goto finally_stop_decoder;
ret = false;
goto finally_stop_stream;
}
if (!controller_start(&controller)) {
ret = false;
goto finally_destroy_controller;
}
}
if (!screen_init_rendering(&screen, device_name, frame_size,
options->always_on_top)) {
ret = false;
goto finally_stop_and_join_controller;
}
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
} }
if (options->show_touches) { if (!controller_start(&controller)) {
wait_show_touches(proc_show_touches); ret = SDL_FALSE;
show_touches_waited = true; goto finally_destroy_controller;
} }
ret = event_loop(display, control); if (!screen_init_rendering(&screen, device_name, frame_size)) {
ret = SDL_FALSE;
goto finally_stop_and_join_controller;
}
event_loop();
LOGD("quit..."); LOGD("quit...");
screen_destroy(&screen); screen_destroy(&screen);
finally_stop_and_join_controller: finally_stop_and_join_controller:
if (display && control) { controller_stop(&controller);
controller_stop(&controller); controller_join(&controller);
controller_join(&controller);
}
finally_destroy_controller: finally_destroy_controller:
if (display && control) { controller_destroy(&controller);
controller_destroy(&controller); finally_stop_decoder:
} decoder_stop(&decoder);
finally_stop_stream: // stop the server before decoder_join() to wake up the decoder
stream_stop(&stream); server_stop(&server, serial);
// stop the server before stream_join() to wake up the stream decoder_join(&decoder);
server_stop(&server); finally_destroy_frames:
stream_join(&stream); frames_destroy(&frames);
finally_destroy_recorder:
if (record) {
recorder_destroy(&recorder);
}
finally_destroy_file_handler:
if (display && control) {
file_handler_stop(&file_handler);
file_handler_join(&file_handler);
file_handler_destroy(&file_handler);
}
finally_destroy_video_buffer:
if (display) {
video_buffer_destroy(&video_buffer);
}
finally_destroy_server: finally_destroy_server:
if (options->show_touches) {
if (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial,
false);
wait_show_touches(proc_show_touches);
}
server_destroy(&server); server_destroy(&server);
return ret; return ret;

View File

@@ -1,26 +1,8 @@
#ifndef SCRCPY_H #ifndef SCRCPY_H
#define SCRCPY_H #define SCRCPY_H
#include <stdbool.h> #include <SDL2/SDL_stdinc.h>
#include <stdint.h>
#include <recorder.h>
struct scrcpy_options { SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate);
const char *serial;
const char *crop;
const char *record_filename;
enum recorder_format record_format;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool show_touches;
bool fullscreen;
bool always_on_top;
bool no_control;
bool no_display;
};
bool
scrcpy(const struct scrcpy_options *options);
#endif #endif

View File

@@ -1,21 +1,40 @@
#include "screen.h" #include "screen.h"
#include <string.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <string.h>
#include "common.h"
#include "compat.h"
#include "icon.xpm" #include "icon.xpm"
#include "lock_util.h" #include "lockutil.h"
#include "log.h" #include "log.h"
#include "tiny_xpm.h" #include "tinyxpm.h"
#include "video_buffer.h"
#define DISPLAY_MARGINS 96 #define DISPLAY_MARGINS 96
SDL_bool sdl_init_and_configure(void) {
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGC("Could not initialize SDL: %s", SDL_GetError());
return SDL_FALSE;
}
atexit(SDL_Quit);
// Bilinear resizing
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable bilinear filtering");
}
#if SDL_VERSION_ATLEAST(2, 0, 5)
// Handle a click to gain focus as any other click
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
LOGW("Could not enable mouse focus clickthrough");
}
#endif
return SDL_TRUE;
}
// get the window size in a struct size // get the window size in a struct size
static struct size static struct size get_native_window_size(SDL_Window *window) {
get_native_window_size(SDL_Window *window) {
int width; int width;
int height; int height;
SDL_GetWindowSize(window, &width, &height); SDL_GetWindowSize(window, &width, &height);
@@ -27,8 +46,7 @@ get_native_window_size(SDL_Window *window) {
} }
// get the windowed window size // get the windowed window size
static struct size static struct size get_window_size(const struct screen *screen) {
get_window_size(const struct screen *screen) {
if (screen->fullscreen) { if (screen->fullscreen) {
return screen->windowed_window_size; return screen->windowed_window_size;
} }
@@ -36,8 +54,7 @@ get_window_size(const struct screen *screen) {
} }
// set the window size to be applied when fullscreen is disabled // set the window size to be applied when fullscreen is disabled
static void static void set_window_size(struct screen *screen, struct size new_size) {
set_window_size(struct screen *screen, struct size new_size) {
// setting the window size during fullscreen is implementation defined, // setting the window size during fullscreen is implementation defined,
// so apply the resize only after fullscreen is disabled // so apply the resize only after fullscreen is disabled
if (screen->fullscreen) { if (screen->fullscreen) {
@@ -49,31 +66,28 @@ set_window_size(struct screen *screen, struct size new_size) {
} }
// get the preferred display bounds (i.e. the screen bounds with some margins) // get the preferred display bounds (i.e. the screen bounds with some margins)
static bool static SDL_bool get_preferred_display_bounds(struct size *bounds) {
get_preferred_display_bounds(struct size *bounds) {
SDL_Rect rect; SDL_Rect rect;
#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS #if SDL_VERSION_ATLEAST(2, 0, 5)
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r)) # define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r))
#else #else
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r)) # define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r))
#endif #endif
if (GET_DISPLAY_BOUNDS(0, &rect)) { if (GET_DISPLAY_BOUNDS(0, &rect)) {
LOGW("Could not get display usable bounds: %s", SDL_GetError()); LOGW("Could not get display usable bounds: %s", SDL_GetError());
return false; return SDL_FALSE;
} }
bounds->width = MAX(0, rect.w - DISPLAY_MARGINS); bounds->width = MAX(0, rect.w - DISPLAY_MARGINS);
bounds->height = MAX(0, rect.h - DISPLAY_MARGINS); bounds->height = MAX(0, rect.h - DISPLAY_MARGINS);
return true; return SDL_TRUE;
} }
// return the optimal size of the window, with the following constraints: // return the optimal size of the window, with the following constraints:
// - it attempts to keep at least one dimension of the current_size (i.e. it // - it attempts to keep at least one dimension of the current_size (i.e. it crops the black borders)
// crops the black borders)
// - it keeps the aspect ratio // - it keeps the aspect ratio
// - it scales down to make it fit in the display_size // - it scales down to make it fit in the display_size
static struct size static struct size get_optimal_size(struct size current_size, struct size frame_size) {
get_optimal_size(struct size current_size, struct size frame_size) {
if (frame_size.width == 0 || frame_size.height == 0) { if (frame_size.width == 0 || frame_size.height == 0) {
// avoid division by 0 // avoid division by 0
return current_size; return current_size;
@@ -81,8 +95,8 @@ get_optimal_size(struct size current_size, struct size frame_size) {
struct size display_size; struct size display_size;
// 32 bits because we need to multiply two 16 bits values // 32 bits because we need to multiply two 16 bits values
uint32_t w; Uint32 w;
uint32_t h; Uint32 h;
if (!get_preferred_display_bounds(&display_size)) { if (!get_preferred_display_bounds(&display_size)) {
// cannot get display bounds, do not constraint the size // cannot get display bounds, do not constraint the size
@@ -93,13 +107,12 @@ get_optimal_size(struct size current_size, struct size frame_size) {
h = MIN(current_size.height, display_size.height); h = MIN(current_size.height, display_size.height);
} }
bool keep_width = frame_size.width * h > frame_size.height * w; SDL_bool keep_width = frame_size.width * h > frame_size.height * w;
if (keep_width) { if (keep_width) {
// remove black borders on top and bottom // remove black borders on top and bottom
h = frame_size.height * w / frame_size.width; h = frame_size.height * w / frame_size.width;
} else { } else {
// remove black borders on left and right (or none at all if it already // remove black borders on left and right (or none at all if it already fits)
// fits)
w = frame_size.width * h / frame_size.height; w = frame_size.width * h / frame_size.height;
} }
@@ -109,101 +122,71 @@ get_optimal_size(struct size current_size, struct size frame_size) {
} }
// same as get_optimal_size(), but read the current size from the window // same as get_optimal_size(), but read the current size from the window
static inline struct size static inline struct size get_optimal_window_size(const struct screen *screen, struct size frame_size) {
get_optimal_window_size(const struct screen *screen, struct size frame_size) {
struct size current_size = get_window_size(screen); struct size current_size = get_window_size(screen);
return get_optimal_size(current_size, frame_size); return get_optimal_size(current_size, frame_size);
} }
// initially, there is no current size, so use the frame size as current size // initially, there is no current size, so use the frame size as current size
static inline struct size static inline struct size get_initial_optimal_size(struct size frame_size) {
get_initial_optimal_size(struct size frame_size) {
return get_optimal_size(frame_size, frame_size); return get_optimal_size(frame_size, frame_size);
} }
void void screen_init(struct screen *screen) {
screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER; *screen = (struct screen) SCREEN_INITIALIZER;
} }
static inline SDL_Texture * SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, struct size frame_size) {
create_texture(SDL_Renderer *renderer, struct size frame_size) {
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
}
bool
screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top) {
screen->frame_size = frame_size; screen->frame_size = frame_size;
struct size window_size = get_initial_optimal_size(frame_size); struct size window_size = get_initial_optimal_size(frame_size);
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
if (always_on_top) {
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else
LOGW("The 'always on top' flag is not available "
"(compile with SDL >= 2.0.5 to enable it)");
#endif
}
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height, window_size.width, window_size.height,
window_flags); SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
if (!screen->window) { if (!screen->window) {
LOGC("Could not create window: %s", SDL_GetError()); LOGC("Could not create window: %s", SDL_GetError());
return false; return SDL_FALSE;
} }
screen->renderer = SDL_CreateRenderer(screen->window, -1, screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED);
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) { if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError()); LOGC("Could not create renderer: %s", SDL_GetError());
screen_destroy(screen); screen_destroy(screen);
return false; return SDL_FALSE;
} }
if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, frame_size.height)) {
frame_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError()); LOGE("Could not set renderer logical size: %s", SDL_GetError());
screen_destroy(screen); screen_destroy(screen);
return false; return SDL_FALSE;
} }
SDL_Surface *icon = read_xpm(icon_xpm); SDL_Surface *icon = read_xpm(icon_xpm);
if (!icon) { if (!icon) {
LOGE("Could not load icon: %s", SDL_GetError()); LOGE("Could not load icon: %s", SDL_GetError());
screen_destroy(screen); screen_destroy(screen);
return false; return SDL_FALSE;
} }
SDL_SetWindowIcon(screen->window, icon); SDL_SetWindowIcon(screen->window, icon);
SDL_FreeSurface(icon); SDL_FreeSurface(icon);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height);
frame_size.height); screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
screen->texture = create_texture(screen->renderer, frame_size); frame_size.width, frame_size.height);
if (!screen->texture) { if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError()); LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen); screen_destroy(screen);
return false; return SDL_FALSE;
} }
return true; return SDL_TRUE;
} }
void void screen_show_window(struct screen *screen) {
screen_show_window(struct screen *screen) {
SDL_ShowWindow(screen->window); SDL_ShowWindow(screen->window);
} }
void void screen_destroy(struct screen *screen) {
screen_destroy(struct screen *screen) {
if (screen->texture) { if (screen->texture) {
SDL_DestroyTexture(screen->texture); SDL_DestroyTexture(screen->texture);
} }
@@ -216,14 +199,11 @@ screen_destroy(struct screen *screen) {
} }
// recreate the texture and resize the window if the frame size has changed // recreate the texture and resize the window if the frame size has changed
static bool static SDL_bool prepare_for_frame(struct screen *screen, struct size new_frame_size) {
prepare_for_frame(struct screen *screen, struct size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) {
if (screen->frame_size.width != new_frame_size.width if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width, new_frame_size.height)) {
|| screen->frame_size.height != new_frame_size.height) {
if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width,
new_frame_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError()); LOGE("Could not set renderer logical size: %s", SDL_GetError());
return false; return SDL_FALSE;
} }
// frame dimension changed, destroy texture // frame dimension changed, destroy texture
@@ -231,67 +211,62 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) {
struct size current_size = get_window_size(screen); struct size current_size = get_window_size(screen);
struct size target_size = { struct size target_size = {
(uint32_t) current_size.width * new_frame_size.width (Uint32) current_size.width * new_frame_size.width / screen->frame_size.width,
/ screen->frame_size.width, (Uint32) current_size.height * new_frame_size.height / screen->frame_size.height,
(uint32_t) current_size.height * new_frame_size.height
/ screen->frame_size.height,
}; };
target_size = get_optimal_size(target_size, new_frame_size); target_size = get_optimal_size(target_size, new_frame_size);
set_window_size(screen, target_size); set_window_size(screen, target_size);
screen->frame_size = new_frame_size; screen->frame_size = new_frame_size;
LOGI("New texture: %" PRIu16 "x%" PRIu16, LOGD("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height); screen->frame_size.width, screen->frame_size.height);
screen->texture = create_texture(screen->renderer, new_frame_size); screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
new_frame_size.width, new_frame_size.height);
if (!screen->texture) { if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError()); LOGC("Could not create texture: %s", SDL_GetError());
return false; return SDL_FALSE;
} }
} }
return true; return SDL_TRUE;
} }
// write the frame into the texture // write the frame into the texture
static void static void update_texture(struct screen *screen, const AVFrame *frame) {
update_texture(struct screen *screen, const AVFrame *frame) {
SDL_UpdateYUVTexture(screen->texture, NULL, SDL_UpdateYUVTexture(screen->texture, NULL,
frame->data[0], frame->linesize[0], frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]); frame->data[2], frame->linesize[2]);
} }
bool SDL_bool screen_update_frame(struct screen *screen, struct frames *frames) {
screen_update_frame(struct screen *screen, struct video_buffer *vb) { mutex_lock(frames->mutex);
mutex_lock(vb->mutex); const AVFrame *frame = frames_consume_rendered_frame(frames);
const AVFrame *frame = video_buffer_consume_rendered_frame(vb);
struct size new_frame_size = {frame->width, frame->height}; struct size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) { if (!prepare_for_frame(screen, new_frame_size)) {
mutex_unlock(vb->mutex); mutex_unlock(frames->mutex);
return false; return SDL_FALSE;
} }
update_texture(screen, frame); update_texture(screen, frame);
mutex_unlock(vb->mutex); mutex_unlock(frames->mutex);
screen_render(screen); screen_render(screen);
return true; return SDL_TRUE;
} }
void void screen_render(struct screen *screen) {
screen_render(struct screen *screen) {
SDL_RenderClear(screen->renderer); SDL_RenderClear(screen->renderer);
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
SDL_RenderPresent(screen->renderer); SDL_RenderPresent(screen->renderer);
} }
void void screen_switch_fullscreen(struct screen *screen) {
screen_switch_fullscreen(struct screen *screen) {
if (!screen->fullscreen) { if (!screen->fullscreen) {
// going to fullscreen, store the current windowed window size // going to fullscreen, store the current windowed window size
screen->windowed_window_size = get_native_window_size(screen->window); screen->windowed_window_size = get_native_window_size(screen->window);
} }
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; Uint32 new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) { if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
return; return;
@@ -300,30 +275,24 @@ screen_switch_fullscreen(struct screen *screen) {
screen->fullscreen = !screen->fullscreen; screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen) { if (!screen->fullscreen) {
// fullscreen disabled, restore expected windowed window size // fullscreen disabled, restore expected windowed window size
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, screen->windowed_window_size.height);
screen->windowed_window_size.height);
} }
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
screen_render(screen); screen_render(screen);
} }
void void screen_resize_to_fit(struct screen *screen) {
screen_resize_to_fit(struct screen *screen) {
if (!screen->fullscreen) { if (!screen->fullscreen) {
struct size optimal_size = get_optimal_window_size(screen, struct size optimal_size = get_optimal_window_size(screen, screen->frame_size);
screen->frame_size); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
SDL_SetWindowSize(screen->window, optimal_size.width,
optimal_size.height);
LOGD("Resized to optimal size"); LOGD("Resized to optimal size");
} }
} }
void void screen_resize_to_pixel_perfect(struct screen *screen) {
screen_resize_to_pixel_perfect(struct screen *screen) {
if (!screen->fullscreen) { if (!screen->fullscreen) {
SDL_SetWindowSize(screen->window, screen->frame_size.width, SDL_SetWindowSize(screen->window, screen->frame_size.width, screen->frame_size.height);
screen->frame_size.height);
LOGD("Resized to pixel-perfect"); LOGD("Resized to pixel-perfect");
} }
} }

View File

@@ -1,13 +1,11 @@
#ifndef SCREEN_H #ifndef SCREEN_H
#define SCREEN_H #define SCREEN_H
#include <stdbool.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include "common.h" #include "common.h"
#include "frames.h"
struct video_buffer;
struct screen { struct screen {
SDL_Window *window; SDL_Window *window;
@@ -16,9 +14,8 @@ struct screen {
struct size frame_size; struct size frame_size;
//used only in fullscreen mode to know the windowed window size //used only in fullscreen mode to know the windowed window size
struct size windowed_window_size; struct size windowed_window_size;
bool has_frame; SDL_bool has_frame;
bool fullscreen; SDL_bool fullscreen;
bool no_window;
}; };
#define SCREEN_INITIALIZER { \ #define SCREEN_INITIALIZER { \
@@ -33,46 +30,40 @@ struct screen {
.width = 0, \ .width = 0, \
.height = 0, \ .height = 0, \
}, \ }, \
.has_frame = false, \ .has_frame = SDL_FALSE, \
.fullscreen = false, \ .fullscreen = SDL_FALSE, \
.no_window = false, \
} }
// init SDL and set appropriate hints
SDL_bool sdl_init_and_configure(void);
// initialize default values // initialize default values
void void screen_init(struct screen *screen);
screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden) // initialize screen, create window, renderer and texture (window is hidden)
bool SDL_bool screen_init_rendering(struct screen *screen,
screen_init_rendering(struct screen *screen, const char *device_name, const char *device_name,
struct size frame_size, bool always_on_top); struct size frame_size);
// show the window // show the window
void void screen_show_window(struct screen *screen);
screen_show_window(struct screen *screen);
// destroy window, renderer and texture (if any) // destroy window, renderer and texture (if any)
void void screen_destroy(struct screen *screen);
screen_destroy(struct screen *screen);
// resize if necessary and write the rendered frame into the texture // resize if necessary and write the rendered frame into the texture
bool SDL_bool screen_update_frame(struct screen *screen, struct frames *frames);
screen_update_frame(struct screen *screen, struct video_buffer *vb);
// render the texture to the renderer // render the texture to the renderer
void void screen_render(struct screen *screen);
screen_render(struct screen *screen);
// switch the fullscreen mode // switch the fullscreen mode
void void screen_switch_fullscreen(struct screen *screen);
screen_switch_fullscreen(struct screen *screen);
// resize window to optimal size (remove black borders) // resize window to optimal size (remove black borders)
void void screen_resize_to_fit(struct screen *screen);
screen_resize_to_fit(struct screen *screen);
// resize window to 1:1 (pixel-perfect) // resize window to 1:1 (pixel-perfect)
void void screen_resize_to_pixel_perfect(struct screen *screen);
screen_resize_to_pixel_perfect(struct screen *screen);
#endif #endif

View File

@@ -2,9 +2,8 @@
#include <errno.h> #include <errno.h>
#include <inttypes.h> #include <inttypes.h>
#include <stdio.h> #include <stdint.h>
#include <SDL2/SDL_assert.h> #include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h" #include "config.h"
#include "log.h" #include "log.h"
@@ -20,8 +19,7 @@
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static const char * static const char *get_server_path(void) {
get_server_path(void) {
const char *server_path = getenv("SCRCPY_SERVER_PATH"); const char *server_path = getenv("SCRCPY_SERVER_PATH");
if (!server_path) { if (!server_path) {
server_path = DEFAULT_SERVER_PATH; server_path = DEFAULT_SERVER_PATH;
@@ -29,60 +27,27 @@ get_server_path(void) {
return server_path; return server_path;
} }
static bool static SDL_bool push_server(const char *serial) {
push_server(const char *serial) {
process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH); process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH);
return process_check_success(process, "adb push"); return process_check_success(process, "adb push");
} }
static bool static SDL_bool remove_server(const char *serial) {
enable_tunnel_reverse(const char *serial, uint16_t local_port) { process_t process = adb_remove_path(serial, DEVICE_SERVER_PATH);
return process_check_success(process, "adb shell rm");
}
static SDL_bool enable_tunnel(const char *serial, Uint16 local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port); process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse"); return process_check_success(process, "adb reverse");
} }
static bool static SDL_bool disable_tunnel(const char *serial) {
disable_tunnel_reverse(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME); process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove"); return process_check_success(process, "adb reverse --remove");
} }
static bool static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate) {
enable_tunnel_forward(const char *serial, uint16_t local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward");
}
static bool
disable_tunnel_forward(const char *serial, uint16_t local_port) {
process_t process = adb_forward_remove(serial, local_port);
return process_check_success(process, "adb forward --remove");
}
static bool
enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return true;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = true;
return enable_tunnel_forward(server->serial, server->local_port);
}
static 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_t max_size, uint32_t bit_rate,
bool tunnel_forward, const char *crop,
bool send_frame_meta) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size); sprintf(max_size_string, "%"PRIu16, max_size);
@@ -95,55 +60,16 @@ execute_server(const char *serial,
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
tunnel_forward ? "true" : "false",
crop ? crop : "-",
send_frame_meta ? "true" : "false",
}; };
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
} }
static socket_t listen_on_port(Uint16 port) {
#define IPV4_LOCALHOST 0x7F000001 #define IPV4_LOCALHOST 0x7F000001
static socket_t
listen_on_port(uint16_t port) {
return net_listen(IPV4_LOCALHOST, port, 1); return net_listen(IPV4_LOCALHOST, port, 1);
} }
static socket_t static void close_socket(socket_t *socket) {
connect_and_read_byte(uint16_t 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_t port, uint32_t attempts, uint32_t 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); SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR); net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) { if (!net_close(*socket)) {
@@ -153,101 +79,69 @@ close_socket(socket_t *socket) {
*socket = INVALID_SOCKET; *socket = INVALID_SOCKET;
} }
void void server_init(struct server *server) {
server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER; *server = (struct server) SERVER_INITIALIZER;
} }
bool SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
server_start(struct server *server, const char *serial, Uint16 max_size, Uint32 bit_rate) {
uint16_t local_port, uint16_t max_size, uint32_t bit_rate,
const char *crop, bool send_frame_meta) {
server->local_port = local_port;
if (serial) {
server->serial = SDL_strdup(serial);
if (!server->serial) {
return false;
}
}
if (!push_server(serial)) { if (!push_server(serial)) {
SDL_free(server->serial); return SDL_FALSE;
return false;
} }
if (!enable_tunnel(server)) { server->server_copied_to_device = SDL_TRUE;
SDL_free(server->serial);
return false; if (!enable_tunnel(serial, local_port)) {
return SDL_FALSE;
} }
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to // At the application level, the device part is "the server" because it
// "adb forward", so the app socket is the client // serves video stream and control. However, at the network level, the
if (!server->tunnel_forward) { // client listens and the server connects to the client. That way, the
// At the application level, the device part is "the server" because it // client can listen before starting the server app, so there is no need to
// serves video stream and control. However, at the network level, the // try to connect until the server socket is listening on the device.
// 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); server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) { if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port); LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server); disable_tunnel(serial);
SDL_free(server->serial); return SDL_FALSE;
return false;
}
} }
// server will connect to our server socket // 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, crop,
send_frame_meta);
if (server->process == PROCESS_NONE) { if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) { close_socket(&server->server_socket);
close_socket(&server->server_socket); disable_tunnel(serial);
} return SDL_FALSE;
disable_tunnel(server);
SDL_free((void *) server->serial);
return false;
} }
server->tunnel_enabled = true; server->adb_reverse_enabled = SDL_TRUE;
return true; return SDL_TRUE;
} }
socket_t socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms) {
server_connect_to(struct server *server) { server->device_socket = net_accept(server->server_socket);
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
uint32_t attempts = 100;
uint32_t delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts,
delay);
}
if (server->device_socket == INVALID_SOCKET) { if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET; return INVALID_SOCKET;
} }
if (!server->tunnel_forward) { // we don't need the server socket anymore
// we don't need the server socket anymore close_socket(&server->server_socket);
close_socket(&server->server_socket);
} // the server is started, we can clean up the jar from the temporary folder
remove_server(serial); // ignore failure
server->server_copied_to_device = SDL_FALSE;
// we don't need the adb tunnel anymore // we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure disable_tunnel(serial); // ignore failure
server->tunnel_enabled = false; server->adb_reverse_enabled = SDL_FALSE;
return server->device_socket; return server->device_socket;
} }
void void server_stop(struct server *server, const char *serial) {
server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE); SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) { if (!cmd_terminate(server->process)) {
@@ -257,19 +151,21 @@ server_stop(struct server *server) {
cmd_simple_wait(server->process, NULL); // ignore exit code cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated"); LOGD("Server terminated");
if (server->tunnel_enabled) { if (server->adb_reverse_enabled) {
// ignore failure // ignore failure
disable_tunnel(server); disable_tunnel(serial);
}
if (server->server_copied_to_device) {
remove_server(serial); // ignore failure
} }
} }
void void server_destroy(struct server *server) {
server_destroy(struct server *server) {
if (server->server_socket != INVALID_SOCKET) { if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket); close_socket(&server->server_socket);
} }
if (server->device_socket != INVALID_SOCKET) { if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket); close_socket(&server->device_socket);
} }
SDL_free(server->serial);
} }

View File

@@ -1,54 +1,39 @@
#ifndef SERVER_H #ifndef SERVER_H
#define SERVER_H #define SERVER_H
#include <stdbool.h>
#include <stdint.h>
#include "command.h" #include "command.h"
#include "net.h" #include "net.h"
struct server { struct server {
char *serial;
process_t process; process_t process;
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket;
socket_t device_socket; socket_t device_socket;
uint16_t local_port; SDL_bool adb_reverse_enabled;
bool tunnel_enabled; SDL_bool server_copied_to_device;
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
bool send_frame_meta; // request frame PTS to be able to record properly
}; };
#define SERVER_INITIALIZER { \ #define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.device_socket = INVALID_SOCKET, \ .device_socket = INVALID_SOCKET, \
.local_port = 0, \ .adb_reverse_enabled = SDL_FALSE, \
.tunnel_enabled = false, \ .server_copied_to_device = SDL_FALSE, \
.tunnel_forward = false, \
.send_frame_meta = false, \
} }
// init default values // init default values
void void server_init(struct server *server);
server_init(struct server *server);
// push, enable tunnel et start the server // push, enable tunnel et start the server
bool SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
server_start(struct server *server, const char *serial, Uint16 max_size, Uint32 bit_rate);
uint16_t local_port, uint16_t max_size, uint32_t bit_rate,
const char *crop, bool send_frame_meta);
// block until the communication with the server is established // block until the communication with the server is established
socket_t socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms);
server_connect_to(struct server *server);
// disconnect and kill the server process // disconnect and kill the server process
void void server_stop(struct server *server, const char *serial);
server_stop(struct server *server);
// close and release sockets // close and release sockets
void void server_destroy(struct server *server);
server_destroy(struct server *server);
#endif #endif

View File

@@ -1,77 +0,0 @@
#include "str_util.h"
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
# include <windows.h>
# include <tchar.h>
#endif
size_t
xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
for (i = 0; i < n - 1 && src[i] != '\0'; ++i)
dest[i] = src[i];
if (n)
dest[i] = '\0';
return src[i] == '\0' ? i : n;
}
size_t
xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
const char *const *remaining = tokens;
const char *token = *remaining++;
size_t i = 0;
while (token) {
if (i) {
dst[i++] = sep;
if (i == n)
goto truncated;
}
size_t w = xstrncpy(dst + i, token, n - i);
if (w >= n - i)
goto truncated;
i += w;
token = *remaining++;
}
return i;
truncated:
dst[n - 1] = '\0';
return n;
}
char *
strquote(const char *src) {
size_t len = strlen(src);
char *quoted = malloc(len + 3);
if (!quoted) {
return NULL;
}
memcpy(&quoted[1], src, len);
quoted[0] = '"';
quoted[len + 1] = '"';
quoted[len + 2] = '\0';
return quoted;
}
#ifdef _WIN32
wchar_t *
utf8_to_wide_char(const char *utf8) {
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
if (!len) {
return NULL;
}
wchar_t *wide = malloc(len * sizeof(wchar_t));
if (!wide) {
return NULL;
}
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, len);
return wide;
}
#endif

View File

@@ -1,286 +0,0 @@
#include "stream.h"
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "buffer_util.h"
#include "decoder.h"
#include "events.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h"
#define BUFSIZE 0x10000
#define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1)
static struct frame_meta *
frame_meta_new(uint64_t pts) {
struct frame_meta *meta = malloc(sizeof(*meta));
if (!meta) {
return meta;
}
meta->pts = pts;
meta->next = NULL;
return meta;
}
static void
frame_meta_delete(struct frame_meta *frame_meta) {
free(frame_meta);
}
static bool
receiver_state_push_meta(struct receiver_state *state, uint64_t pts) {
struct frame_meta *frame_meta = frame_meta_new(pts);
if (!frame_meta) {
return false;
}
// append to the list
// (iterate to find the last item, in practice the list should be tiny)
struct frame_meta **p = &state->frame_meta_queue;
while (*p) {
p = &(*p)->next;
}
*p = frame_meta;
return true;
}
static uint64_t
receiver_state_take_meta(struct receiver_state *state) {
struct frame_meta *frame_meta = state->frame_meta_queue; // first item
SDL_assert(frame_meta); // must not be empty
uint64_t pts = frame_meta->pts;
state->frame_meta_queue = frame_meta->next; // remove the item
frame_meta_delete(frame_meta);
return pts;
}
static int
read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
struct receiver_state *state = &stream->receiver_state;
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
//
// The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
// <-------------> <-----> <-----------------------------...
// PTS packet raw packet
// size
//
// It is followed by <packet_size> bytes containing the packet/frame.
if (!state->remaining) {
#define HEADER_SIZE 12
uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
// no partial read (net_recv_all())
SDL_assert_release(r == HEADER_SIZE);
uint64_t pts = buffer_read64be(header);
state->remaining = buffer_read32be(&header[8]);
if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
LOGE("Could not store PTS for recording");
// we cannot save the PTS, the recording would be broken
return AVERROR(ENOMEM);
}
}
SDL_assert(state->remaining);
if (buf_size > state->remaining) {
buf_size = state->remaining;
}
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
SDL_assert(state->remaining >= r);
state->remaining -= r;
return r;
}
static int
read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
}
static void
notify_stopped(void) {
SDL_Event stop_event;
stop_event.type = EVENT_STREAM_STOPPED;
SDL_PushEvent(&stop_event);
}
static int
run_stream(void *data) {
struct stream *stream = data;
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto end;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto finally_free_format_ctx;
}
// initialize the receiver state
stream->receiver_state.frame_meta_queue = NULL;
stream->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets
int (*read_packet)(void *, uint8_t *, int) =
stream->recorder ? read_packet_with_meta : read_raw_packet;
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream,
read_packet, NULL, NULL);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto finally_free_format_ctx;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
goto finally_free_avio_ctx;
}
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto end;
}
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder");
goto finally_close_input;
}
if (stream->recorder && !recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_input;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet)) {
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
av_packet_unref(&packet);
goto quit;
}
if (stream->recorder) {
// we retrieve the PTS in order they were received, so they will
// be assigned to the correct frame
uint64_t pts = receiver_state_take_meta(&stream->receiver_state);
packet.pts = pts;
packet.dts = pts;
// no need to rescale with av_packet_rescale_ts(), the timestamps
// are in microseconds both in input and output
if (!recorder_write(stream->recorder, &packet)) {
LOGE("Could not write frame to output file");
av_packet_unref(&packet);
goto quit;
}
}
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
break;
}
}
LOGD("End of frames");
quit:
if (stream->recorder) {
recorder_close(stream->recorder);
}
finally_close_input:
avformat_close_input(&format_ctx);
finally_free_avio_ctx:
av_free(avio_ctx->buffer);
av_free(avio_ctx);
finally_free_format_ctx:
avformat_free_context(format_ctx);
end:
notify_stopped();
return 0;
}
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder) {
stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
}
bool
stream_start(struct stream *stream) {
LOGD("Starting stream thread");
stream->thread = SDL_CreateThread(run_stream, "stream", stream);
if (!stream->thread) {
LOGC("Could not start stream thread");
return false;
}
return true;
}
void
stream_stop(struct stream *stream) {
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}
}
void
stream_join(struct stream *stream) {
SDL_WaitThread(stream->thread, NULL);
}

View File

@@ -1,43 +0,0 @@
#ifndef STREAM_H
#define STREAM_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
struct video_buffer;
struct frame_meta {
uint64_t pts;
struct frame_meta *next;
};
struct stream {
socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread;
struct decoder *decoder;
struct recorder *recorder;
struct receiver_state {
// meta (in order) for frames not consumed yet
struct frame_meta *frame_meta_queue;
size_t remaining; // remaining bytes to receive for the current frame
} receiver_state;
};
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder);
bool
stream_start(struct stream *stream);
void
stream_stop(struct stream *stream);
void
stream_join(struct stream *stream);
#endif

33
app/src/strutil.c Normal file
View File

@@ -0,0 +1,33 @@
#include "strutil.h"
size_t xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
for (i = 0; i < n - 1 && src[i] != '\0'; ++i)
dest[i] = src[i];
if (n)
dest[i] = '\0';
return src[i] == '\0' ? i : n;
}
size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
const char *const *remaining = tokens;
const char *token = *remaining++;
size_t i = 0;
while (token) {
if (i) {
dst[i++] = sep;
if (i == n)
goto truncated;
}
size_t w = xstrncpy(dst + i, token, n - i);
if (w >= n - i)
goto truncated;
i += w;
token = *remaining++;
}
return i;
truncated:
dst[n - 1] = '\0';
return n;
}

View File

@@ -9,25 +9,11 @@
// - it does not write useless bytes if strlen(src) < n // - it does not write useless bytes if strlen(src) < n
// - it returns the number of chars actually written (max n-1) if src has // - it returns the number of chars actually written (max n-1) if src has
// been copied completely, or n if src has been truncated // been copied completely, or n if src has been truncated
size_t size_t xstrncpy(char *dest, const char *src, size_t n);
xstrncpy(char *dest, const char *src, size_t n);
// join tokens by sep into dst // join tokens by sep into dst
// returns the number of chars actually written (max n-1) if no trucation // returns the number of chars actually written (max n-1) if no trucation
// occurred, or n if truncated // occurred, or n if truncated
size_t size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
// quote a string
// returns the new allocated string, to be freed by the caller
char *
strquote(const char *src);
#ifdef _WIN32
// convert a UTF-8 string to a wchar_t string
// returns the new allocated string, to be freed by the caller
wchar_t *
utf8_to_wide_char(const char *utf8);
#endif
#endif #endif

View File

@@ -1,90 +1,29 @@
#define _POSIX_SOURCE // for kill() #include "../../command.h"
#include "command.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h> #include <signal.h>
#include <stdlib.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#include "log.h"
enum process_result pid_t cmd_execute(const char *path, const char *const argv[]) {
cmd_execute(const char *path, const char *const argv[], pid_t *pid) { pid_t pid = fork();
int fd[2]; if (pid == -1) {
if (pipe(fd) == -1) {
perror("pipe");
return PROCESS_ERROR_GENERIC;
}
enum process_result ret = PROCESS_SUCCESS;
*pid = fork();
if (*pid == -1) {
perror("fork"); perror("fork");
ret = PROCESS_ERROR_GENERIC; return -1;
goto end;
} }
if (pid == 0) {
if (*pid > 0) { execvp(path, (char *const *)argv);
// parent close write side perror("exec");
close(fd[1]);
fd[1] = -1;
// wait for EOF or receive errno from child
if (read(fd[0], &ret, sizeof(ret)) == -1) {
perror("read");
ret = PROCESS_ERROR_GENERIC;
goto end;
}
} else if (*pid == 0) {
// child close read side
close(fd[0]);
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
execvp(path, (char *const *)argv);
if (errno == ENOENT) {
ret = PROCESS_ERROR_MISSING_BINARY;
} else {
ret = PROCESS_ERROR_GENERIC;
}
perror("exec");
} else {
perror("fcntl");
ret = PROCESS_ERROR_GENERIC;
}
// send ret to the parent
if (write(fd[1], &ret, sizeof(ret)) == -1) {
perror("write");
}
// close write side before exiting
close(fd[1]);
_exit(1); _exit(1);
} }
return pid;
end:
if (fd[0] != -1) {
close(fd[0]);
}
if (fd[1] != -1) {
close(fd[1]);
}
return ret;
} }
bool SDL_bool cmd_terminate(pid_t pid) {
cmd_terminate(pid_t pid) {
if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n",
(int) pid);
abort();
}
return kill(pid, SIGTERM) != -1; return kill(pid, SIGTERM) != -1;
} }
bool SDL_bool cmd_simple_wait(pid_t pid, int *exit_code) {
cmd_simple_wait(pid_t pid, int *exit_code) {
int status; int status;
int code; int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {

View File

@@ -1,19 +1,16 @@
#include "net.h" #include "../../net.h"
#include <unistd.h> # include <unistd.h>
bool SDL_bool net_init(void) {
net_init(void) {
// do nothing // do nothing
return true; return SDL_TRUE;
} }
void void net_cleanup(void) {
net_cleanup(void) {
// do nothing // do nothing
} }
bool SDL_bool net_close(socket_t socket) {
net_close(socket_t socket) {
return !close(socket); return !close(socket);
} }

View File

@@ -1,72 +1,39 @@
#include "command.h" #include "../../command.h"
#include "config.h" #include "../../log.h"
#include "log.h" #include "../../strutil.h"
#include "str_util.h"
static int HANDLE cmd_execute(const char *path, const char *const argv[]) {
build_cmd(char *cmd, size_t len, const char *const argv[]) { STARTUPINFO si;
// Windows command-line parsing is WTF:
// <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS>
// only make it work for this very specific program
// (don't handle escaping nor quotes)
size_t ret = xstrjoin(cmd, argv, ' ', len);
if (ret >= len) {
LOGE("Command too long (%" PRIsizet " chars)", len - 1);
return -1;
}
return 0;
}
enum process_result
cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
STARTUPINFOW si;
PROCESS_INFORMATION pi; PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si)); memset(&si, 0, sizeof(si));
si.cb = sizeof(si); si.cb = sizeof(si);
// Windows command-line parsing is WTF:
// <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS>
// only make it work for this very specific program
// (don't handle escaping nor quotes)
char cmd[256]; char cmd[256];
if (build_cmd(cmd, sizeof(cmd), argv)) { size_t ret = xstrjoin(cmd, argv, ' ', sizeof(cmd));
*handle = NULL; if (ret >= sizeof(cmd)) {
return PROCESS_ERROR_GENERIC; LOGE("Command too long (%" PRIsizet " chars)", sizeof(cmd) - 1);
return NULL;
} }
wchar_t *wide = utf8_to_wide_char(cmd); if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
if (!wide) { return NULL;
LOGC("Cannot allocate wide char string");
return PROCESS_ERROR_GENERIC;
} }
#ifdef WINDOWS_NOCONSOLE return pi.hProcess;
int flags = CREATE_NO_WINDOW;
#else
int flags = 0;
#endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) {
free(wide);
*handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY;
}
return PROCESS_ERROR_GENERIC;
}
free(wide);
*handle = pi.hProcess;
return PROCESS_SUCCESS;
} }
bool SDL_bool cmd_terminate(HANDLE handle) {
cmd_terminate(HANDLE handle) {
return TerminateProcess(handle, 1) && CloseHandle(handle); return TerminateProcess(handle, 1) && CloseHandle(handle);
} }
bool SDL_bool cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code; DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) {
|| !GetExitCodeProcess(handle, &code)) {
// cannot wait or retrieve the exit code // cannot wait or retrieve the exit code
code = -1; // max value, it's unsigned code = -1; // max value, it's unsigned
} }

View File

@@ -1,24 +1,21 @@
#include "net.h" #include "../../net.h"
#include "log.h" #include "../../log.h"
bool SDL_bool net_init(void) {
net_init(void) {
WSADATA wsa; WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) { if (res < 0) {
LOGC("WSAStartup failed with error %d", res); LOGC("WSAStartup failed with error %d", res);
return false; return SDL_FALSE;
} }
return true; return SDL_TRUE;
} }
void void net_cleanup(void) {
net_cleanup(void) {
WSACleanup(); WSACleanup();
} }
bool SDL_bool net_close(socket_t socket) {
net_close(socket_t socket) {
return !closesocket(socket); return !closesocket(socket);
} }

View File

@@ -1,7 +1,5 @@
#include "tiny_xpm.h" #include "tinyxpm.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -9,20 +7,19 @@
struct index { struct index {
char c; char c;
uint32_t color; Uint32 color;
}; };
static bool static SDL_bool find_color(struct index *index, int len, char c, Uint32 *color) {
find_color(struct index *index, int len, char c, uint32_t *color) {
// there are typically very few color, so it's ok to iterate over the array // there are typically very few color, so it's ok to iterate over the array
for (int i = 0; i < len; ++i) { for (int i = 0; i < len; ++i) {
if (index[i].c == c) { if (index[i].c == c) {
*color = index[i].color; *color = index[i].color;
return true; return SDL_TRUE;
} }
} }
*color = 0; *color = 0;
return false; return SDL_FALSE;
} }
// We encounter some problems with SDL2_image on MSYS2 (Windows), // We encounter some problems with SDL2_image on MSYS2 (Windows),
@@ -33,8 +30,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) {
// //
// Parameter is not "const char *" because XPM formats are generally stored in a // Parameter is not "const char *" because XPM formats are generally stored in a
// (non-const) "char *" // (non-const) "char *"
SDL_Surface * SDL_Surface *read_xpm(char *xpm[]) {
read_xpm(char *xpm[]) {
#if SDL_ASSERT_LEVEL >= 2 #if SDL_ASSERT_LEVEL >= 2
// patch the XPM to change the icon color in debug mode // patch the XPM to change the icon color in debug mode
xpm[2] = ". c #CC00CC"; xpm[2] = ". c #CC00CC";
@@ -44,15 +40,15 @@ read_xpm(char *xpm[]) {
// *** No error handling, assume the XPM source is valid *** // *** No error handling, assume the XPM source is valid ***
// (it's in our source repo) // (it's in our source repo)
// Assertions are only checked in debug // Assertions are only checked in debug
int width = strtol(xpm[0], &endptr, 10); Uint32 width = strtol(xpm[0], &endptr, 10);
int height = strtol(endptr + 1, &endptr, 10); Uint32 height = strtol(endptr + 1, &endptr, 10);
int colors = strtol(endptr + 1, &endptr, 10); Uint32 colors = strtol(endptr + 1, &endptr, 10);
int chars = strtol(endptr + 1, &endptr, 10); Uint32 chars = strtol(endptr + 1, &endptr, 10);
// sanity checks // sanity checks
SDL_assert(0 <= width && width < 256); SDL_assert(width < 256);
SDL_assert(0 <= height && height < 256); SDL_assert(height < 256);
SDL_assert(0 <= colors && colors < 256); SDL_assert(colors < 256);
SDL_assert(chars == 1); // this implementation does not support more SDL_assert(chars == 1); // this implementation does not support more
// init index // init index
@@ -73,7 +69,7 @@ read_xpm(char *xpm[]) {
} }
// parse image // parse image
uint32_t *pixels = SDL_malloc(4 * width * height); Uint32 *pixels = SDL_malloc(4 * width * height);
if (!pixels) { if (!pixels) {
LOGE("Could not allocate icon memory"); LOGE("Could not allocate icon memory");
return NULL; return NULL;
@@ -82,23 +78,23 @@ read_xpm(char *xpm[]) {
const char *line = xpm[1 + colors + y]; const char *line = xpm[1 + colors + y];
for (int x = 0; x < width; ++x) { for (int x = 0; x < width; ++x) {
char c = line[x]; char c = line[x];
uint32_t color; Uint32 color;
bool color_found = find_color(index, colors, c, &color); SDL_bool color_found = find_color(index, colors, c, &color);
SDL_assert(color_found); SDL_assert(color_found);
pixels[y * width + x] = color; pixels[y * width + x] = color;
} }
} }
#if SDL_BYTEORDER == SDL_BIG_ENDIAN #if SDL_BYTEORDER == SDL_BIG_ENDIAN
uint32_t amask = 0x000000ff; Uint32 amask = 0x000000ff;
uint32_t rmask = 0x0000ff00; Uint32 rmask = 0x0000ff00;
uint32_t gmask = 0x00ff0000; Uint32 gmask = 0x00ff0000;
uint32_t bmask = 0xff000000; Uint32 bmask = 0xff000000;
#else // little endian, like x86 #else // little endian, like x86
uint32_t amask = 0xff000000; Uint32 amask = 0xff000000;
uint32_t rmask = 0x00ff0000; Uint32 rmask = 0x00ff0000;
uint32_t gmask = 0x0000ff00; Uint32 gmask = 0x0000ff00;
uint32_t bmask = 0x000000ff; Uint32 bmask = 0x000000ff;
#endif #endif
SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels,

View File

@@ -3,7 +3,6 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
SDL_Surface * SDL_Surface *read_xpm(char *xpm[]);
read_xpm(char *xpm[]);
#endif #endif

View File

@@ -1,116 +0,0 @@
#include "video_buffer.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "lock_util.h"
#include "log.h"
bool
video_buffer_init(struct video_buffer *vb) {
if (!(vb->decoding_frame = av_frame_alloc())) {
goto error_0;
}
if (!(vb->rendering_frame = av_frame_alloc())) {
goto error_1;
}
if (!(vb->mutex = SDL_CreateMutex())) {
goto error_2;
}
#ifndef SKIP_FRAMES
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(vb->mutex);
goto error_2;
}
vb->interrupted = false;
#endif
// there is initially no rendering frame, so consider it has already been
// consumed
vb->rendering_frame_consumed = true;
fps_counter_init(&vb->fps_counter);
return true;
error_2:
av_frame_free(&vb->rendering_frame);
error_1:
av_frame_free(&vb->decoding_frame);
error_0:
return false;
}
void
video_buffer_destroy(struct video_buffer *vb) {
#ifndef SKIP_FRAMES
SDL_DestroyCond(vb->rendering_frame_consumed_cond);
#endif
SDL_DestroyMutex(vb->mutex);
av_frame_free(&vb->rendering_frame);
av_frame_free(&vb->decoding_frame);
}
static void
video_buffer_swap_frames(struct video_buffer *vb) {
AVFrame *tmp = vb->decoding_frame;
vb->decoding_frame = vb->rendering_frame;
vb->rendering_frame = tmp;
}
void
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped) {
mutex_lock(vb->mutex);
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then the decoder must wait for the current
// frame to be consumed
while (!vb->rendering_frame_consumed && !vb->interrupted) {
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex);
}
#else
if (vb->fps_counter.started && !vb->rendering_frame_consumed) {
fps_counter_add_skipped_frame(&vb->fps_counter);
}
#endif
video_buffer_swap_frames(vb);
*previous_frame_skipped = !vb->rendering_frame_consumed;
vb->rendering_frame_consumed = false;
mutex_unlock(vb->mutex);
}
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) {
SDL_assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true;
if (vb->fps_counter.started) {
fps_counter_add_rendered_frame(&vb->fps_counter);
}
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then notify the decoder the current frame is
// consumed, so that it may push a new one
cond_signal(vb->rendering_frame_consumed_cond);
#endif
return vb->rendering_frame;
}
void
video_buffer_interrupt(struct video_buffer *vb) {
#ifdef SKIP_FRAMES
(void) vb; // unused
#else
mutex_lock(vb->mutex);
vb->interrupted = true;
mutex_unlock(vb->mutex);
// wake up blocking wait
cond_signal(vb->rendering_frame_consumed_cond);
#endif
}

View File

@@ -1,49 +0,0 @@
#ifndef VIDEO_BUFFER_H
#define VIDEO_BUFFER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "fps_counter.h"
// forward declarations
typedef struct AVFrame AVFrame;
struct video_buffer {
AVFrame *decoding_frame;
AVFrame *rendering_frame;
SDL_mutex *mutex;
#ifndef SKIP_FRAMES
bool interrupted;
SDL_cond *rendering_frame_consumed_cond;
#endif
bool rendering_frame_consumed;
struct fps_counter fps_counter;
};
bool
video_buffer_init(struct video_buffer *vb);
void
video_buffer_destroy(struct video_buffer *vb);
// set the decoded frame as ready for rendering
// this function locks frames->mutex during its execution
// the output flag is set to report whether the previous frame has been skipped
void
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped);
// mark the rendering frame as consumed and return it
// MUST be called with frames->mutex locked!!!
// the caller is expected to render the returned frame to some texture before
// unlocking frames->mutex
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb);
// wake up and avoid any blocking call
void
video_buffer_interrupt(struct video_buffer *vb);
#endif

View File

@@ -1,9 +1,8 @@
#include <assert.h> #include <assert.h>
#include <string.h>
#include "control_event.h" #include "controlevent.h"
static void test_control_event_queue_empty(void) { static void test_control_event_queue_empty() {
struct control_event_queue queue; struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue); SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok); assert(init_ok);
@@ -25,7 +24,7 @@ static void test_control_event_queue_empty(void) {
control_event_queue_destroy(&queue); control_event_queue_destroy(&queue);
} }
static void test_control_event_queue_full(void) { static void test_control_event_queue_full() {
struct control_event_queue queue; struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue); SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok); assert(init_ok);
@@ -43,7 +42,7 @@ static void test_control_event_queue_full(void) {
control_event_queue_destroy(&queue); control_event_queue_destroy(&queue);
} }
static void test_control_event_queue_push_take(void) { static void test_control_event_queue_push_take() {
struct control_event_queue queue; struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue); SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok); assert(init_ok);
@@ -87,7 +86,7 @@ static void test_control_event_queue_push_take(void) {
control_event_queue_destroy(&queue); control_event_queue_destroy(&queue);
} }
int main(void) { int main() {
test_control_event_queue_empty(); test_control_event_queue_empty();
test_control_event_queue_full(); test_control_event_queue_full();
test_control_event_queue_push_take(); test_control_event_queue_push_take();

View File

@@ -1,9 +1,8 @@
#include <assert.h> #include <assert.h>
#include <string.h>
#include "control_event.h" #include "controlevent.h"
static void test_serialize_keycode_event(void) { static void test_serialize_keycode_event() {
struct control_event event = { struct control_event event = {
.type = CONTROL_EVENT_TYPE_KEYCODE, .type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = { .keycode_event = {
@@ -26,7 +25,7 @@ static void test_serialize_keycode_event(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_text_event(void) { static void test_serialize_text_event() {
struct control_event event = { struct control_event event = {
.type = CONTROL_EVENT_TYPE_TEXT, .type = CONTROL_EVENT_TYPE_TEXT,
.text_event = { .text_event = {
@@ -36,37 +35,17 @@ static void test_serialize_text_event(void) {
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf); int size = control_event_serialize(&event, buf);
assert(size == 16); assert(size == 15);
const unsigned char expected[] = { const unsigned char expected[] = {
0x01, // CONTROL_EVENT_TYPE_KEYCODE 0x01, // CONTROL_EVENT_TYPE_KEYCODE
0x00, 0x0d, // text length 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_long_text_event(void) { static void test_serialize_mouse_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(void) {
struct control_event event = { struct control_event event = {
.type = CONTROL_EVENT_TYPE_MOUSE, .type = CONTROL_EVENT_TYPE_MOUSE,
.mouse_event = { .mouse_event = {
@@ -87,19 +66,19 @@ static void test_serialize_mouse_event(void) {
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf); int size = control_event_serialize(&event, buf);
assert(size == 18); assert(size == 14);
const unsigned char expected[] = { const unsigned char expected[] = {
0x02, // CONTROL_EVENT_TYPE_MOUSE 0x02, // CONTROL_EVENT_TYPE_MOUSE
0x00, // AKEY_EVENT_ACTION_DOWN 0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x01, 0x04, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_scroll_event(void) { static void test_serialize_scroll_event() {
struct control_event event = { struct control_event event = {
.type = CONTROL_EVENT_TYPE_SCROLL, .type = CONTROL_EVENT_TYPE_SCROLL,
.scroll_event = { .scroll_event = {
@@ -120,11 +99,11 @@ static void test_serialize_scroll_event(void) {
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf); int size = control_event_serialize(&event, buf);
assert(size == 21); assert(size == 17);
const unsigned char expected[] = { const unsigned char expected[] = {
0x03, // CONTROL_EVENT_TYPE_SCROLL 0x03, // CONTROL_EVENT_TYPE_SCROLL
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x01, 0x04, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1 0x00, 0x00, 0x00, 0x01, // 1
0xFF, 0xFF, 0xFF, 0xFF, // -1 0xFF, 0xFF, 0xFF, 0xFF, // -1
@@ -132,10 +111,10 @@ static void test_serialize_scroll_event(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
int main(void) { int main() {
test_serialize_keycode_event(); test_serialize_keycode_event();
test_serialize_text_event(); test_serialize_text_event();
test_serialize_long_text_event();
test_serialize_mouse_event(); test_serialize_mouse_event();
test_serialize_scroll_event(); test_serialize_scroll_event();
return 0;
} }

View File

@@ -1,9 +1,9 @@
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include "str_util.h" #include "strutil.h"
static void test_xstrncpy_simple(void) { static void test_xstrncpy_simple() {
char s[] = "xxxxxxxxxx"; char s[] = "xxxxxxxxxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s)); size_t w = xstrncpy(s, "abcdef", sizeof(s));
@@ -20,7 +20,7 @@ static void test_xstrncpy_simple(void) {
assert(!strcmp("abcdef", s)); assert(!strcmp("abcdef", s));
} }
static void test_xstrncpy_just_fit(void) { static void test_xstrncpy_just_fit() {
char s[] = "xxxxxx"; char s[] = "xxxxxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s)); size_t w = xstrncpy(s, "abcdef", sizeof(s));
@@ -34,7 +34,7 @@ static void test_xstrncpy_just_fit(void) {
assert(!strcmp("abcdef", s)); assert(!strcmp("abcdef", s));
} }
static void test_xstrncpy_truncated(void) { static void test_xstrncpy_truncated() {
char s[] = "xxx"; char s[] = "xxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s)); size_t w = xstrncpy(s, "abcdef", sizeof(s));
@@ -48,7 +48,7 @@ static void test_xstrncpy_truncated(void) {
assert(!strncmp("abcdef", s, 3)); assert(!strncmp("abcdef", s, 3));
} }
static void test_xstrjoin_simple(void) { static void test_xstrjoin_simple() {
const char *const tokens[] = { "abc", "de", "fghi", NULL }; const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxxxxxxxxx"; char s[] = "xxxxxxxxxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@@ -66,7 +66,7 @@ static void test_xstrjoin_simple(void) {
assert(!strcmp("abc de fghi", s)); assert(!strcmp("abc de fghi", s));
} }
static void test_xstrjoin_just_fit(void) { static void test_xstrjoin_just_fit() {
const char *const tokens[] = { "abc", "de", "fghi", NULL }; const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxxxxxx"; char s[] = "xxxxxxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@@ -81,7 +81,7 @@ static void test_xstrjoin_just_fit(void) {
assert(!strcmp("abc de fghi", s)); assert(!strcmp("abc de fghi", s));
} }
static void test_xstrjoin_truncated_in_token(void) { static void test_xstrjoin_truncated_in_token() {
const char *const tokens[] = { "abc", "de", "fghi", NULL }; const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxx"; char s[] = "xxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@@ -96,7 +96,7 @@ static void test_xstrjoin_truncated_in_token(void) {
assert(!strcmp("abc d", s)); assert(!strcmp("abc d", s));
} }
static void test_xstrjoin_truncated_before_sep(void) { static void test_xstrjoin_truncated_before_sep() {
const char *const tokens[] = { "abc", "de", "fghi", NULL }; const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxx"; char s[] = "xxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@@ -111,7 +111,7 @@ static void test_xstrjoin_truncated_before_sep(void) {
assert(!strcmp("abc de", s)); assert(!strcmp("abc de", s));
} }
static void test_xstrjoin_truncated_after_sep(void) { static void test_xstrjoin_truncated_after_sep() {
const char *const tokens[] = { "abc", "de", "fghi", NULL }; const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxx"; char s[] = "xxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@@ -126,7 +126,7 @@ static void test_xstrjoin_truncated_after_sep(void) {
assert(!strcmp("abc de ", s)); assert(!strcmp("abc de ", s));
} }
int main(void) { int main() {
test_xstrncpy_simple(); test_xstrncpy_simple();
test_xstrncpy_just_fit(); test_xstrncpy_just_fit();
test_xstrncpy_truncated(); test_xstrncpy_truncated();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

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

View File

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

View File

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

View File

@@ -15,4 +15,3 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit # This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true

View File

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

View File

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

View File

@@ -1,8 +1,5 @@
option('build_app', type: 'boolean', value: true, description: 'Build the client') option('build_app', type: 'boolean', value: true, description: 'Build the client')
option('build_server', type: 'boolean', value: true, description: 'Build the server') option('build_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('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('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('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

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

View File

@@ -1,40 +0,0 @@
.PHONY: prepare-win32 prepare-win64 \
prepare-ffmpeg-shared-win32 \
prepare-ffmpeg-dev-win32 \
prepare-ffmpeg-shared-win64 \
prepare-ffmpeg-dev-win64 \
prepare-sdl2 \
prepare-adb
prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1-win32-shared.zip \
e692b18c01745d262c03294b382fd64df68fabe3c66aa4546a3ad3935175cde3 \
ffmpeg-4.1-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1-win32-dev.zip \
34bc5e471fb9160609abd6bc271e361050f3ff7376b1b8a0873cca02b38277c8 \
ffmpeg-4.1-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1-win64-shared.zip \
c4908c97436c946509dc365e421159274fa4b1e66dce6fb5b63d82a6294d5357 \
ffmpeg-4.1-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1-win64-dev.zip \
761ec79aa3dae66698c9791a2f0bb9da8794246f8356cadc741ddc0eabab0471 \
ffmpeg-4.1-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz \
0f9f00d0f2a9a95dfb5cce929718210c3f85432cc2e9d4abade4adcb7f6bb39d \
SDL2-2.0.9
prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \
db78f726d5dc653706dcd15a462ab1b946c643f598df76906c4c1858411c54df \
platform-tools

View File

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

View File

@@ -1,35 +0,0 @@
#!/bin/bash
set -e
# build and test locally
BUILDDIR=build_release
rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
cd "$BUILDDIR"
ninja
ninja test
cd -
# build Windows releases
make -f Makefile.CrossWindows
# the generated server must be the same everywhere
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar
# get version name
TAG=$(git describe --tags --always)
# create release directory
mkdir -p "release-$TAG"
cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar"
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
# generate checksums
cd "release-$TAG"
sha256sum "scrcpy-server-$TAG.jar" \
"scrcpy-win32-$TAG.zip" \
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt
echo "Release generated in release-$TAG/"

View File

@@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 27 targetSdkVersion 27
versionCode 9 versionCode 1
versionName "1.8" versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@@ -10,10 +10,6 @@ if prebuilt_server == ''
install: true, install: true,
install_dir: 'share/scrcpy') install_dir: 'share/scrcpy')
else else
if not prebuilt_server.startswith('/')
# relative path needs some trick
prebuilt_server = meson.source_root() + '/' + prebuilt_server
endif
custom_target('scrcpy-server-prebuilt', custom_target('scrcpy-server-prebuilt',
input: prebuilt_server, input: prebuilt_server,
output: 'scrcpy-server.jar', output: 'scrcpy-server.jar',

View File

@@ -11,9 +11,7 @@ public final class ControlEvent {
public static final int TYPE_SCROLL = 3; public static final int TYPE_SCROLL = 3;
public static final int TYPE_COMMAND = 4; public static final int TYPE_COMMAND = 4;
public static final int COMMAND_BACK_OR_SCREEN_ON = 0; public static final int COMMAND_SCREEN_ON = 0;
public static final int COMMAND_EXPAND_NOTIFICATION_PANEL = 1;
public static final int COMMAND_COLLAPSE_NOTIFICATION_PANEL = 2;
private int type; private int type;
private String text; private String text;

View File

@@ -9,16 +9,16 @@ import java.nio.charset.StandardCharsets;
public class ControlEventReader { public class ControlEventReader {
private static final int KEYCODE_PAYLOAD_LENGTH = 9; private static final int KEYCODE_PAYLOAD_LENGTH = 9;
private static final int MOUSE_PAYLOAD_LENGTH = 17; private static final int MOUSE_PAYLOAD_LENGTH = 13;
private static final int SCROLL_PAYLOAD_LENGTH = 20; private static final int SCROLL_PAYLOAD_LENGTH = 16;
private static final int COMMAND_PAYLOAD_LENGTH = 1; private static final int COMMAND_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300; private static final int MAX_TEXT_LENGTH = 32;
private static final int RAW_BUFFER_SIZE = 1024; private static final int RAW_BUFFER_SIZE = 128;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH]; private final byte[] textBuffer = new byte[MAX_TEXT_LENGTH];
public ControlEventReader() { public ControlEventReader() {
// invariant: the buffer is always in "get" mode // invariant: the buffer is always in "get" mode
@@ -94,7 +94,7 @@ public class ControlEventReader {
if (buffer.remaining() < 1) { if (buffer.remaining() < 1) {
return null; return null;
} }
int len = toUnsigned(buffer.getShort()); int len = toUnsigned(buffer.get());
if (buffer.remaining() < len) { if (buffer.remaining() < len) {
return null; return null;
} }
@@ -132,8 +132,8 @@ public class ControlEventReader {
} }
private static Position readPosition(ByteBuffer buffer) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = toUnsigned(buffer.getShort());
int y = buffer.getInt(); int y = toUnsigned(buffer.getShort());
int screenWidth = toUnsigned(buffer.getShort()); int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort()); int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight); return new Position(x, y, screenWidth, screenHeight);

View File

@@ -1,13 +1,12 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.net.LocalServerSocket;
import android.net.LocalSocket; import android.net.LocalSocket;
import android.net.LocalSocketAddress; import android.net.LocalSocketAddress;
import java.io.Closeable; import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public final class DesktopConnection implements Closeable { public final class DesktopConnection implements Closeable {
@@ -18,14 +17,14 @@ public final class DesktopConnection implements Closeable {
private final LocalSocket socket; private final LocalSocket socket;
private final InputStream inputStream; private final InputStream inputStream;
private final FileDescriptor fd; private final OutputStream outputStream;
private final ControlEventReader reader = new ControlEventReader(); private final ControlEventReader reader = new ControlEventReader();
private DesktopConnection(LocalSocket socket) throws IOException { private DesktopConnection(LocalSocket socket) throws IOException {
this.socket = socket; this.socket = socket;
inputStream = socket.getInputStream(); inputStream = socket.getInputStream();
fd = socket.getFileDescriptor(); outputStream = socket.getOutputStream();
} }
private static LocalSocket connect(String abstractName) throws IOException { private static LocalSocket connect(String abstractName) throws IOException {
@@ -34,24 +33,8 @@ public final class DesktopConnection implements Closeable {
return localSocket; return localSocket;
} }
private static LocalSocket listenAndAccept(String abstractName) throws IOException { public static DesktopConnection open(Device device) throws IOException {
LocalServerSocket localServerSocket = new LocalServerSocket(abstractName); LocalSocket socket = connect(SOCKET_NAME);
try {
return localServerSocket.accept();
} finally {
localServerSocket.close();
}
}
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket socket;
if (tunnelForward) {
socket = listenAndAccept(SOCKET_NAME);
// send one byte so the client may read() to detect a connection error
socket.getOutputStream().write(0);
} else {
socket = connect(SOCKET_NAME);
}
DesktopConnection connection = new DesktopConnection(socket); DesktopConnection connection = new DesktopConnection(socket);
Size videoSize = device.getScreenInfo().getVideoSize(); Size videoSize = device.getScreenInfo().getVideoSize();
@@ -78,11 +61,11 @@ public final class DesktopConnection implements Closeable {
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
IO.writeFully(fd, buffer, 0, buffer.length); outputStream.write(buffer, 0, buffer.length);
} }
public FileDescriptor getFd() { public OutputStream getOutputStream() {
return fd; return outputStream;
} }
public ControlEvent receiveControlEvent() throws IOException { public ControlEvent receiveControlEvent() throws IOException {

View File

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

View File

@@ -1,7 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Point; import android.graphics.Point;
import android.os.SystemClock; import android.os.SystemClock;
@@ -40,6 +39,10 @@ public class EventController {
coords.orientation = 0; coords.orientation = 0;
coords.pressure = 1; coords.pressure = 1;
coords.size = 1; coords.size = 1;
coords.toolMajor = 1;
coords.toolMinor = 1;
coords.touchMajor = 1;
coords.touchMinor = 1;
} }
private void setPointerCoords(Point point) { private void setPointerCoords(Point point) {
@@ -90,12 +93,14 @@ public class EventController {
return injectKeyEvent(action, keycode, 0, metaState); return injectKeyEvent(action, keycode, 0, metaState);
} }
private boolean injectChar(char c) { private boolean injectText(String text) {
String decomposed = KeyComposition.decompose(c); return injectText(text, true);
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c}; }
KeyEvent[] events = charMap.getEvents(chars);
private boolean injectText(String text, boolean decomposeOnFailure) {
KeyEvent[] events = charMap.getEvents(text.toCharArray());
if (events == null) { if (events == null) {
return false; return decomposeOnFailure ? injectDecomposition(text) : false;
} }
for (KeyEvent event : events) { for (KeyEvent event : events) {
if (!injectEvent(event)) { if (!injectEvent(event)) {
@@ -105,9 +110,10 @@ public class EventController {
return true; return true;
} }
private boolean injectText(String text) { private boolean injectDecomposition(String text) {
for (char c : text.toCharArray()) { for (char c : text.toCharArray()) {
if (!injectChar(c)) { String composedText = KeyComposition.decompose(c);
if (composedText == null || !injectText(composedText, false)) {
return false; return false;
} }
} }
@@ -164,21 +170,10 @@ public class EventController {
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER); 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) { private boolean executeCommand(int action) {
switch (action) { switch (action) {
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON: case ControlEvent.COMMAND_SCREEN_ON:
return pressBackOrTurnScreenOn(); return turnScreenOn();
case ControlEvent.COMMAND_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
return true;
case ControlEvent.COMMAND_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
return true;
default: default:
Ln.w("Unsupported command: " + action); Ln.w("Unsupported command: " + action);
} }

View File

@@ -1,40 +0,0 @@
package com.genymobile.scrcpy;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public class IO {
private IO() {
// not instantiable
}
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
// count the remaining bytes manually.
// See <https://github.com/Genymobile/scrcpy/issues/291>.
int remaining = from.remaining();
while (remaining > 0) {
try {
int w = Os.write(fd, from);
if (BuildConfig.DEBUG && w < 0) {
// w should not be negative, since an exception is thrown on error
throw new AssertionError("Os.write() returned a negative value (" + w + ")");
}
remaining -= w;
} catch (ErrnoException e) {
if (e.errno != OsConstants.EINTR) {
throw new IOException(e);
}
}
}
}
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
}
}

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[])} * 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"}). * KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
* <p> * <p>
* See <a href="https://source.android.com/devices/input/key-character-map-files#behaviors">diacritical dead key characters</a>. * See <a href="https://source.android.com/devices/input/key-character-map-files#key-declarations">diacritical dead key characters</a>.
*/ */
public final class KeyComposition { public final class KeyComposition {

View File

@@ -1,13 +1,8 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
public class Options { public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
@@ -24,28 +19,4 @@ public class Options {
public void setBitRate(int bitRate) { public void setBitRate(int bitRate) {
this.bitRate = bitRate; this.bitRate = bitRate;
} }
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
public boolean getSendFrameMeta() {
return sendFrameMeta;
}
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
} }

View File

@@ -3,15 +3,14 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.MediaMuxer;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -23,26 +22,21 @@ public class ScreenEncoder implements Device.RotationListener {
private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames private static final int REPEAT_FRAME_DELAY = 6; // repeat after 6 frames
private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000; private static final int MICROSECONDS_IN_ONE_SECOND = 1_000_000;
private static final int NO_PTS = -1;
private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private int bitRate; private int bitRate;
private int frameRate; private int frameRate;
private int iFrameInterval; private int iFrameInterval;
private boolean sendFrameMeta;
private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) { public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.frameRate = frameRate; this.frameRate = frameRate;
this.iFrameInterval = iFrameInterval; this.iFrameInterval = iFrameInterval;
} }
public ScreenEncoder(boolean sendFrameMeta, int bitRate) { public ScreenEncoder(int bitRate) {
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL); this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@@ -54,25 +48,23 @@ public class ScreenEncoder implements Device.RotationListener {
return rotationChanged.getAndSet(false); return rotationChanged.getAndSet(false);
} }
public void streamScreen(Device device, FileDescriptor fd) throws IOException { public void streamScreen(Device device, OutputStream outputStream) throws IOException {
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
device.setRotationListener(this); device.setRotationListener(this);
IBinder d = SurfaceControl.getBuiltInDisplay(0);
SurfaceControl.setDisplayPowerMode(d, SurfaceControl.POWER_MODE_OFF);
boolean alive; boolean alive;
try { try {
do { do {
MediaCodec codec = createCodec(); MediaCodec codec = createCodec();
IBinder display = createDisplay(); IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect(); Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
setSize(format, videoRect.width(), videoRect.height()); setSize(format, videoRect.width(), videoRect.height());
configure(codec, format); configure(codec, format);
Surface surface = codec.createInputSurface(); Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, contentRect, videoRect); setDisplaySurface(display, surface, deviceRect, videoRect);
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, outputStream);
} finally { } finally {
codec.stop(); codec.stop();
destroyDisplay(display); destroyDisplay(display);
@@ -85,10 +77,11 @@ public class ScreenEncoder implements Device.RotationListener {
} }
} }
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { private boolean encode(MediaCodec codec, OutputStream outputStream) throws IOException {
@SuppressWarnings("checkstyle:MagicNumber")
byte[] buf = new byte[bitRate / 8]; // may contain up to 1 second of video
boolean eof = false; boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) { while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
@@ -98,13 +91,15 @@ public class ScreenEncoder implements Device.RotationListener {
break; break;
} }
if (outputBufferId >= 0) { if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
while (outputBuffer.hasRemaining()) {
if (sendFrameMeta) { int remaining = outputBuffer.remaining();
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); int len = Math.min(buf.length, remaining);
// the outputBuffer is probably direct (it has no underlying array), and LocalSocket does not expose channels,
// so we must copy the data locally to write them manually to the output stream
outputBuffer.get(buf, 0, len);
outputStream.write(buf, 0, len);
} }
IO.writeFully(fd, codecBuffer);
} }
} finally { } finally {
if (outputBufferId >= 0) { if (outputBufferId >= 0) {
@@ -116,25 +111,6 @@ public class ScreenEncoder implements Device.RotationListener {
return !eof; return !eof;
} }
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = NO_PTS; // non-media data packet
} else {
if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodec createCodec() throws IOException { private static MediaCodec createCodec() throws IOException {
return MediaCodec.createEncoderByType("video/avc"); return MediaCodec.createEncoderByType("video/avc");
} }
@@ -152,7 +128,7 @@ public class ScreenEncoder implements Device.RotationListener {
} }
private static IBinder createDisplay() { private static IBinder createDisplay() {
return SurfaceControl.createDisplay("scrcpy", true); return SurfaceControl.createDisplay("scrcpy", false);
} }
private static void configure(MediaCodec codec, MediaFormat format) { private static void configure(MediaCodec codec, MediaFormat format) {

View File

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

View File

@@ -1,31 +1,24 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
public final class Server { public final class Server {
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private Server() { private Server() {
// not instantiable // not instantiable
} }
private static void scrcpy(Options options) throws IOException { private static void scrcpy(Options options) throws IOException {
final Device device = new Device(options); final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device)) {
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
// asynchronous // asynchronous
startEventController(device, connection); startEventController(device, connection);
try { try {
// synchronous // synchronous
screenEncoder.streamScreen(device, connection.getFd()); screenEncoder.streamScreen(device, connection.getOutputStream());
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");
@@ -49,54 +42,22 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber") @SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) { private static Options createOptions(String... args) {
if (args.length != 5)
throw new IllegalArgumentException("Expecting 5 parameters");
Options options = new Options(); Options options = new Options();
if (args.length < 1) {
return options;
}
int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8 int maxSize = Integer.parseInt(args[0]) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
if (args.length < 2) {
return options;
}
int bitRate = Integer.parseInt(args[1]); int bitRate = Integer.parseInt(args[1]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[3]);
options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
options.setSendFrameMeta(sendFrameMeta);
return options; return options;
} }
private static Rect parseCrop(String crop) {
if ("-".equals(crop)) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Cannot unlink server", e);
}
}
public static void main(String... args) throws Exception { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override @Override
@@ -105,7 +66,6 @@ public final class Server {
} }
}); });
unlinkSelf();
Options options = createOptions(args); Options options = createOptions(args);
scrcpy(options); scrcpy(options);
} }

View File

@@ -14,7 +14,6 @@ public final class ServiceManager {
private DisplayManager displayManager; private DisplayManager displayManager;
private InputManager inputManager; private InputManager inputManager;
private PowerManager powerManager; private PowerManager powerManager;
private StatusBarManager statusBarManager;
public ServiceManager() { public ServiceManager() {
try { try {
@@ -61,11 +60,4 @@ public final class ServiceManager {
} }
return powerManager; return powerManager;
} }
public StatusBarManager getStatusBarManager() {
if (statusBarManager == null) {
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
}
return statusBarManager;
}
} }

View File

@@ -1,41 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import android.annotation.SuppressLint;
import android.os.IInterface;
import android.view.InputEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class StatusBarManager {
private final IInterface manager;
private final Method expandNotificationsPanelMethod;
private final Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) {
this.manager = manager;
try {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
}
public void expandNotificationsPanel() {
try {
expandNotificationsPanelMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
public void collapsePanels() {
try {
collapsePanelsMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -10,11 +10,6 @@ public final class SurfaceControl {
private static final Class<?> CLASS; private static final Class<?> CLASS;
public static final int POWER_MODE_OFF = 0;
public static final int POWER_MODE_DOZE = 1;
public static final int POWER_MODE_NORMAL = 2;
public static final int POWER_MODE_DOZE_SUSPEND = 3;
static { static {
try { try {
CLASS = Class.forName("android.view.SurfaceControl"); CLASS = Class.forName("android.view.SurfaceControl");
@@ -76,22 +71,6 @@ public final class SurfaceControl {
} }
} }
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
try {
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
try {
CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void destroyDisplay(IBinder displayToken) { public static void destroyDisplay(IBinder displayToken) {
try { try {
CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);

View File

@@ -11,8 +11,6 @@ import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlEventReaderTest { public class ControlEventReaderTest {
@@ -45,8 +43,8 @@ public class ControlEventReaderTest {
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT); dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8); byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length); dos.writeByte(text.length);
dos.write(text); dos.write("testé".getBytes(StandardCharsets.UTF_8));
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet)); reader.readFrom(new ByteArrayInputStream(packet));
@@ -56,26 +54,6 @@ public class ControlEventReaderTest {
Assert.assertEquals("testé", event.getText()); 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 @Test
public void testParseMouseEvent() throws IOException { public void testParseMouseEvent() throws IOException {
ControlEventReader reader = new ControlEventReader(); ControlEventReader reader = new ControlEventReader();