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
79 changed files with 885 additions and 2923 deletions

245
BUILD.md
View File

@@ -1,245 +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 \
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
```
### 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.4.jar`][direct-scrcpy-server]
_(SHA-256: 1ff7a72fcfe81dadccfab9d6f86c971cd7c7f38f17196748fe05480e301b443d)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.4/scrcpy-server-v1.4.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
`shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
[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 `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
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
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].
[hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view
[wrappers]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/aidl/android/view
### 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
stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`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
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
[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
new video stream is produced.
@@ -105,8 +105,8 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
[rotation]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
[repeat]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
@@ -124,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
[`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
[`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
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@@ -195,8 +195,8 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one.
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/decoder.c
[frames]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/frames.h
### Controller
@@ -211,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
client.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
[controller]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/controlevent.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/inputmanager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/convert.h
### UI and event loop
@@ -225,9 +225,9 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/scrcpy.c#L38
[screen]: https://github.com/Genymobile/scrcpy/blob/079d750d41b7849eb1b9caaa6151ef2429581584/app/src/screen.h
## Hack

70
FAQ.md
View File

@@ -1,70 +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
### I get a black screen for some applications like Silence
This is expected, they requested to [protect] the screen.
In [Silence], you can disable it in settings → Privacy → Screen security.
[protect]: https://developer.android.com/reference/android/view/Display#FLAG_SECURE
[silence]: https://f-droid.org/en/packages/org.smssecure.smssecure/
See [issue 36].
[issue 36]: https://github.com/Genymobile/scrcpy/issues/36
### Mouse clicks do not work
On some devices, you may need to enable an option to allow [simulating input].
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
### Mouse clicks at wrong location
On MacOS, with HiDPI support and multiple screens, input location are wrongly
scaled. See [issue 15].
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
A workaround is to build with HiDPI support disabled:
```bash
meson x --buildtype release -Dhidpi_support=false
```
However, the video will be displayed at lower resolution.
### KWin compositor crashes
On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
As a workaround, [disable "Block compositing"][kwin].
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613

62
Makefile 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 "Release created in $(DIST)/."
clean:
$(GRADLE) clean
rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \
"$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)"
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dbuild_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
-$(MAKE) -C prebuilt-deps prepare-win32
build-win32: prepare-deps-win32
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
meson "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32
[ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64:
-$(MAKE) -C prebuilt-deps prepare-win64
build-win64: prepare-deps-win64
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
meson "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64
[ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.0.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0.2-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.0.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.0.2-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

348
README.md
View File

@@ -1,8 +1,8 @@
# scrcpy (v1.4)
# scrcpy
This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
It works on _GNU/Linux_, _Windows_ and _MacOS_.
USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
and _Mac OS_.
![screenshot](assets/screenshot-debian-600.jpg)
@@ -11,238 +11,178 @@ It works on _GNU/Linux_, _Windows_ and _MacOS_.
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).
[adb]: https://developer.android.com/studio/command-line/adb.html
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
The client requires _FFmpeg_ and _LibSDL2_.
## 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,
it's not that hard.
For Windows, for simplicity, a prebuilt package with all the dependencies
(including `adb`) is available: TODO.
For Arch Linux, two [AUR] packages have been created by users:
Instead, you may want to build it manually. You need [MSYS2] to build the
project. From an MSYS2 terminal, install the required packages:
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
[MSYS2]: http://www.msys2.org/
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
# build dependencies
pacman -S mingw-w64-x86_64-make \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-meson \
zip
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
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
(including `adb`) are available:
- [`scrcpy-win32-v1.4.zip`][direct-win32]
_(SHA-256: 1f72fa520980727e8943b7214b64c66b00b9b5267f7cffefb64fa37c3ca803cf)_
- [`scrcpy-win64-v1.4.zip`][direct-win64]
_(SHA-256: 382f02bd8ed3db2cc7ab15aabdb83674744993b936d602b01e6959a150584a79)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.4/scrcpy-win32-v1.4.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.4/scrcpy-win64-v1.4.zip
You can also [build the app manually][BUILD].
### Mac OS
The application is available in [Homebrew]. Just install it:
Use [Homebrew] to install the packages:
[Homebrew]: https://brew.sh/
```bash
brew install scrcpy
```
# runtime dependencies
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
brew cask install android-platform-tools
```
Java (>= 7) is not available in Homebrew, so if you plan to build the server,
install it manually and make it available from the `PATH`:
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
Plug an Android device, and execute:
_At runtime, `adb` must be accessible from your `PATH`._
```bash
scrcpy
```
If everything is ok, just plug an Android device, and execute:
scrcpy
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
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.mp4
```
"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
scrcpy -m 1024
If several devices are listed in `adb devices`, you must specify the _serial_:
```bash
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`.
### 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).
### 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
scrcpy -s 0123456789abcdef
## Shortcuts
| Action | Shortcut |
| -------------------------------------- |:---------------------------- |
| switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| click on `APP_SWITCH` | `Ctrl`+`s` |
| click on `MENU` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`` _(up)_ (`Cmd`+`↑` on MacOS) |
| click on `VOLUME_DOWN` | `Ctrl`+`` _(down)_ (`Cmd`+`↓` on MacOS) |
| click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click²_ |
| paste computer clipboard to device | `Ctrl`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| install APK from computer | drag & drop APK file |
| push file to `/sdcard/` | drag & drop non-APK file |
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
| Action | Shortcut |
| ------------------------------------- | -------------:|
| switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` |
| click on `HOME` | `Ctrl`+`h` |
| click on `BACK` | `Ctrl`+`b` |
| click on `APP_SWITCH` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_DOWN` | `Ctrl`+`-` |
| click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click_ |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
## Why _scrcpy_?
@@ -255,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
## How to build?
See [BUILD].
[BUILD]: BUILD.md
## Common issues
See the [FAQ](FAQ.md).
## Developers
Read the [developers page].
@@ -289,11 +217,3 @@ Read the [developers page].
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## Articles
- [Introducing scrcpy][article-intro]
- [Scrcpy now works wirelessly][article-tcpip]
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
#ifndef BUFFER_UTIL_H
#define BUFFER_UTIL_H
#include <SDL2/SDL_stdinc.h>
static inline void buffer_write16be(Uint8 *buf, Uint16 value) {
buf[0] = value >> 8;
buf[1] = value;
}
static inline void buffer_write32be(Uint8 *buf, Uint32 value) {
buf[0] = value >> 24;
buf[1] = value >> 16;
buf[2] = value >> 8;
buf[3] = value;
}
static inline Uint32 buffer_read32be(Uint8 *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
}
static inline Uint64 buffer_read64be(Uint8 *buf) {
Uint32 msb = buffer_read32be(buf);
Uint32 lsb = buffer_read32be(&buf[4]);
return ((Uint64) msb << 32) | lsb;
}
#endif

View File

@@ -4,13 +4,13 @@
#include <stdlib.h>
#include <string.h>
#include "common.h"
#include "log.h"
#include "str_util.h"
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
static const char *adb_command;
static inline const char *get_adb_command(void) {
static inline const char *get_adb_command() {
if (!adb_command) {
adb_command = getenv("ADB");
if (!adb_command)
@@ -19,25 +19,9 @@ static inline const char *get_adb_command(void) {
return adb_command;
}
static void 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];
int i;
process_t process;
cmd[0] = get_adb_command();
if (serial) {
cmd[1] = "-s";
@@ -49,12 +33,7 @@ process_t adb_execute(const char *serial, const char *const adb_cmd[], int len)
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd[0], cmd, &process);
if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r);
return PROCESS_NONE;
}
return process;
return cmd_execute(cmd[0], cmd);
}
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) {
@@ -66,13 +45,6 @@ process_t adb_forward(const char *serial, uint16_t local_port, const char *devic
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_forward_remove(const char *serial, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port);
const char *const adb_cmd[] = {"forward", "--remove", local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
@@ -90,49 +62,8 @@ 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) {
#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};
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) remote);
free((void *) local);
#endif
return proc;
}
process_t adb_install(const char *serial, const char *local) {
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
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;
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_remove_path(const char *serial, const char *path) {

View File

@@ -7,11 +7,12 @@
// <https://stackoverflow.com/a/44383330/1987178>
#ifdef _WIN32
# define PRIexitcode "lu"
# ifdef _WIN64
# define PRIsizet PRIu64
# define PRIexitcode "lu"
# else
# define PRIsizet PRIu32
# define PRIexitcode "u"
# endif
#else
# define PRIsizet "zu"
@@ -32,23 +33,15 @@
#endif
# define NO_EXIT_CODE -1
enum process_result {
PROCESS_SUCCESS,
PROCESS_ERROR_GENERIC,
PROCESS_ERROR_MISSING_BINARY,
};
enum process_result cmd_execute(const char *path, const char *const argv[], process_t *process);
process_t cmd_execute(const char *path, const char *const argv[]);
SDL_bool cmd_terminate(process_t pid);
SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
process_t adb_forward_remove(const char *serial, uint16_t local_port);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t adb_push(const char *serial, const char *local, const char *remote);
process_t adb_install(const char *serial, const char *local);
process_t adb_remove_path(const char *serial, const char *path);
// convenience function to wait for a successful process execution

View File

@@ -3,7 +3,6 @@
#include <SDL2/SDL_stdinc.h>
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)

View File

@@ -1,17 +1,27 @@
#include "control_event.h"
#include "controlevent.h"
#include <SDL2/SDL_stdinc.h>
#include <string.h>
#include "buffer_util.h"
#include "lock_util.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) {
buffer_write16be(&buf[0], position->point.x);
buffer_write16be(&buf[2], position->point.y);
buffer_write16be(&buf[4], position->screen_size.width);
buffer_write16be(&buf[6], position->screen_size.height);
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) {
@@ -19,29 +29,28 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
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);
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) {
// injecting a text takes time, so limit the text length
len = TEXT_MAX_LENGTH;
}
buffer_write16be(&buf[1], (Uint16) len);
memcpy(&buf[3], event->text_event.text, len);
return 3 + len;
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;
buffer_write32be(&buf[2], event->mouse_event.buttons);
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);
buffer_write32be(&buf[9], (Uint32) event->scroll_event.hscroll);
buffer_write32be(&buf[13], (Uint32) event->scroll_event.vscroll);
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;
@@ -52,12 +61,6 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
}
}
void control_event_destroy(struct control_event *event) {
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
SDL_free(event->text_event.text);
}
}
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
return queue->head == queue->tail;
}
@@ -74,11 +77,7 @@ SDL_bool control_event_queue_init(struct control_event_queue *queue) {
}
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;
}
// nothing to do in the current implementation
}
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {

View File

@@ -9,8 +9,8 @@
#include "common.h"
#define CONTROL_EVENT_QUEUE_SIZE 64
#define TEXT_MAX_LENGTH 300
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
#define SERIALIZED_EVENT_MAX_SIZE 33
#define TEXT_MAX_LENGTH 31
enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE,
@@ -20,7 +20,7 @@ enum control_event_type {
CONTROL_EVENT_TYPE_COMMAND,
};
#define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0
#define CONTROL_EVENT_COMMAND_SCREEN_ON 0
struct control_event {
enum control_event_type type;
@@ -31,7 +31,7 @@ struct control_event {
enum android_metastate metastate;
} keycode_event;
struct {
char *text; // owned, to be freed by SDL_free()
char text[TEXT_MAX_LENGTH + 1]; // nul-terminated string
} text_event;
struct {
enum android_motionevent_action action;
@@ -68,6 +68,4 @@ SDL_bool control_event_queue_is_full(const struct control_event_queue *queue);
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event);
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event);
void control_event_destroy(struct control_event *event);
#endif

View File

@@ -1,8 +1,6 @@
#include "controller.h"
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "lock_util.h"
#include "lockutil.h"
#include "log.h"
SDL_bool controller_init(struct controller *controller, socket_t video_socket) {
@@ -56,28 +54,25 @@ static SDL_bool process_event(struct controller *controller, const struct contro
static int run_controller(void *data) {
struct controller *controller = data;
mutex_lock(controller->mutex);
for (;;) {
mutex_lock(controller->mutex);
while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
cond_wait(controller->event_cond, controller->mutex);
}
if (controller->stopped) {
// stop immediately, do not process further events
mutex_unlock(controller->mutex);
break;
}
struct control_event event;
SDL_bool non_empty = control_event_queue_take(&controller->queue, &event);
SDL_assert(non_empty);
mutex_unlock(controller->mutex);
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
break;
while (control_event_queue_take(&controller->queue, &event)) {
if (!process_event(controller, &event)) {
LOGD("Cannot write event to socket");
goto end;
}
}
}
end:
mutex_unlock(controller->mutex);
return 0;
}

View File

@@ -1,7 +1,7 @@
#ifndef CONTROL_H
#define CONTROL_H
#include "control_event.h"
#include "controlevent.h"
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>

View File

@@ -70,10 +70,9 @@ static enum android_metastate convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate);
}
static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to, Uint16 mod) {
static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to) {
switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB);
@@ -86,39 +85,6 @@ static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to, Uint
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
}
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return SDL_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;
}
}
@@ -159,18 +125,18 @@ SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
return SDL_FALSE;
}
Uint16 mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) {
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode)) {
return SDL_FALSE;
}
to->keycode_event.metastate = convert_meta_state(mod);
to->keycode_event.metastate = convert_meta_state(from->keysym.mod);
return SDL_TRUE;
}
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) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
@@ -180,21 +146,30 @@ SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
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;
}
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct hidpi_scale *hidpi_scale,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->mouse_event.buttons = convert_mouse_buttons(from->state);
to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
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;
}
@@ -207,10 +182,7 @@ SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
to->scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->scroll_event.hscroll = -mul * from->x;
to->scroll_event.hscroll = mul * from->x;
to->scroll_event.vscroll = mul * from->y;
return SDL_TRUE;

View File

@@ -3,7 +3,10 @@
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_events.h>
#include "control_event.h"
#include "common.h"
#include "controlevent.h"
#include "hidpi.h"
struct complete_mouse_motion_event {
SDL_MouseMotionEvent *mouse_motion_event;
@@ -19,12 +22,14 @@ SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
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);
// the video size may be different from the real device size, so we need the size
// to which the absolute position apply, to scale it accordingly
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct hidpi_scale *hidpi_scale,
struct control_event *to);
// on Android, a scroll event requires the current mouse position

View File

@@ -1,119 +1,20 @@
#include "decoder.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 "config.h"
#include "buffer_util.h"
#include "events.h"
#include "frames.h"
#include "lock_util.h"
#include "lockutil.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 SDL_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 SDL_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 SDL_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 decoder *decoder = opaque;
struct receiver_state *state = &decoder->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 ret = net_recv_all(decoder->video_socket, header, HEADER_SIZE);
if (ret <= 0) {
return ret;
}
// no partial read (net_recv_all())
SDL_assert_release(ret == 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 -1;
}
}
SDL_assert(state->remaining);
if (buf_size > state->remaining)
buf_size = state->remaining;
ssize_t ret = net_recv(decoder->video_socket, buf, buf_size);
if (ret <= 0) {
return ret;
}
SDL_assert(state->remaining >= ret);
state->remaining -= ret;
return ret;
}
static int read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
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);
}
@@ -139,50 +40,47 @@ static void notify_stopped(void) {
static int run_decoder(void *data) {
struct decoder *decoder = data;
int ret = 0;
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto run_end;
return -1;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGC("Could not allocate decoder context");
goto run_end;
return -1;
}
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Could not open H.264 codec");
ret = -1;
goto run_finally_free_codec_ctx;
}
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
ret = -1;
goto run_finally_close_codec;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
ret = -1;
goto run_finally_free_format_ctx;
}
// initialize the receiver state
decoder->receiver_state.frame_meta_queue = NULL;
decoder->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets
int (*read_packet)(void *, uint8_t *, int) =
decoder->recorder ? read_packet_with_meta : read_raw_packet;
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder,
read_packet, NULL, NULL);
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, 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);
ret = -1;
goto run_finally_free_format_ctx;
}
@@ -190,45 +88,24 @@ static int run_decoder(void *data) {
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
ret = -1;
goto run_finally_free_avio_ctx;
}
if (decoder->recorder &&
!recorder_open(decoder->recorder, codec)) {
LOGE("Could not open recorder");
goto run_finally_close_input;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet)) {
while (!av_read_frame(format_ctx, &packet) && !avio_ctx->eof_reached) {
// the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0)
int ret;
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
goto run_quit;
}
ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame);
if (!ret) {
// a frame was received
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
av_packet_unref(&packet);
goto run_quit;
}
#else
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 37, 0)
while (packet.size > 0) {
int got_picture;
int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet);
if (len < 0) {
LOGE("Could not decode video packet: %d", len);
av_packet_unref(&packet);
goto run_quit;
}
if (got_picture) {
@@ -237,38 +114,24 @@ static int run_decoder(void *data) {
packet.size -= len;
packet.data += len;
}
#else
int ret;
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
goto run_quit;
}
if ((ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame)) < 0) {
LOGE("Could not receive video frame: %d", ret);
goto run_quit;
}
push_frame(decoder);
#endif
if (decoder->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(&decoder->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(decoder->recorder, &packet)) {
LOGE("Could not write frame to output file");
av_packet_unref(&packet);
goto run_quit;
}
}
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
break;
}
}
LOGD("End of frames");
run_quit:
if (decoder->recorder) {
recorder_close(decoder->recorder);
}
run_finally_close_input:
avformat_close_input(&format_ctx);
run_finally_free_avio_ctx:
av_freep(&avio_ctx);
@@ -279,15 +142,12 @@ run_finally_close_codec:
run_finally_free_codec_ctx:
avcodec_free_context(&codec_ctx);
notify_stopped();
run_end:
return 0;
return ret;
}
void decoder_init(struct decoder *decoder, struct frames *frames,
socket_t video_socket, struct recorder *recorder) {
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) {
decoder->frames = frames;
decoder->video_socket = video_socket;
decoder->recorder = recorder;
}
SDL_bool decoder_start(struct decoder *decoder) {
@@ -298,6 +158,7 @@ SDL_bool decoder_start(struct decoder *decoder) {
LOGC("Could not start decoder thread");
return SDL_FALSE;
}
return SDL_TRUE;
}

View File

@@ -4,31 +4,18 @@
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h>
#include "common.h"
#include "net.h"
struct frames;
struct frame_meta {
uint64_t pts;
struct frame_meta *next;
};
struct decoder {
struct frames *frames;
socket_t video_socket;
SDL_Thread *thread;
SDL_mutex *mutex;
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 decoder_init(struct decoder *decoder, struct frames *frames,
socket_t video_socket, struct recorder *recoder);
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket);
SDL_bool decoder_start(struct decoder *decoder);
void decoder_stop(struct decoder *decoder);
void decoder_join(struct decoder *decoder);

View File

@@ -7,7 +7,6 @@
#include "net.h"
#define DEVICE_NAME_FIELD_LENGTH 64
#define DEVICE_SDCARD_PATH "/sdcard/"
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
SDL_bool device_read_info(socket_t device_socket, char *name, struct size *frame_size);

View File

@@ -1,231 +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 SDL_bool request_queue_is_empty(const struct request_queue *queue) {
return queue->head == queue->tail;
}
static SDL_bool request_queue_is_full(const struct request_queue *queue) {
return (queue->head + 1) % REQUEST_QUEUE_SIZE == queue->tail;
}
static SDL_bool request_queue_init(struct request_queue *queue) {
queue->head = 0;
queue->tail = 0;
return SDL_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 SDL_bool request_queue_push(struct request_queue *queue, struct request *req) {
if (request_queue_is_full(queue)) {
return SDL_FALSE;
}
queue->reqs[queue->head] = req;
queue->head = (queue->head + 1) % REQUEST_QUEUE_SIZE;
return SDL_TRUE;
}
static SDL_bool request_queue_take(struct request_queue *queue, struct request **req) {
if (request_queue_is_empty(queue)) {
return SDL_FALSE;
}
// transfer ownership
*req = queue->reqs[queue->tail];
queue->tail = (queue->tail + 1) % REQUEST_QUEUE_SIZE;
return SDL_TRUE;
}
SDL_bool file_handler_init(struct file_handler *file_handler, const char *serial) {
if (!request_queue_init(&file_handler->queue)) {
return SDL_FALSE;
}
if (!(file_handler->mutex = SDL_CreateMutex())) {
return SDL_FALSE;
}
if (!(file_handler->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(file_handler->mutex);
return SDL_FALSE;
}
if (serial) {
file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) {
LOGW("Cannot strdup serial");
SDL_DestroyMutex(file_handler->mutex);
return SDL_FALSE;
}
} else {
file_handler->serial = NULL;
}
// lazy initialization
file_handler->initialized = SDL_FALSE;
file_handler->stopped = SDL_FALSE;
file_handler->current_process = PROCESS_NONE;
return SDL_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);
}
SDL_bool file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
const char *file) {
SDL_bool res;
// start file_handler if it's used for the first time
if (!file_handler->initialized) {
if (!file_handler_start(file_handler)) {
return SDL_FALSE;
}
file_handler->initialized = SDL_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 SDL_FALSE;
}
mutex_lock(file_handler->mutex);
SDL_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;
SDL_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;
}
SDL_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 SDL_FALSE;
}
return SDL_TRUE;
}
void file_handler_stop(struct file_handler *file_handler) {
mutex_lock(file_handler->mutex);
file_handler->stopped = SDL_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,44 +0,0 @@
#ifndef FILE_HANDLER_H
#define FILE_HANDLER_H
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.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;
SDL_bool stopped;
SDL_bool initialized;
process_t current_process;
struct request_queue queue;
};
SDL_bool file_handler_init(struct file_handler *file_handler, const char *serial);
void file_handler_destroy(struct file_handler *file_handler);
SDL_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);
SDL_bool file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
const char *file);
#endif

View File

@@ -1,4 +1,4 @@
#include "fps_counter.h"
#include "fpscounter.h"
#include <SDL2/SDL_timer.h>

View File

@@ -6,7 +6,7 @@
#include <libavformat/avformat.h>
#include "config.h"
#include "lock_util.h"
#include "lockutil.h"
#include "log.h"
SDL_bool frames_init(struct frames *frames) {

View File

@@ -5,7 +5,7 @@
#include <SDL2/SDL_stdinc.h>
#include "config.h"
#include "fps_counter.h"
#include "fpscounter.h"
// forward declarations
typedef struct AVFrame AVFrame;

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,317 +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);
SDL_assert_release(x >= 0 && x < 0x10000 && y >= 0 && y < 0x10000);
return (struct point) {
.x = (Uint16) x,
.y = (Uint16) 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 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);
}
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) {
SDL_bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
SDL_bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
SDL_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_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (shift) {
// currently, there is no shortcut involving SHIFT
return;
}
SDL_Keycode keycode = event->keysym.sym;
int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP;
SDL_bool repeat = event->repeat;
switch (keycode) {
case SDLK_h:
if (ctrl && !meta && !repeat) {
action_home(input_manager->controller, action);
}
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
if (ctrl && !meta && !repeat) {
action_back(input_manager->controller, action);
}
return;
case SDLK_s:
if (ctrl && !meta && !repeat) {
action_app_switch(input_manager->controller, action);
}
return;
case SDLK_m:
if (ctrl && !meta && !repeat) {
action_menu(input_manager->controller, action);
}
return;
case SDLK_p:
if (ctrl && !meta && !repeat) {
action_power(input_manager->controller, action);
}
return;
case SDLK_DOWN:
#ifdef __APPLE__
if (!ctrl && meta) {
#else
if (ctrl && !meta) {
#endif
// forward repeated events
action_volume_down(input_manager->controller, action);
}
return;
case SDLK_UP:
#ifdef __APPLE__
if (!ctrl && meta) {
#else
if (ctrl && !meta) {
#endif
// forward repeated events
action_volume_up(input_manager->controller, action);
}
return;
case SDLK_v:
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
clipboard_paste(input_manager->controller);
}
return;
case SDLK_f:
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
screen_switch_fullscreen(input_manager->screen);
}
return;
case SDLK_x:
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_fit(input_manager->screen);
}
return;
case SDLK_g:
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_pixel_perfect(input_manager->screen);
}
return;
case SDLK_i:
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
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 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");
}
}
}
void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event) {
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
return;
}
if (event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
SDL_bool outside_device_screen =
event->x < 0 || event->x >= input_manager->screen->frame_size.width ||
event->y < 0 || event->y >= input_manager->screen->frame_size.height;
if (outside_device_screen) {
screen_resize_to_fit(input_manager->screen);
return;
}
// otherwise, send the click event to the device
}
}
struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
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");
}
}
}

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

View File

@@ -3,7 +3,7 @@
#include "common.h"
#include "controller.h"
#include "fps_counter.h"
#include "fpscounter.h"
#include "frames.h"
#include "screen.h"

View File

@@ -1,4 +1,3 @@
#include <lock_util.h>
#include <stdlib.h>
#include <SDL2/SDL_mutex.h>

View File

@@ -10,12 +10,8 @@
struct args {
const char *serial;
const char *crop;
const char *record_filename;
SDL_bool fullscreen;
SDL_bool help;
SDL_bool version;
SDL_bool show_touches;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
@@ -32,15 +28,6 @@ static void usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n"
"\n"
" -c, --crop width:height:x:y\n"
" Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n"
"\n"
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
" -h, --help\n"
" Print this help.\n"
"\n"
@@ -54,17 +41,10 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n"
" Default is %d.\n"
"\n"
" -r, --record file.mp4\n"
" Record screen to file.\n"
"\n"
" -s, --serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
@@ -77,45 +57,33 @@ static void usage(const char *arg0) {
" resize window to 1:1 (pixel-perfect)\n"
"\n"
" Ctrl+x\n"
" Double-click on black borders\n"
" resize window to remove black borders\n"
"\n"
" Ctrl+h\n"
" Home\n"
" Middle-click\n"
" click on HOME\n"
"\n"
" Ctrl+b\n"
" Ctrl+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n"
"\n"
" Ctrl+s\n"
" Ctrl+m\n"
" click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" click on MENU\n"
"\n"
" Ctrl+Up\n"
" Ctrl+'+'\n"
" click on VOLUME_UP\n"
"\n"
" Ctrl+Down\n"
" Ctrl+'-'\n"
" click on VOLUME_DOWN\n"
"\n"
" Ctrl+p\n"
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" Right-click\n"
" turn screen on\n"
"\n"
" Ctrl+v\n"
" paste computer clipboard to device\n"
"\n"
" Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
" install APK from computer\n"
"\n",
arg0,
DEFAULT_BIT_RATE,
@@ -206,57 +174,47 @@ static SDL_bool parse_port(char *optarg, Uint16 *port) {
static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'},
{"record", required_argument, NULL, 'r'},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
{"bit-rate", required_argument, NULL, 'b'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'},
{"serial", required_argument, NULL, 's'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "b:c:fhm:p:r:s:tv", long_options, NULL)) != -1) {
while ((c = getopt_long(argc, argv, "b:hm:p:s:v", long_options, NULL)) != -1) {
switch (c) {
case 'b':
case 'b': {
if (!parse_bit_rate(optarg, &args->bit_rate)) {
return SDL_FALSE;
}
break;
case 'c':
args->crop = optarg;
break;
case 'f':
args->fullscreen = SDL_TRUE;
break;
case 'h':
}
case 'h': {
args->help = SDL_TRUE;
break;
case 'm':
}
case 'm': {
if (!parse_max_size(optarg, &args->max_size)) {
return SDL_FALSE;
}
break;
case 'p':
}
case 'p': {
if (!parse_port(optarg, &args->port)) {
return SDL_FALSE;
}
break;
case 'r':
args->record_filename = optarg;
break;
case 's':
}
case 's': {
args->serial = optarg;
break;
case 't':
args->show_touches = SDL_TRUE;
break;
case 'v':
}
case 'v': {
args->version = SDL_TRUE;
break;
}
default:
// getopt prints the error message on stderr
return SDL_FALSE;
@@ -272,19 +230,10 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
}
int main(int argc, char *argv[]) {
#ifdef __WINDOWS__
// disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL);
setbuf(stderr, NULL);
#endif
struct args args = {
.serial = NULL,
.crop = NULL,
.record_filename = NULL,
.help = SDL_FALSE,
.version = SDL_FALSE,
.show_touches = SDL_FALSE,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
@@ -303,9 +252,7 @@ int main(int argc, char *argv[]) {
return 0;
}
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
if (avformat_network_init()) {
return 1;
@@ -315,25 +262,9 @@ int main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_options options = {
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
};
int res = scrcpy(&options) ? 0 : 1;
int res = scrcpy(args.serial, args.port, args.max_size, args.bit_rate) ? 0 : 1;
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;
}

View File

@@ -18,26 +18,6 @@
typedef struct in_addr IN_ADDR;
#endif
socket_t net_connect(Uint32 addr, Uint16 port) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
return INVALID_SOCKET;
}
SOCKADDR_IN sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(addr);
sin.sin_port = htons(port);
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("connect");
return INVALID_SOCKET;
}
return sock;
}
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
@@ -82,19 +62,19 @@ ssize_t net_recv_all(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, MSG_WAITALL);
}
ssize_t net_send(socket_t socket, const void *buf, size_t len) {
ssize_t net_send(socket_t socket, void *buf, size_t len) {
return send(socket, buf, len, 0);
}
ssize_t net_send_all(socket_t socket, const void *buf, size_t len) {
ssize_t w = 0;
ssize_t net_send_all(socket_t socket, void *buf, size_t len) {
ssize_t w;
while (len > 0) {
w = send(socket, buf, len, 0);
if (w == -1) {
return -1;
}
len -= w;
buf = (char *) buf + w;
buf += w;
}
return w;
}

View File

@@ -9,6 +9,8 @@
#define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH
typedef SIZE_T size_t;
typedef SSIZE_T ssize_t;
typedef SOCKET socket_t;
#else
# include <sys/socket.h>
@@ -19,15 +21,14 @@
SDL_bool net_init(void);
void net_cleanup(void);
socket_t net_connect(Uint32 addr, Uint16 port);
socket_t net_listen(Uint32 addr, Uint16 port, int backlog);
socket_t net_accept(socket_t server_socket);
// the _all versions wait/retry until len bytes have been written/read
ssize_t net_recv(socket_t socket, void *buf, size_t len);
ssize_t net_recv_all(socket_t socket, void *buf, size_t len);
ssize_t net_send(socket_t socket, const void *buf, size_t len);
ssize_t net_send_all(socket_t socket, const void *buf, size_t len);
ssize_t net_send(socket_t socket, void *buf, size_t len);
ssize_t net_send_all(socket_t socket, void *buf, size_t len);
// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
SDL_bool net_shutdown(socket_t socket, int how);
SDL_bool net_close(socket_t socket);

View File

@@ -1,118 +0,0 @@
#include "recorder.h"
#include <libavutil/time.h>
#include "config.h"
#include "log.h"
static const AVOutputFormat *find_mp4_muxer(void) {
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
void *opaque = NULL;
#endif
const AVOutputFormat *oformat = NULL;
do {
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
oformat = av_muxer_iterate(&opaque);
#else
oformat = av_oformat_next(oformat);
#endif
// until null or with name "mp4"
} while (oformat && strcmp(oformat->name, "mp4"));
return oformat;
}
SDL_bool recorder_init(struct recorder *recorder, const char *filename,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Cannot strdup filename");
return SDL_FALSE;
}
recorder->declared_frame_size = declared_frame_size;
return SDL_TRUE;
}
void recorder_destroy(struct recorder *recorder) {
SDL_free(recorder->filename);
}
SDL_bool recorder_open(struct recorder *recorder, AVCodec *input_codec) {
const AVOutputFormat *mp4 = find_mp4_muxer();
if (!mp4) {
LOGE("Could not find mp4 muxer");
return SDL_FALSE;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOGE("Could not allocate output context");
return SDL_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 *) mp4;
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
avformat_free_context(recorder->ctx);
return SDL_FALSE;
}
// 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))
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
ostream->time_base = (AVRational) {1, 1000000}; // timestamps in us
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 SDL_FALSE;
}
ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return SDL_FALSE;
}
return SDL_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);
}
SDL_bool recorder_write(struct recorder *recorder, AVPacket *packet) {
return av_write_frame(recorder->ctx, packet) >= 0;
}

View File

@@ -1,24 +0,0 @@
#ifndef RECORDER_H
#define RECORDER_H
#include <libavformat/avformat.h>
#include <SDL2/SDL_stdinc.h>
#include "common.h"
struct recorder {
char *filename;
AVFormatContext *ctx;
struct size declared_frame_size;
};
SDL_bool recorder_init(struct recorder *recoder, const char *filename,
struct size declared_frame_size);
void recorder_destroy(struct recorder *recorder);
SDL_bool recorder_open(struct recorder *recorder, AVCodec *input_codec);
void recorder_close(struct recorder *recorder);
SDL_bool recorder_write(struct recorder *recorder, AVPacket *packet);
#endif

View File

@@ -13,25 +13,21 @@
#include "decoder.h"
#include "device.h"
#include "events.h"
#include "file_handler.h"
#include "frames.h"
#include "fps_counter.h"
#include "input_manager.h"
#include "fpscounter.h"
#include "inputmanager.h"
#include "log.h"
#include "lock_util.h"
#include "lockutil.h"
#include "net.h"
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "tiny_xpm.h"
#include "tinyxpm.h"
static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER;
static struct frames frames;
static struct decoder decoder;
static struct controller controller;
static struct file_handler file_handler;
static struct recorder recorder;
static struct input_manager input_manager = {
.controller = &controller,
@@ -39,43 +35,16 @@ static struct input_manager input_manager = {
.screen = &screen,
};
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
#ifdef CONTINUOUS_RESIZING_WORKAROUND
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
// not triggered. As a workaround, handle them in an event handler.
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static int event_watcher(void *data, SDL_Event *event) {
if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) {
// called from another thread, not very safe, but it's a workaround!
screen_render(&screen);
}
return 0;
}
#endif
static SDL_bool is_apk(const char *file) {
const char *ext = strrchr(file, '.');
return ext && !strcmp(ext, ".apk");
}
static SDL_bool event_loop(void) {
#ifdef CONTINUOUS_RESIZING_WORKAROUND
SDL_AddEventWatch(event_watcher, NULL);
#endif
static void event_loop(void) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case EVENT_DECODER_STOPPED:
LOGD("Video decoder stopped");
return SDL_FALSE;
return;
case SDL_QUIT:
LOGD("User requested to quit");
return SDL_TRUE;
return;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = SDL_TRUE;
@@ -83,20 +52,21 @@ static SDL_bool event_loop(void) {
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &frames)) {
return SDL_FALSE;
return;
}
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
break;
case SDL_TEXTINPUT:
case SDL_TEXTINPUT: {
input_manager_process_text_input(&input_manager, &event.text);
break;
}
case SDL_KEYDOWN:
case SDL_KEYUP:
input_manager_process_key(&input_manager, &event.key);
@@ -104,57 +74,24 @@ static SDL_bool event_loop(void) {
case SDL_MOUSEMOTION:
input_manager_process_mouse_motion(&input_manager, &event.motion);
break;
case SDL_MOUSEWHEEL:
case SDL_MOUSEWHEEL: {
input_manager_process_mouse_wheel(&input_manager, &event.wheel);
break;
}
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
case SDL_MOUSEBUTTONUP: {
input_manager_process_mouse_button(&input_manager, &event.button);
break;
case SDL_DROPFILE: {
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 SDL_FALSE;
}
static process_t set_show_touches_enabled(const char *serial, SDL_bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
SDL_bool scrcpy(const struct scrcpy_options *options) {
SDL_bool send_frame_meta = !!options->record_filename;
if (!server_start(&server, options->serial, options->port,
options->max_size, options->bit_rate, options->crop,
send_frame_meta)) {
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate) {
if (!server_start(&server, serial, local_port, max_size, bit_rate)) {
return SDL_FALSE;
}
process_t proc_show_touches = PROCESS_NONE;
SDL_bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
show_touches_waited = SDL_FALSE;
}
SDL_bool ret = SDL_TRUE;
if (!sdl_init_and_configure()) {
@@ -162,9 +99,13 @@ SDL_bool scrcpy(const struct scrcpy_options *options) {
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) {
server_stop(&server);
server_stop(&server, serial);
ret = SDL_FALSE;
goto finally_destroy_server;
}
@@ -176,41 +117,25 @@ SDL_bool scrcpy(const struct scrcpy_options *options) {
// therefore, we transmit the screen size before the video stream, to be able
// to init the window immediately
if (!device_read_info(device_socket, device_name, &frame_size)) {
server_stop(&server);
server_stop(&server, serial);
ret = SDL_FALSE;
goto finally_destroy_server;
}
if (!frames_init(&frames)) {
server_stop(&server);
server_stop(&server, serial);
ret = SDL_FALSE;
goto finally_destroy_server;
}
if (!file_handler_init(&file_handler, server.serial)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_frames;
}
struct recorder *rec = NULL;
if (options->record_filename) {
if (!recorder_init(&recorder, options->record_filename, frame_size)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_file_handler;
}
rec = &recorder;
}
decoder_init(&decoder, &frames, device_socket, rec);
decoder_init(&decoder, &frames, device_socket);
// now we consumed the header values, the socket receives the video stream
// start the decoder
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_recorder;
server_stop(&server, serial);
goto finally_destroy_frames;
}
if (!controller_init(&controller, device_socket)) {
@@ -228,20 +153,10 @@ SDL_bool scrcpy(const struct scrcpy_options *options) {
goto finally_stop_and_join_controller;
}
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = SDL_TRUE;
}
event_loop();
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
ret = event_loop();
LOGD("quit...");
screen_destroy(&screen);
finally_stop_and_join_controller:
controller_stop(&controller);
controller_join(&controller);
@@ -250,29 +165,11 @@ finally_destroy_controller:
finally_stop_decoder:
decoder_stop(&decoder);
// stop the server before decoder_join() to wake up the decoder
server_stop(&server);
server_stop(&server, serial);
decoder_join(&decoder);
finally_destroy_file_handler:
file_handler_stop(&file_handler);
file_handler_join(&file_handler);
file_handler_destroy(&file_handler);
finally_destroy_recorder:
if (options->record_filename) {
recorder_destroy(&recorder);
}
finally_destroy_frames:
frames_destroy(&frames);
finally_destroy_server:
if (options->show_touches) {
if (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_FALSE);
wait_show_touches(proc_show_touches);
}
server_destroy(&server);
return ret;

View File

@@ -3,17 +3,6 @@
#include <SDL2/SDL_stdinc.h>
struct scrcpy_options {
const char *serial;
const char *crop;
const char *record_filename;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
SDL_bool show_touches;
SDL_bool fullscreen;
};
SDL_bool scrcpy(const struct scrcpy_options *options);
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate);
#endif

View File

@@ -4,9 +4,9 @@
#include <string.h>
#include "icon.xpm"
#include "lock_util.h"
#include "lockutil.h"
#include "log.h"
#include "tiny_xpm.h"
#include "tinyxpm.h"
#define DISPLAY_MARGINS 96
@@ -18,8 +18,8 @@ SDL_bool sdl_init_and_configure(void) {
atexit(SDL_Quit);
// Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
// Bilinear resizing
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable bilinear filtering");
}
@@ -136,21 +136,13 @@ void screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER;
}
static inline SDL_Texture *create_texture(SDL_Renderer *renderer, struct size frame_size) {
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
}
SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, struct size frame_size) {
screen->frame_size = frame_size;
struct size window_size = get_initial_optimal_size(frame_size);
Uint32 window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height, window_flags);
window_size.width, window_size.height,
SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
if (!screen->window) {
LOGC("Could not create window: %s", SDL_GetError());
return SDL_FALSE;
@@ -179,7 +171,8 @@ SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, s
SDL_FreeSurface(icon);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height);
screen->texture = create_texture(screen->renderer, frame_size);
screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen);
@@ -228,7 +221,8 @@ static SDL_bool prepare_for_frame(struct screen *screen, struct size new_frame_s
LOGD("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height);
screen->texture = 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) {
LOGC("Could not create texture: %s", SDL_GetError());
return SDL_FALSE;

View File

@@ -3,9 +3,7 @@
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h"
#include "log.h"
@@ -39,47 +37,17 @@ static SDL_bool remove_server(const char *serial) {
return process_check_success(process, "adb shell rm");
}
static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
static SDL_bool enable_tunnel(const char *serial, Uint16 local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse");
}
static SDL_bool disable_tunnel_reverse(const char *serial) {
static SDL_bool disable_tunnel(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove");
}
static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward");
}
static SDL_bool disable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward_remove(serial, local_port);
return process_check_success(process, "adb forward --remove");
}
static SDL_bool enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return SDL_TRUE;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = SDL_TRUE;
return enable_tunnel_forward(server->serial, server->local_port);
}
static SDL_bool disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
return disable_tunnel_forward(server->serial, server->local_port);
}
return disable_tunnel_reverse(server->serial);
}
static process_t execute_server(const char *serial,
Uint16 max_size, Uint32 bit_rate,
SDL_bool tunnel_forward, const char *crop,
SDL_bool send_frame_meta) {
static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate) {
char max_size_string[6];
char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size);
@@ -92,50 +60,15 @@ static process_t execute_server(const char *serial,
"com.genymobile.scrcpy.Server",
max_size_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]));
}
#define IPV4_LOCALHOST 0x7F000001
static socket_t listen_on_port(Uint16 port) {
#define IPV4_LOCALHOST 0x7F000001
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t connect_and_read_byte(Uint16 port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
if (socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
char byte;
// the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection
if (net_recv_all(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
return INVALID_SOCKET;
}
return socket;
}
static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) {
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
socket_t socket = connect_and_read_byte(port);
if (socket != INVALID_SOCKET) {
// it worked!
return socket;
}
if (attempts) {
SDL_Delay(delay);
}
} while (--attempts > 0);
return INVALID_SOCKET;
}
static void close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
@@ -150,97 +83,65 @@ void server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER;
}
SDL_bool server_start(struct server *server, const char *serial,
Uint16 local_port, Uint16 max_size, Uint32 bit_rate,
const char *crop, SDL_bool send_frame_meta) {
server->local_port = local_port;
if (serial) {
server->serial = SDL_strdup(serial);
if (!server->serial) {
return SDL_FALSE;
}
}
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate) {
if (!push_server(serial)) {
SDL_free((void *) server->serial);
return SDL_FALSE;
}
server->server_copied_to_device = SDL_TRUE;
if (!enable_tunnel(server)) {
SDL_free((void *) server->serial);
if (!enable_tunnel(serial, local_port)) {
return SDL_FALSE;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!server->tunnel_forward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
SDL_free((void *) server->serial);
return SDL_FALSE;
}
server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(serial);
return SDL_FALSE;
}
// server will connect to our server socket
server->process = execute_server(serial, max_size, bit_rate,
server->tunnel_forward, crop,
send_frame_meta);
server->process = execute_server(serial, max_size, bit_rate);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
SDL_free((void *) server->serial);
close_socket(&server->server_socket);
disable_tunnel(serial);
return SDL_FALSE;
}
server->tunnel_enabled = SDL_TRUE;
server->adb_reverse_enabled = SDL_TRUE;
return SDL_TRUE;
}
socket_t server_connect_to(struct server *server) {
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
Uint32 attempts = 100;
Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay);
}
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms) {
server->device_socket = net_accept(server->server_socket);
if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
if (!server->tunnel_forward) {
// we don't need the server socket anymore
close_socket(&server->server_socket);
}
// we don't need the server socket anymore
close_socket(&server->server_socket);
// the server is started, we can clean up the jar from the temporary folder
remove_server(server->serial); // ignore failure
remove_server(serial); // ignore failure
server->server_copied_to_device = SDL_FALSE;
// we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure
server->tunnel_enabled = SDL_FALSE;
disable_tunnel(serial); // ignore failure
server->adb_reverse_enabled = SDL_FALSE;
return server->device_socket;
}
void server_stop(struct server *server) {
void server_stop(struct server *server, const char *serial) {
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
@@ -250,13 +151,13 @@ void server_stop(struct server *server) {
cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated");
if (server->tunnel_enabled) {
if (server->adb_reverse_enabled) {
// ignore failure
disable_tunnel(server);
disable_tunnel(serial);
}
if (server->server_copied_to_device) {
remove_server(server->serial); // ignore failure
remove_server(serial); // ignore failure
}
}
@@ -267,5 +168,4 @@ void server_destroy(struct server *server) {
if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket);
}
SDL_free((void *) server->serial);
}

View File

@@ -5,26 +5,18 @@
#include "net.h"
struct server {
const char *serial;
process_t process;
socket_t server_socket; // only used if !tunnel_forward
socket_t server_socket;
socket_t device_socket;
Uint16 local_port;
SDL_bool tunnel_enabled;
SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse"
SDL_bool send_frame_meta; // request frame PTS to be able to record properly
SDL_bool adb_reverse_enabled;
SDL_bool server_copied_to_device;
};
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \
.device_socket = INVALID_SOCKET, \
.local_port = 0, \
.tunnel_enabled = SDL_FALSE, \
.tunnel_forward = SDL_FALSE, \
.send_frame_meta = SDL_FALSE, \
.adb_reverse_enabled = SDL_FALSE, \
.server_copied_to_device = SDL_FALSE, \
}
@@ -32,15 +24,14 @@ struct server {
void server_init(struct server *server);
// push, enable tunnel et start the server
SDL_bool server_start(struct server *server, const char *serial,
Uint16 local_port, Uint16 max_size, Uint32 bit_rate,
const char *crop, SDL_bool send_frame_meta);
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate);
// block until the communication with the server is established
socket_t server_connect_to(struct server *server);
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms);
// disconnect and kill the server process
void server_stop(struct server *server);
void server_stop(struct server *server, const char *serial);
// close and release sockets
void server_destroy(struct server *server);

View File

@@ -1,7 +1,4 @@
#include "str_util.h"
#include <stdlib.h>
#include <string.h>
#include "strutil.h"
size_t xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
@@ -34,16 +31,3 @@ 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;
}

View File

@@ -16,8 +16,4 @@ size_t xstrncpy(char *dest, const char *src, size_t n);
// occurred, or n if truncated
size_t 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);
#endif

View File

@@ -1,82 +1,25 @@
#define _POSIX_SOURCE // for kill()
#include "../../command.h"
#include "command.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "log.h"
enum process_result cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
int fd[2];
if (pipe(fd) == -1) {
perror("pipe");
return PROCESS_ERROR_GENERIC;
}
enum process_result ret = PROCESS_SUCCESS;
*pid = fork();
if (*pid == -1) {
pid_t cmd_execute(const char *path, const char *const argv[]) {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
ret = PROCESS_ERROR_GENERIC;
goto end;
return -1;
}
if (*pid > 0) {
// parent close write side
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]);
if (pid == 0) {
execvp(path, (char *const *)argv);
perror("exec");
_exit(1);
}
end:
if (fd[0] != -1) {
close(fd[0]);
}
if (fd[1] != -1) {
close(fd[1]);
}
return ret;
return pid;
}
SDL_bool cmd_terminate(pid_t pid) {
if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid);
abort();
}
return kill(pid, SIGTERM) != -1;
}

View File

@@ -1,4 +1,4 @@
#include "net.h"
#include "../../net.h"
# include <unistd.h>

View File

@@ -1,49 +1,30 @@
#include "command.h"
#include "../../command.h"
#include "config.h"
#include "log.h"
#include "str_util.h"
#include "../../log.h"
#include "../../strutil.h"
static int build_cmd(char *cmd, size_t len, const char *const argv[]) {
// 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) {
HANDLE cmd_execute(const char *path, const char *const argv[]) {
STARTUPINFO si;
PROCESS_INFORMATION pi;
memset(&si, 0, 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];
if (build_cmd(cmd, sizeof(cmd), argv)) {
*handle = NULL;
return PROCESS_ERROR_GENERIC;
size_t ret = xstrjoin(cmd, argv, ' ', sizeof(cmd));
if (ret >= sizeof(cmd)) {
LOGE("Command too long (%" PRIsizet " chars)", sizeof(cmd) - 1);
return NULL;
}
#ifdef WINDOWS_NOCONSOLE
int flags = CREATE_NO_WINDOW;
#else
int flags = 0;
#endif
if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, flags, NULL, NULL, &si, &pi)) {
*handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY;
}
return PROCESS_ERROR_GENERIC;
if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
return NULL;
}
*handle = pi.hProcess;
return PROCESS_SUCCESS;
return pi.hProcess;
}
SDL_bool cmd_terminate(HANDLE handle) {

View File

@@ -1,6 +1,6 @@
#include "net.h"
#include "../../net.h"
#include "log.h"
#include "../../log.h"
SDL_bool net_init(void) {
WSADATA wsa;

View File

@@ -1,4 +1,4 @@
#include "tiny_xpm.h"
#include "tinyxpm.h"
#include <stdio.h>
#include <stdlib.h>
@@ -40,15 +40,15 @@ SDL_Surface *read_xpm(char *xpm[]) {
// *** No error handling, assume the XPM source is valid ***
// (it's in our source repo)
// Assertions are only checked in debug
int width = strtol(xpm[0], &endptr, 10);
int height = strtol(endptr + 1, &endptr, 10);
int colors = strtol(endptr + 1, &endptr, 10);
int chars = strtol(endptr + 1, &endptr, 10);
Uint32 width = strtol(xpm[0], &endptr, 10);
Uint32 height = strtol(endptr + 1, &endptr, 10);
Uint32 colors = strtol(endptr + 1, &endptr, 10);
Uint32 chars = strtol(endptr + 1, &endptr, 10);
// sanity checks
SDL_assert(0 <= width && width < 256);
SDL_assert(0 <= height && height < 256);
SDL_assert(0 <= colors && colors < 256);
SDL_assert(width < 256);
SDL_assert(height < 256);
SDL_assert(colors < 256);
SDL_assert(chars == 1); // this implementation does not support more
// init index

View File

@@ -1,9 +1,8 @@
#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;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
@@ -25,7 +24,7 @@ static void test_control_event_queue_empty(void) {
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;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
@@ -43,7 +42,7 @@ static void test_control_event_queue_full(void) {
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;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
@@ -87,7 +86,7 @@ static void test_control_event_queue_push_take(void) {
control_event_queue_destroy(&queue);
}
int main(void) {
int main() {
test_control_event_queue_empty();
test_control_event_queue_full();
test_control_event_queue_push_take();

View File

@@ -1,9 +1,8 @@
#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 = {
.type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = {
@@ -26,7 +25,7 @@ static void test_serialize_keycode_event(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_text_event(void) {
static void test_serialize_text_event() {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_TEXT,
.text_event = {
@@ -36,37 +35,17 @@ static void test_serialize_text_event(void) {
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 16);
assert(size == 15);
const unsigned char expected[] = {
0x01, // CONTROL_EVENT_TYPE_KEYCODE
0x00, 0x0d, // text length
0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_long_text_event(void) {
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) {
static void test_serialize_mouse_event() {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_MOUSE,
.mouse_event = {
@@ -99,7 +78,7 @@ static void test_serialize_mouse_event(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_scroll_event(void) {
static void test_serialize_scroll_event() {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_SCROLL,
.scroll_event = {
@@ -132,10 +111,10 @@ static void test_serialize_scroll_event(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) {
int main() {
test_serialize_keycode_event();
test_serialize_text_event();
test_serialize_long_text_event();
test_serialize_mouse_event();
test_serialize_scroll_event();
return 0;
}

View File

@@ -1,9 +1,9 @@
#include <assert.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";
size_t w = xstrncpy(s, "abcdef", sizeof(s));
@@ -20,7 +20,7 @@ static void test_xstrncpy_simple(void) {
assert(!strcmp("abcdef", s));
}
static void test_xstrncpy_just_fit(void) {
static void test_xstrncpy_just_fit() {
char s[] = "xxxxxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s));
@@ -34,7 +34,7 @@ static void test_xstrncpy_just_fit(void) {
assert(!strcmp("abcdef", s));
}
static void test_xstrncpy_truncated(void) {
static void test_xstrncpy_truncated() {
char s[] = "xxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s));
@@ -48,7 +48,7 @@ static void test_xstrncpy_truncated(void) {
assert(!strncmp("abcdef", s, 3));
}
static void test_xstrjoin_simple(void) {
static void test_xstrjoin_simple() {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxxxxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@@ -66,7 +66,7 @@ static void test_xstrjoin_simple(void) {
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 };
char s[] = "xxxxxxxxxxx";
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));
}
static void test_xstrjoin_truncated_in_token(void) {
static void test_xstrjoin_truncated_in_token() {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxx";
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));
}
static void test_xstrjoin_truncated_before_sep(void) {
static void test_xstrjoin_truncated_before_sep() {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxx";
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));
}
static void test_xstrjoin_truncated_after_sep(void) {
static void test_xstrjoin_truncated_after_sep() {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxx";
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));
}
int main(void) {
int main() {
test_xstrncpy_simple();
test_xstrncpy_just_fit();
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()
}
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
// 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.0.2-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.0.2-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.0.2-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.0.2-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
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# 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
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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,4 +1,4 @@
project('scrcpy', 'c', meson_version: '>= 0.37', default_options : 'c_std=c11')
project('scrcpy', 'c', meson_version: '>= 0.37')
if get_option('build_app')
subdir('app')

View File

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

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.0.2-win32-shared.zip \
cc190a3a4cf7bfbd4fbaa92609c1501a1de458055e6cfea8b745c1d515013aa8 \
ffmpeg-4.0.2-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.0.2-win32-dev.zip \
c72c74bad74ac0541f1b43090c26a50017c49041c182a703abd2057bb8cdc238 \
ffmpeg-4.0.2-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.0.2-win64-shared.zip \
ede566aca8b5348dff85570f9638c6bad33209f9419f79db7bde7daa37599bff \
ffmpeg-4.0.2-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.0.2-win64-dev.zip \
23ee994161c51285cb956b98d3caa499d48083dae7b26c1fdf77f22e98df1c5f \
ffmpeg-4.0.2-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 "$1" "$2" "$3"

View File

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

View File

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

View File

@@ -13,12 +13,12 @@ public class ControlEventReader {
private static final int SCROLL_PAYLOAD_LENGTH = 16;
private static final int COMMAND_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300;
private static final int RAW_BUFFER_SIZE = 1024;
private static final int MAX_TEXT_LENGTH = 32;
private static final int RAW_BUFFER_SIZE = 128;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH];
private final byte[] textBuffer = new byte[MAX_TEXT_LENGTH];
public ControlEventReader() {
// invariant: the buffer is always in "get" mode
@@ -94,7 +94,7 @@ public class ControlEventReader {
if (buffer.remaining() < 1) {
return null;
}
int len = toUnsigned(buffer.getShort());
int len = toUnsigned(buffer.get());
if (buffer.remaining() < len) {
return null;
}

View File

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

View File

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

View File

@@ -39,6 +39,10 @@ public class EventController {
coords.orientation = 0;
coords.pressure = 1;
coords.size = 1;
coords.toolMajor = 1;
coords.toolMinor = 1;
coords.touchMajor = 1;
coords.touchMinor = 1;
}
private void setPointerCoords(Point point) {
@@ -89,12 +93,14 @@ public class EventController {
return injectKeyEvent(action, keycode, 0, metaState);
}
private boolean injectChar(char c) {
String decomposed = KeyComposition.decompose(c);
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
KeyEvent[] events = charMap.getEvents(chars);
private boolean injectText(String text) {
return injectText(text, true);
}
private boolean injectText(String text, boolean decomposeOnFailure) {
KeyEvent[] events = charMap.getEvents(text.toCharArray());
if (events == null) {
return false;
return decomposeOnFailure ? injectDecomposition(text) : false;
}
for (KeyEvent event : events) {
if (!injectEvent(event)) {
@@ -104,9 +110,10 @@ public class EventController {
return true;
}
private boolean injectText(String text) {
private boolean injectDecomposition(String text) {
for (char c : text.toCharArray()) {
if (!injectChar(c)) {
String composedText = KeyComposition.decompose(c);
if (composedText == null || !injectText(composedText, false)) {
return false;
}
}
@@ -163,15 +170,10 @@ public class EventController {
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
}
private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode);
}
private boolean executeCommand(int action) {
switch (action) {
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
return pressBackOrTurnScreenOn();
case ControlEvent.COMMAND_SCREEN_ON:
return turnScreenOn();
default:
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[])}
* KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
* <p>
* See <a href="https://source.android.com/devices/input/key-character-map-files#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 {

View File

@@ -1,13 +1,8 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
public class Options {
private int maxSize;
private int bitRate;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
public int getMaxSize() {
return maxSize;
@@ -24,28 +19,4 @@ public class Options {
public void setBitRate(int bitRate) {
this.bitRate = bitRate;
}
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
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 android.graphics.Rect;
import android.media.MediaMuxer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.IBinder;
import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
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 MICROSECONDS_IN_ONE_SECOND = 1_000_000;
private static final int NO_PTS = -1;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private int bitRate;
private int frameRate;
private int iFrameInterval;
private boolean sendFrameMeta;
private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int frameRate, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta;
public ScreenEncoder(int bitRate, int frameRate, int iFrameInterval) {
this.bitRate = bitRate;
this.frameRate = frameRate;
this.iFrameInterval = iFrameInterval;
}
public ScreenEncoder(boolean sendFrameMeta, int bitRate) {
this(sendFrameMeta, bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
public ScreenEncoder(int bitRate) {
this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
}
@Override
@@ -54,7 +48,7 @@ public class ScreenEncoder implements Device.RotationListener {
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);
device.setRotationListener(this);
boolean alive;
@@ -62,15 +56,15 @@ public class ScreenEncoder implements Device.RotationListener {
do {
MediaCodec codec = createCodec();
IBinder display = createDisplay();
Rect contentRect = device.getScreenInfo().getContentRect();
Rect deviceRect = device.getScreenInfo().getDeviceSize().toRect();
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, contentRect, videoRect);
setDisplaySurface(display, surface, deviceRect, videoRect);
codec.start();
try {
alive = encode(codec, fd);
alive = encode(codec, outputStream);
} finally {
codec.stop();
destroyDisplay(display);
@@ -83,11 +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;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
@@ -97,13 +91,15 @@ public class ScreenEncoder implements Device.RotationListener {
break;
}
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
while (outputBuffer.hasRemaining()) {
int remaining = outputBuffer.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 {
if (outputBufferId >= 0) {
@@ -115,25 +111,6 @@ public class ScreenEncoder implements Device.RotationListener {
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 {
return MediaCodec.createEncoderByType("video/avc");
}

View File

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

View File

@@ -1,9 +1,6 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.io.IOException;
import java.util.Arrays;
public final class Server {
@@ -13,16 +10,15 @@ public final class Server {
private static void scrcpy(Options options) throws IOException {
final Device device = new Device(options);
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
try (DesktopConnection connection = DesktopConnection.open(device)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
// asynchronous
startEventController(device, connection);
try {
// synchronous
screenEncoder.streamScreen(device, connection.getFd());
screenEncoder.streamScreen(device, connection.getOutputStream());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
@@ -59,44 +55,9 @@ public final class Server {
int bitRate = Integer.parseInt(args[1]);
options.setBitRate(bitRate);
if (args.length < 3) {
return options;
}
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward);
if (args.length < 4) {
return options;
}
Rect crop = parseCrop(args[3]);
options.setCrop(crop);
if (args.length < 5) {
return options;
}
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
options.setSendFrameMeta(sendFrameMeta);
return options;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override

View File

@@ -11,8 +11,6 @@ import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlEventReaderTest {
@@ -45,8 +43,8 @@ public class ControlEventReaderTest {
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length);
dos.write(text);
dos.writeByte(text.length);
dos.write("testé".getBytes(StandardCharsets.UTF_8));
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
@@ -56,26 +54,6 @@ public class ControlEventReaderTest {
Assert.assertEquals("testé", event.getText());
}
@Test
public void testParseLongTextEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
}
@Test
public void testParseMouseEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();