Compare commits

..

1 Commits

Author SHA1 Message Date
57687bdfcd Write header file with correct extradata
When recording, the header must be written with extradata set to the
content of the very first packet.

Suggested-by: Steve Lhomme <robux4@ycbcr.xyz>
2019-02-09 12:54:12 +01:00
105 changed files with 2716 additions and 5446 deletions

View File

@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
## Requirements
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`adb`).
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`:
@ -40,10 +40,10 @@ Install the required packages from your package manager.
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0-0
sudo apt install ffmpeg libsdl2-2.0.0
# client build dependencies
sudo apt install make gcc git pkg-config meson ninja-build \
sudo apt install make gcc pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
@ -70,7 +70,7 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
sudo dnf install java
```
@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
_(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_
- [`scrcpy-server-v1.6.jar`][direct-scrcpy-server]
_(SHA-256: 08df924bf6d10943df9eaacfff548a99871ebfca4641f8c7ddddb73f27cb905b)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.6/scrcpy-server-v1.6.jar
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@ -65,20 +65,17 @@ 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/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view
### Threading
The server uses 3 threads:
The server uses 2 threads:
- the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control messages_ (typically,
keyboard and mouse events) from the client;
- the **receiver** thread (managed by the controller), sending _device messges_
to the clients (currently, it is only used to send the device clipboard
content).
- the **controller** thread, listening for _control events_ (typically,
keyboard and mouse events) from the client.
Since the video encoding is typically hardware, there would be no benefit in
encoding and streaming in two different threads.
@ -92,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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced.
@ -108,30 +105,30 @@ 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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection
_Control messages_ are received from the client by the [`Controller`] (run in a
separate thread). There are several types of input events:
_Control events_ are received from the client by the [`EventController`] (run in
a separate thread). There are 5 types of input events:
- keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly),
- mouse motion/click,
- mouse scroll,
- other commands (e.g. to switch the screen on or to copy the clipboard).
- custom command (e.g. to switch the screen on).
Some of them need to inject input events to the system. To do so, they use the
_hidden_ method [`InputManager.injectInputEvent`] (exposed by our
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]).
[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@ -148,15 +145,15 @@ The video stream is decoded by [libav] (FFmpeg).
### Initialization
On startup, in addition to _libav_ and _SDL_ initialization, the client must
push and start the server on the device, and open two sockets (one for the video
stream, one for control) so that they may communicate.
push and start the server on the device, and open a socket so that they may
communicate.
Note that the client-server roles are expressed at the application level:
- the server _serves_ video stream and handle requests from the client,
- the client _controls_ the device through the server.
However, the roles are reversed at the network level:
However, the roles are inverted at the network level:
- the client opens a server socket and listen on a port before starting the
server,
@ -165,9 +162,6 @@ However, the roles are reversed at the network level:
This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling.
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
reverse`. See commit [1038bad] and [issue #5].)_
Once the server is connected, it sends the device information (name and initial
screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available.
@ -175,38 +169,25 @@ the first frame is available.
To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]).
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
### Threading
The client uses 4 threads:
The client uses 3 threads:
- the **main** thread, executing the SDL event loop,
- the **stream** thread, receiving the video and used for decoding and
recording,
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the client.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to
print the framerate regularly in the console.
- the **decoder** thread, decoding video frames,
- the **controller** thread, sending _control events_ to the server.
### Decoder
### Stream
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264
stream from the socket, and notifies the main thread when a new frame is
available.
The video [stream] is received from the socket (connected to the server on the
device) in a separate thread.
If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_
to decode the H.264 stream from the socket, and notifies the main thread when a
new frame is available.
There are two [frames][video_buffer] simultaneously in memory:
There are two [frames] simultaneously in memory:
- the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread.
@ -214,39 +195,26 @@ 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.
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
H.264 packet to the output video file.
[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
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h
```
+----------+ +----------+
---> | decoder | ---> | screen |
+---------+ / +----------+ +----------+
socket ---> | stream | ----
+---------+ \ +----------+
---> | recorder |
+----------+
```
### Controller
The [controller] is responsible to send _control messages_ to the device. It
runs in a separate thread, to avoid I/O on the main thread.
The [controller] is responsible to send _control events_ to the device. It runs
in a separate thread, to avoid I/O on the main thread.
On SDL event, received on the main thread, the [input manager][inputmanager]
creates appropriate [_control messages_][controlmsg]. It is responsible to
creates appropriate [_control events_][controlevent]. It is responsible to
convert SDL events to Android events (using [convert]). It pushes the _control
messages_ to a queue hold by the controller. On its own thread, the controller
takes messages from the queue, that it serializes and sends to the client.
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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h
[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
### UI and event loop
@ -257,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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201
[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
## Hack

4
FAQ.md
View File

@ -19,6 +19,10 @@ Windows may need some [drivers] to detect your device.
[drivers]: https://developer.android.com/studio/run/oem-usb.html
If you still encounter problems, please see [issue 9].
[issue 9]: https://github.com/Genymobile/scrcpy/issues/9
### Mouse clicks do not work

View File

@ -188,7 +188,6 @@
identification within third-party archives.
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -34,7 +34,7 @@ WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
release: clean zip-win32 zip-win64 sums
@echo "Windows archives generated in $(DIST)/"
@echo "Release created in $(DIST)/."
clean:
$(GRADLE) clean
@ -44,7 +44,7 @@ clean:
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dcompile_app=false )
--buildtype release -Dbuild_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
@ -56,8 +56,8 @@ build-win32: prepare-deps-win32
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dportable=true )
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32
@ -66,9 +66,9 @@ build-win32-noconsole: prepare-deps-win32
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64:
@ -80,8 +80,8 @@ build-win64: prepare-deps-win64
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dportable=true )
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64
@ -90,9 +90,9 @@ build-win64-noconsole: prepare-deps-win64
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
@ -100,29 +100,28 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.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)"; \

162
README.md
View File

@ -1,15 +1,15 @@
# scrcpy (v1.9)
# scrcpy (v1.6)
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_.
It works on _GNU/Linux_, _Windows_ and _MacOS_.
![screenshot](assets/screenshot-debian-600.jpg)
## Requirements
The Android device requires at least API 21 (Android 5.0).
The Android part requires at least API 21 (Android 5.0).
Make sure you [enabled adb debugging][enable-adb] on your device(s).
@ -29,16 +29,12 @@ control it using keyboard and mouse.
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard.
A [Snap] package is available: [`scrcpy`][snap-link].
For Arch Linux, two [AUR] packages have been created by users:
[snap-link]: https://snapstats.org/snaps/scrcpy
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
@ -51,18 +47,18 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available:
- [`scrcpy-win32-v1.9.zip`][direct-win32]
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
- [`scrcpy-win64-v1.9.zip`][direct-win64]
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
- [`scrcpy-win32-v1.6.zip`][direct-win32]
_(SHA-256: 4ca0c5924ab2ebf19b70f6598b2e546f65ba469a72ded2d1b213df3380fb46b1)_
- [`scrcpy-win64-v1.6.zip`][direct-win64]
_(SHA-256: f66b7eace8dd6537a9a27176fd824704a284d8e82077ccc903344396043f90c9)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.6/scrcpy-win32-v1.6.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.6/scrcpy-win64-v1.6.zip
You can also [build the app manually][BUILD].
### macOS
### Mac OS
The application is available in [Homebrew]. Just install it:
@ -101,22 +97,22 @@ scrcpy --help
### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to
increase performance.
increase performances.
To limit both the width and height to some value (e.g. 1024):
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.
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 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps):
```bash
scrcpy --bit-rate 2M
@ -128,7 +124,7 @@ scrcpy -b 2M # short version
The device screen may be cropped to mirror only part of the screen.
This is useful for example to mirror only one eye of the Oculus Go:
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)
@ -166,16 +162,7 @@ It is possible to record the screen while mirroring:
```bash
scrcpy --record file.mp4
scrcpy -r file.mkv
```
To disable mirroring while recording:
```bash
scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
# Ctrl+C does not terminate properly on Windows, so disconnect the device
scrcpy -r file.mp4
```
"Skipped frames" are recorded, even if they are not displayed in real time (for
@ -251,108 +238,42 @@ _scrcpy_ window.
There is no visual feedback, a log is printed to the console.
The target directory can be changed on start:
```bash
scrcpy --push-target /sdcard/foo/bar/
```
### Read-only
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash
scrcpy --no-control
scrcpy -n
```
### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
command-line option:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
### Custom window title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
```
### Forward audio
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
Audio is not forwarded by _scrcpy_.
Also see [issue #14].
There is a limited solution using [AOA], implemented in the [`audio`] branch. If
you are interested, see [issue 14].
[USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
[AOA]: https://source.android.com/devices/accessories/aoa2
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts
| Action | Shortcut | Shortcut (macOS)
| -------------------------------------- |:----------------------------- |:-----------------------------
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
| 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` |
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
## Custom paths
To use a specific _adb_ binary, configure its path in the environment variable
`ADB`:
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file, configure its path in
`SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## Why _scrcpy_?
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
@ -385,7 +306,6 @@ Read the [developers page].
## Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,25 +1,23 @@
src = [
'src/main.c',
'src/command.c',
'src/control_msg.c',
'src/control_event.c',
'src/controller.c',
'src/convert.c',
'src/decoder.c',
'src/device.c',
'src/device_msg.c',
'src/file_handler.c',
'src/fps_counter.c',
'src/frames.c',
'src/input_manager.c',
'src/lock_util.c',
'src/net.c',
'src/receiver.c',
'src/recorder.c',
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/str_util.c',
'src/tiny_xpm.c',
'src/stream.c',
'src/video_buffer.c',
]
if not get_option('crossbuild_windows')
@ -93,9 +91,21 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
# build a "portable" version (with scrcpy-server.jar accessible from the same
# directory as the executable)
conf.set('PORTABLE', get_option('portable'))
# the path of the server, which will be appended to the prefix
# ignored if OVERRIDE_SERVER_PATH if defined
# must be consistent with the install_dir in server/meson.build
conf.set_quoted('PREFIXED_SERVER_PATH', '/share/scrcpy/scrcpy-server.jar')
# the path of the server to be used "as is"
# this is useful for building a "portable" version (with the server in the same
# directory as the client)
override_server_path = get_option('override_server_path')
if override_server_path != ''
conf.set_quoted('OVERRIDE_SERVER_PATH', override_server_path)
else
# undefine it
conf.set('OVERRIDE_SERVER_PATH', false)
endif
# the default client TCP port for the "adb reverse" tunnel
# overridden by option --port
@ -109,6 +119,11 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# whether the app should always display the most recent available frame, even
# if the previous one has not been displayed
# SKIP_FRAMES improves latency at the cost of framerate
conf.set('SKIP_FRAMES', get_option('skip_frames'))
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
@ -127,38 +142,18 @@ else
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, include_directories: src_dir, install: true, c_args: c_args, link_args: link_args)
### TESTS
tests = [
['test_cbuf', [
'tests/test_cbuf.c',
]],
['test_control_event_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
'src/str_util.c'
]],
['test_device_event_deserialize', [
'tests/test_device_msg_deserialize.c',
'src/device_msg.c'
]],
['test_strutil', [
'tests/test_strutil.c',
'src/str_util.c'
]],
['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']],
]
foreach t : tests
exe = executable(t[0], t[1],
include_directories: src_dir,
dependencies: dependencies)
exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies)
test(t[0], exe)
endforeach

View File

@ -1,38 +1,28 @@
#ifndef BUFFER_UTIL_H
#define BUFFER_UTIL_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_stdinc.h>
static inline void
buffer_write16be(uint8_t *buf, uint16_t value) {
static inline void buffer_write16be(Uint8 *buf, Uint16 value) {
buf[0] = value >> 8;
buf[1] = value;
}
static inline void
buffer_write32be(uint8_t *buf, uint32_t 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 uint16_t
buffer_read16be(const uint8_t *buf) {
return (buf[0] << 8) | buf[1];
}
static inline uint32_t
buffer_read32be(const uint8_t *buf) {
static inline Uint32 buffer_read32be(Uint8 *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
}
static inline
uint64_t buffer_read64be(const uint8_t *buf) {
uint32_t msb = buffer_read32be(buf);
uint32_t lsb = buffer_read32be(&buf[4]);
return ((uint64_t) msb << 32) | lsb;
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

@ -1,50 +0,0 @@
// generic circular buffer (bounded queue) implementation
#ifndef CBUF_H
#define CBUF_H
#include <stdbool.h>
#include <unistd.h>
// To define a circular buffer type of 20 ints:
// typedef CBUF(int, 20) my_cbuf_t;
//
// data has length CAP + 1 to distinguish empty vs full.
#define CBUF(TYPE, CAP) { \
TYPE data[(CAP) + 1]; \
size_t head; \
size_t tail; \
}
#define cbuf_size_(PCBUF) \
(sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data))
#define cbuf_is_empty(PCBUF) \
((PCBUF)->head == (PCBUF)->tail)
#define cbuf_is_full(PCBUF) \
(((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail)
#define cbuf_init(PCBUF) \
(void) ((PCBUF)->head = (PCBUF)->tail = 0)
#define cbuf_push(PCBUF, ITEM) \
({ \
bool ok = !cbuf_is_full(PCBUF); \
if (ok) { \
(PCBUF)->data[(PCBUF)->head] = (ITEM); \
(PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \
} \
ok; \
}) \
#define cbuf_take(PCBUF, PITEM) \
({ \
bool ok = !cbuf_is_empty(PCBUF); \
if (ok) { \
*(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \
(PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \
} \
ok; \
})
#endif

View File

@ -1,6 +1,5 @@
#include "command.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -11,8 +10,7 @@
static const char *adb_command;
static inline const char *
get_adb_command(void) {
static inline const char *get_adb_command(void) {
if (!adb_command) {
adb_command = getenv("ADB");
if (!adb_command)
@ -21,61 +19,22 @@ get_adb_command(void) {
return adb_command;
}
// serialize argv to string "[arg1], [arg2], [arg3]"
static size_t
argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
size_t idx = 0;
bool first = true;
while (*argv) {
const char *arg = *argv;
size_t len = strlen(arg);
// count space for "[], ...\0"
if (idx + len + 8 >= bufsize) {
// not enough space, truncate
assert(idx < bufsize - 4);
memcpy(&buf[idx], "...", 3);
idx += 3;
break;
}
if (first) {
first = false;
} else {
buf[idx++] = ',';
buf[idx++] = ' ';
}
buf[idx++] = '[';
memcpy(&buf[idx], arg, len);
idx += len;
buf[idx++] = ']';
argv++;
}
assert(idx < bufsize);
buf[idx] = '\0';
return idx;
}
static void
show_adb_err_msg(enum process_result err, const char *const argv[]) {
char buf[512];
static void show_adb_err_msg(enum process_result err) {
switch (err) {
case PROCESS_ERROR_GENERIC:
argv_to_string(argv, buf, sizeof(buf));
LOGE("Failed to execute: %s", buf);
LOGE("Failed to execute adb");
break;
case PROCESS_ERROR_MISSING_BINARY:
argv_to_string(argv, buf, sizeof(buf));
LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)");
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
/* do nothing */
break;
}
}
process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len) {
const char *cmd[len + 4];
int i;
process_t process;
@ -92,15 +51,13 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd[0], cmd, &process);
if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, cmd);
show_adb_err_msg(r);
return PROCESS_NONE;
}
return process;
}
process_t
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
@ -109,17 +66,14 @@ adb_forward(const char *serial, uint16_t local_port,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t
adb_forward_remove(const char *serial, uint16_t local_port) {
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) {
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
sprintf(local, "tcp:%" PRIu16, local_port);
@ -128,16 +82,14 @@ adb_reverse(const char *serial, const char *device_socket_name,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t
adb_reverse_remove(const char *serial, const char *device_socket_name) {
process_t adb_reverse_remove(const char *serial, const char *device_socket_name) {
char remote[108 + 14 + 1]; // localabstract:NAME
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
const char *const adb_cmd[] = {"reverse", "--remove", remote};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t
adb_push(const char *serial, const char *local, const char *remote) {
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)
@ -147,7 +99,7 @@ adb_push(const char *serial, const char *local, const char *remote) {
}
remote = strquote(remote);
if (!remote) {
SDL_free((void *) local);
free((void *) local);
return PROCESS_NONE;
}
#endif
@ -156,15 +108,14 @@ adb_push(const char *serial, const char *local, const char *remote) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
SDL_free((void *) remote);
SDL_free((void *) local);
free((void *) remote);
free((void *) local);
#endif
return proc;
}
process_t
adb_install(const char *serial, const char *local) {
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)
@ -178,17 +129,16 @@ adb_install(const char *serial, const char *local) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
SDL_free((void *) local);
free((void *) local);
#endif
return proc;
}
bool
process_check_success(process_t proc, const char *name) {
SDL_bool process_check_success(process_t proc, const char *name) {
if (proc == PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name);
return false;
return SDL_FALSE;
}
exit_code_t exit_code;
if (!cmd_simple_wait(proc, &exit_code)) {
@ -197,7 +147,7 @@ process_check_success(process_t proc, const char *name) {
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
return false;
return SDL_FALSE;
}
return true;
return SDL_TRUE;
}

View File

@ -1,38 +1,35 @@
#ifndef COMMAND_H
#define COMMAND_H
#include <stdbool.h>
#include <inttypes.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_platform.h>
#ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h>
# define PATH_SEPARATOR '\\'
# define PRIexitcode "lu"
// <https://stackoverflow.com/a/44383330/1987178>
#ifdef _WIN32
# define PRIexitcode "lu"
# ifdef _WIN64
# define PRIsizet PRIu64
# else
# define PRIsizet PRIu32
# endif
#else
# define PRIsizet "zu"
# define PRIexitcode "d"
#endif
#ifdef __WINDOWS__
# include <winsock2.h> // not needed here, but must never be included AFTER windows.h
# include <windows.h>
# define PROCESS_NONE NULL
typedef HANDLE process_t;
typedef DWORD exit_code_t;
#else
# include <sys/types.h>
# define PATH_SEPARATOR '/'
# define PRIsizet "zu"
# define PRIexitcode "d"
# define PROCESS_NONE -1
typedef pid_t process_t;
typedef int exit_code_t;
#endif
# define NO_EXIT_CODE -1
enum process_result {
@ -41,46 +38,20 @@ enum process_result {
PROCESS_ERROR_MISSING_BINARY,
};
enum process_result
cmd_execute(const char *path, const char *const argv[], process_t *process);
enum process_result cmd_execute(const char *path, const char *const argv[], process_t *process);
SDL_bool cmd_terminate(process_t pid);
SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
bool
cmd_terminate(process_t pid);
bool
cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t 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_execute(const char *serial, const char *const adb_cmd[], int len);
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
process_t adb_forward_remove(const char *serial, uint16_t local_port);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t adb_push(const char *serial, const char *local, const char *remote);
process_t adb_install(const char *serial, const char *local);
// convenience function to wait for a successful process execution
// automatically log process errors with the provided process name
bool
process_check_success(process_t proc, const char *name);
// return the absolute path of the executable (the scrcpy binary)
// may be NULL on error; to be freed by SDL_free
char *
get_executable_path(void);
SDL_bool process_check_success(process_t process, const char *name);
#endif

View File

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

View File

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

100
app/src/control_event.c Normal file
View File

@ -0,0 +1,100 @@
#include "control_event.h"
#include <SDL2/SDL_stdinc.h>
#include <string.h>
#include "buffer_util.h"
#include "lock_util.h"
#include "log.h"
static void write_position(Uint8 *buf, const struct position *position) {
buffer_write32be(&buf[0], position->point.x);
buffer_write32be(&buf[4], position->point.y);
buffer_write16be(&buf[8], position->screen_size.width);
buffer_write16be(&buf[10], position->screen_size.height);
}
int control_event_serialize(const struct control_event *event, unsigned char *buf) {
buf[0] = event->type;
switch (event->type) {
case CONTROL_EVENT_TYPE_KEYCODE:
buf[1] = event->keycode_event.action;
buffer_write32be(&buf[2], event->keycode_event.keycode);
buffer_write32be(&buf[6], event->keycode_event.metastate);
return 10;
case CONTROL_EVENT_TYPE_TEXT: {
// write length (2 bytes) + string (non nul-terminated)
size_t len = strlen(event->text_event.text);
if (len > TEXT_MAX_LENGTH) {
// injecting a text takes time, so limit the text length
len = TEXT_MAX_LENGTH;
}
buffer_write16be(&buf[1], (Uint16) len);
memcpy(&buf[3], event->text_event.text, len);
return 3 + len;
}
case CONTROL_EVENT_TYPE_MOUSE:
buf[1] = event->mouse_event.action;
buffer_write32be(&buf[2], event->mouse_event.buttons);
write_position(&buf[6], &event->mouse_event.position);
return 18;
case CONTROL_EVENT_TYPE_SCROLL:
write_position(&buf[1], &event->scroll_event.position);
buffer_write32be(&buf[13], (Uint32) event->scroll_event.hscroll);
buffer_write32be(&buf[17], (Uint32) event->scroll_event.vscroll);
return 21;
case CONTROL_EVENT_TYPE_COMMAND:
buf[1] = event->command_event.action;
return 2;
default:
LOGW("Unknown event type: %u", (unsigned) event->type);
return 0;
}
}
void control_event_destroy(struct control_event *event) {
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
SDL_free(event->text_event.text);
}
}
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
return queue->head == queue->tail;
}
SDL_bool control_event_queue_is_full(const struct control_event_queue *queue) {
return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail;
}
SDL_bool control_event_queue_init(struct control_event_queue *queue) {
queue->head = 0;
queue->tail = 0;
// the current implementation may not fail
return SDL_TRUE;
}
void control_event_queue_destroy(struct control_event_queue *queue) {
int i = queue->tail;
while (i != queue->head) {
control_event_destroy(&queue->data[i]);
i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE;
}
}
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {
if (control_event_queue_is_full(queue)) {
return SDL_FALSE;
}
queue->data[queue->head] = *event;
queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE;
return SDL_TRUE;
}
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event) {
if (control_event_queue_is_empty(queue)) {
return SDL_FALSE;
}
*event = queue->data[queue->tail];
queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE;
return SDL_TRUE;
}

73
app/src/control_event.h Normal file
View File

@ -0,0 +1,73 @@
#ifndef CONTROLEVENT_H
#define CONTROLEVENT_H
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include "android/input.h"
#include "android/keycodes.h"
#include "common.h"
#define CONTROL_EVENT_QUEUE_SIZE 64
#define TEXT_MAX_LENGTH 300
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE,
CONTROL_EVENT_TYPE_TEXT,
CONTROL_EVENT_TYPE_MOUSE,
CONTROL_EVENT_TYPE_SCROLL,
CONTROL_EVENT_TYPE_COMMAND,
};
#define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0
struct control_event {
enum control_event_type type;
union {
struct {
enum android_keyevent_action action;
enum android_keycode keycode;
enum android_metastate metastate;
} keycode_event;
struct {
char *text; // owned, to be freed by SDL_free()
} text_event;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
struct position position;
} mouse_event;
struct {
struct position position;
Sint32 hscroll;
Sint32 vscroll;
} scroll_event;
struct {
int action;
} command_event;
};
};
struct control_event_queue {
struct control_event data[CONTROL_EVENT_QUEUE_SIZE];
int head;
int tail;
};
// buf size must be at least SERIALIZED_EVENT_MAX_SIZE
int control_event_serialize(const struct control_event *event, unsigned char *buf);
SDL_bool control_event_queue_init(struct control_event_queue *queue);
void control_event_queue_destroy(struct control_event_queue *queue);
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue);
SDL_bool control_event_queue_is_full(const struct control_event_queue *queue);
// event is copied, the queue does not use the event after the function returns
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,86 +0,0 @@
#include "control_msg.h"
#include <string.h>
#include "buffer_util.h"
#include "log.h"
#include "str_util.h"
static void
write_position(uint8_t *buf, const struct position *position) {
buffer_write32be(&buf[0], position->point.x);
buffer_write32be(&buf[4], position->point.y);
buffer_write16be(&buf[8], position->screen_size.width);
buffer_write16be(&buf[10], position->screen_size.height);
}
// write length (2 bytes) + string (non nul-terminated)
static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
size_t len = utf8_truncation_index(utf8, max_len);
buffer_write16be(buf, (uint16_t) len);
memcpy(&buf[2], utf8, len);
return 2 + len;
}
size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[0] = msg->type;
switch (msg->type) {
case CONTROL_MSG_TYPE_INJECT_KEYCODE:
buf[1] = msg->inject_keycode.action;
buffer_write32be(&buf[2], msg->inject_keycode.keycode);
buffer_write32be(&buf[6], msg->inject_keycode.metastate);
return 10;
case CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len = write_string(msg->inject_text.text,
CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]);
return 1 + len;
}
case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT:
buf[1] = msg->inject_mouse_event.action;
buffer_write32be(&buf[2], msg->inject_mouse_event.buttons);
write_position(&buf[6], &msg->inject_mouse_event.position);
return 18;
case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
buffer_write32be(&buf[13],
(uint32_t) msg->inject_scroll_event.hscroll);
buffer_write32be(&buf[17],
(uint32_t) msg->inject_scroll_event.vscroll);
return 21;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
size_t len = write_string(msg->inject_text.text,
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[1]);
return 1 + len;
}
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode;
return 2;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
// no additional data
return 1;
default:
LOGW("Unknown message type: %u", (unsigned) msg->type);
return 0;
}
}
void
control_msg_destroy(struct control_msg *msg) {
switch (msg->type) {
case CONTROL_MSG_TYPE_INJECT_TEXT:
SDL_free(msg->inject_text.text);
break;
case CONTROL_MSG_TYPE_SET_CLIPBOARD:
SDL_free(msg->set_clipboard.text);
break;
default:
// do nothing
break;
}
}

View File

@ -1,74 +0,0 @@
#ifndef CONTROLMSG_H
#define CONTROLMSG_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "android/input.h"
#include "android/keycodes.h"
#include "common.h"
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
CONTROL_MSG_TYPE_INJECT_TEXT,
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
};
enum screen_power_mode {
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SCREEN_POWER_MODE_OFF = 0,
SCREEN_POWER_MODE_NORMAL = 2,
};
struct control_msg {
enum control_msg_type type;
union {
struct {
enum android_keyevent_action action;
enum android_keycode keycode;
enum android_metastate metastate;
} inject_keycode;
struct {
char *text; // owned, to be freed by SDL_free()
} inject_text;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
struct position position;
} inject_mouse_event;
struct {
struct position position;
int32_t hscroll;
int32_t vscroll;
} inject_scroll_event;
struct {
char *text; // owned, to be freed by SDL_free()
} set_clipboard;
struct {
enum screen_power_mode mode;
} set_screen_power_mode;
};
};
// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE
// return the number of bytes written
size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf);
void
control_msg_destroy(struct control_msg *msg);
#endif

View File

@ -1,133 +1,105 @@
#include "controller.h"
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "lock_util.h"
#include "log.h"
bool
controller_init(struct controller *controller, socket_t control_socket) {
cbuf_init(&controller->queue);
if (!receiver_init(&controller->receiver, control_socket)) {
return false;
SDL_bool controller_init(struct controller *controller, socket_t video_socket) {
if (!control_event_queue_init(&controller->queue)) {
return SDL_FALSE;
}
if (!(controller->mutex = SDL_CreateMutex())) {
receiver_destroy(&controller->receiver);
return false;
return SDL_FALSE;
}
if (!(controller->msg_cond = SDL_CreateCond())) {
receiver_destroy(&controller->receiver);
if (!(controller->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(controller->mutex);
return false;
return SDL_FALSE;
}
controller->control_socket = control_socket;
controller->stopped = false;
controller->video_socket = video_socket;
controller->stopped = SDL_FALSE;
return true;
return SDL_TRUE;
}
void
controller_destroy(struct controller *controller) {
SDL_DestroyCond(controller->msg_cond);
void controller_destroy(struct controller *controller) {
SDL_DestroyCond(controller->event_cond);
SDL_DestroyMutex(controller->mutex);
struct control_msg msg;
while (cbuf_take(&controller->queue, &msg)) {
control_msg_destroy(&msg);
}
receiver_destroy(&controller->receiver);
control_event_queue_destroy(&controller->queue);
}
bool
controller_push_msg(struct controller *controller,
const struct control_msg *msg) {
SDL_bool controller_push_event(struct controller *controller, const struct control_event *event) {
SDL_bool res;
mutex_lock(controller->mutex);
bool was_empty = cbuf_is_empty(&controller->queue);
bool res = cbuf_push(&controller->queue, *msg);
SDL_bool was_empty = control_event_queue_is_empty(&controller->queue);
res = control_event_queue_push(&controller->queue, event);
if (was_empty) {
cond_signal(controller->msg_cond);
cond_signal(controller->event_cond);
}
mutex_unlock(controller->mutex);
return res;
}
static bool
process_msg(struct controller *controller,
const struct control_msg *msg) {
unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int length = control_msg_serialize(msg, serialized_msg);
static SDL_bool process_event(struct controller *controller, const struct control_event *event) {
unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE];
int length = control_event_serialize(event, serialized_event);
if (!length) {
return false;
return SDL_FALSE;
}
int w = net_send_all(controller->control_socket, serialized_msg, length);
int w = net_send_all(controller->video_socket, serialized_event, length);
return w == length;
}
static int
run_controller(void *data) {
static int run_controller(void *data) {
struct controller *controller = data;
for (;;) {
mutex_lock(controller->mutex);
while (!controller->stopped && cbuf_is_empty(&controller->queue)) {
cond_wait(controller->msg_cond, 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 msgs
// stop immediately, do not process further events
mutex_unlock(controller->mutex);
break;
}
struct control_msg msg;
bool non_empty = cbuf_take(&controller->queue, &msg);
struct control_event event;
SDL_bool non_empty = control_event_queue_take(&controller->queue, &event);
SDL_assert(non_empty);
mutex_unlock(controller->mutex);
bool ok = process_msg(controller, &msg);
control_msg_destroy(&msg);
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Could not write msg to socket");
LOGD("Cannot write event to socket");
break;
}
}
return 0;
}
bool
controller_start(struct controller *controller) {
SDL_bool controller_start(struct controller *controller) {
LOGD("Starting controller thread");
controller->thread = SDL_CreateThread(run_controller, "controller",
controller);
controller->thread = SDL_CreateThread(run_controller, "controller", controller);
if (!controller->thread) {
LOGC("Could not start controller thread");
return false;
return SDL_FALSE;
}
if (!receiver_start(&controller->receiver)) {
controller_stop(controller);
SDL_WaitThread(controller->thread, NULL);
return false;
}
return true;
return SDL_TRUE;
}
void
controller_stop(struct controller *controller) {
void controller_stop(struct controller *controller) {
mutex_lock(controller->mutex);
controller->stopped = true;
cond_signal(controller->msg_cond);
controller->stopped = SDL_TRUE;
cond_signal(controller->event_cond);
mutex_unlock(controller->mutex);
}
void
controller_join(struct controller *controller) {
void controller_join(struct controller *controller) {
SDL_WaitThread(controller->thread, NULL);
receiver_join(&controller->receiver);
}

View File

@ -1,44 +1,31 @@
#ifndef CONTROLLER_H
#define CONTROLLER_H
#ifndef CONTROL_H
#define CONTROL_H
#include "control_event.h"
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h>
#include "cbuf.h"
#include "control_msg.h"
#include "net.h"
#include "receiver.h"
struct control_msg_queue CBUF(struct control_msg, 64);
struct controller {
socket_t control_socket;
socket_t video_socket;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *msg_cond;
bool stopped;
struct control_msg_queue queue;
struct receiver receiver;
SDL_cond *event_cond;
SDL_bool stopped;
struct control_event_queue queue;
};
bool
controller_init(struct controller *controller, socket_t control_socket);
SDL_bool controller_init(struct controller *controller, socket_t video_socket);
void controller_destroy(struct controller *controller);
void
controller_destroy(struct controller *controller);
SDL_bool controller_start(struct controller *controller);
void controller_stop(struct controller *controller);
void controller_join(struct controller *controller);
bool
controller_start(struct controller *controller);
void
controller_stop(struct controller *controller);
void
controller_join(struct controller *controller);
bool
controller_push_msg(struct controller *controller,
const struct control_msg *msg);
// expose simple API to hide control_event_queue
SDL_bool controller_push_event(struct controller *controller, const struct control_event *event);
#endif

View File

@ -1,10 +1,9 @@
#include "convert.h"
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
#define MAP(FROM, TO) case FROM: *to = TO; return SDL_TRUE
#define FAIL default: return SDL_FALSE
static bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
static SDL_bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP);
@ -12,8 +11,7 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
}
}
static enum android_metastate
autocomplete_metastate(enum android_metastate metastate) {
static enum android_metastate autocomplete_metastate(enum android_metastate metastate) {
// fill dependant flags
if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
metastate |= AMETA_SHIFT_ON;
@ -32,8 +30,7 @@ autocomplete_metastate(enum android_metastate metastate) {
}
static enum android_metastate
convert_meta_state(SDL_Keymod mod) {
static enum android_metastate convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) {
metastate |= AMETA_SHIFT_LEFT_ON;
@ -73,17 +70,16 @@ convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate);
}
static bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to, Uint16 mod) {
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);
MAP(SDLK_HOME, AKEYCODE_HOME);
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
MAP(SDLK_HOME, AKEYCODE_MOVE_HOME);
MAP(SDLK_END, AKEYCODE_MOVE_END);
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
@ -92,7 +88,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
}
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return false;
return SDL_FALSE;
}
// if ALT and META are not pressed, also handle letters and space
switch (from) {
@ -127,8 +123,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
}
}
static bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
static SDL_bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
@ -136,8 +131,7 @@ convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
}
}
static enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) {
static enum android_motionevent_buttons convert_mouse_buttons(Uint32 state) {
enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) {
buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
@ -157,72 +151,67 @@ convert_mouse_buttons(uint32_t state) {
return buttons;
}
bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
if (!convert_keycode_action(from->type, &to->keycode_event.action)) {
return SDL_FALSE;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
return false;
Uint16 mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) {
return SDL_FALSE;
}
to->inject_keycode.metastate = convert_meta_state(mod);
to->keycode_event.metastate = convert_meta_state(mod);
return true;
return SDL_TRUE;
}
bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) {
return false;
if (!convert_mouse_action(from->type, &to->mouse_event.action)) {
return SDL_FALSE;
}
to->inject_mouse_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
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;
return true;
return SDL_TRUE;
}
bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_mouse_event.buttons = convert_mouse_buttons(from->state);
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
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;
return true;
return SDL_TRUE;
}
bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_SCROLL;
to->inject_scroll_event.position = position;
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->inject_scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y;
to->scroll_event.hscroll = -mul * from->x;
to->scroll_event.vscroll = mul * from->y;
return true;
return SDL_TRUE;
}

View File

@ -1,10 +1,9 @@
#ifndef CONVERT_H
#define CONVERT_H
#include <stdbool.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_events.h>
#include "control_msg.h"
#include "control_event.h"
struct complete_mouse_motion_event {
SDL_MouseMotionEvent *mouse_motion_event;
@ -16,26 +15,21 @@ struct complete_mouse_wheel_event {
struct point position;
};
bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_msg *to);
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 control_event *to);
bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_msg *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
bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_msg *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 control_event *to);
// on Android, a scroll event requires the current mouse position
bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_msg *to);
SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_event *to);
#endif

View File

@ -8,22 +8,133 @@
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "buffer_util.h"
#include "events.h"
#include "frames.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h"
#include "video_buffer.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 r = net_recv_all(decoder->video_socket, header, HEADER_SIZE);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
// no partial read (net_recv_all())
SDL_assert_release(r == HEADER_SIZE);
uint64_t pts = buffer_read64be(header);
state->remaining = buffer_read32be(&header[8]);
if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
LOGE("Could not store PTS for recording");
// we cannot save the PTS, the recording would be broken
return AVERROR(ENOMEM);
}
}
SDL_assert(state->remaining);
if (buf_size > state->remaining)
buf_size = state->remaining;
ssize_t r = net_recv(decoder->video_socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
SDL_assert(state->remaining >= r);
state->remaining -= r;
return r;
}
static int read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct decoder *decoder = opaque;
ssize_t r = net_recv(decoder->video_socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
}
// set the decoded frame as ready for rendering, and notify
static void
push_frame(struct decoder *decoder) {
bool previous_frame_skipped;
video_buffer_offer_decoded_frame(decoder->video_buffer,
&previous_frame_skipped);
if (previous_frame_skipped) {
static void push_frame(struct decoder *decoder) {
SDL_bool previous_frame_consumed = frames_offer_decoded_frame(decoder->frames);
if (!previous_frame_consumed) {
// the previous EVENT_NEW_FRAME will consume this frame
return;
}
@ -33,71 +144,181 @@ push_frame(struct decoder *decoder) {
SDL_PushEvent(&new_frame_event);
}
void
decoder_init(struct decoder *decoder, struct video_buffer *vb) {
decoder->video_buffer = vb;
static void notify_stopped(void) {
SDL_Event stop_event;
stop_event.type = EVENT_DECODER_STOPPED;
SDL_PushEvent(&stop_event);
}
bool
decoder_open(struct decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx = avcodec_alloc_context3(codec);
if (!decoder->codec_ctx) {
static int run_decoder(void *data) {
struct decoder *decoder = data;
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto run_end;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGC("Could not allocate decoder context");
return false;
goto run_end;
}
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
LOGE("Could not open codec");
avcodec_free_context(&decoder->codec_ctx);
return false;
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Could not open H.264 codec");
goto run_finally_free_codec_ctx;
}
return true;
}
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto run_finally_close_codec;
}
void
decoder_close(struct decoder *decoder) {
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto run_finally_free_format_ctx;
}
bool
decoder_push(struct decoder *decoder, const AVPacket *packet) {
// 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);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto run_finally_free_format_ctx;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
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)) {
// the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
int ret;
if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
return false;
}
ret = avcodec_receive_frame(decoder->codec_ctx,
decoder->video_buffer->decoding_frame);
if (!ret) {
// a frame was received
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
return false;
}
#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
int got_picture;
int len = avcodec_decode_video2(decoder->codec_ctx,
decoder->video_buffer->decoding_frame,
&got_picture,
packet);
if (len < 0) {
LOGE("Could not decode video packet: %d", len);
return false;
}
if (got_picture) {
push_frame(decoder);
}
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) {
push_frame(decoder);
}
packet.size -= len;
packet.data += len;
}
#endif
return true;
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_free(avio_ctx->buffer);
av_free(avio_ctx);
run_finally_free_format_ctx:
avformat_free_context(format_ctx);
run_finally_close_codec:
avcodec_close(codec_ctx);
run_finally_free_codec_ctx:
avcodec_free_context(&codec_ctx);
notify_stopped();
run_end:
return 0;
}
void
decoder_interrupt(struct decoder *decoder) {
video_buffer_interrupt(decoder->video_buffer);
void decoder_init(struct decoder *decoder, struct frames *frames,
socket_t video_socket, struct recorder *recorder) {
decoder->frames = frames;
decoder->video_socket = video_socket;
decoder->recorder = recorder;
}
SDL_bool decoder_start(struct decoder *decoder) {
LOGD("Starting decoder thread");
decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder);
if (!decoder->thread) {
LOGC("Could not start decoder thread");
return SDL_FALSE;
}
return SDL_TRUE;
}
void decoder_stop(struct decoder *decoder) {
frames_stop(decoder->frames);
}
void decoder_join(struct decoder *decoder) {
SDL_WaitThread(decoder->thread, NULL);
}

View File

@ -1,29 +1,36 @@
#ifndef DECODER_H
#define DECODER_H
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h>
struct video_buffer;
#include "common.h"
#include "net.h"
struct decoder {
struct video_buffer *video_buffer;
AVCodecContext *codec_ctx;
struct frames;
struct frame_meta {
uint64_t pts;
struct frame_meta *next;
};
void
decoder_init(struct decoder *decoder, struct video_buffer *vb);
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;
};
bool
decoder_open(struct decoder *decoder, const AVCodec *codec);
void
decoder_close(struct decoder *decoder);
bool
decoder_push(struct decoder *decoder, const AVPacket *packet);
void
decoder_interrupt(struct decoder *decoder);
void decoder_init(struct decoder *decoder, struct frames *frames,
socket_t video_socket, struct recorder *recoder);
SDL_bool decoder_start(struct decoder *decoder);
void decoder_stop(struct decoder *decoder);
void decoder_join(struct decoder *decoder);
#endif

View File

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

View File

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

View File

@ -1,48 +0,0 @@
#include "device_msg.h"
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "buffer_util.h"
#include "log.h"
ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg) {
if (len < 3) {
// at least type + empty string length
return 0; // not available
}
msg->type = buf[0];
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: {
uint16_t clipboard_len = buffer_read16be(&buf[1]);
if (clipboard_len > len - 3) {
return 0; // not available
}
char *text = SDL_malloc(clipboard_len + 1);
if (!text) {
LOGW("Could not allocate text for clipboard");
return -1;
}
if (clipboard_len) {
memcpy(text, &buf[3], clipboard_len);
}
text[clipboard_len] = '\0';
msg->clipboard.text = text;
return 3 + clipboard_len;
}
default:
LOGW("Unknown device message type: %d", (int) msg->type);
return -1; // error, we cannot recover
}
}
void
device_msg_destroy(struct device_msg *msg) {
if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) {
SDL_free(msg->clipboard.text);
}
}

View File

@ -1,32 +0,0 @@
#ifndef DEVICEMSG_H
#define DEVICEMSG_H
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)
enum device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD,
};
struct device_msg {
enum device_msg_type type;
union {
struct {
char *text; // owned, to be freed by SDL_free()
} clipboard;
};
};
// return the number of bytes consumed (0 for no msg available, -1 on error)
ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg);
void
device_msg_destroy(struct device_msg *msg);
#endif

View File

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

View File

@ -2,100 +2,149 @@
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "command.h"
#include "device.h"
#include "lock_util.h"
#include "log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/"
struct request {
file_handler_action_t action;
const char *file;
};
static void
file_handler_request_destroy(struct file_handler_request *req) {
SDL_free(req->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;
}
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target) {
static void request_free(struct request *req) {
if (!req) {
return;
}
SDL_free((void *) req->file);
SDL_free((void *) req);
}
cbuf_init(&file_handler->queue);
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 false;
return SDL_FALSE;
}
if (!(file_handler->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(file_handler->mutex);
return false;
return SDL_FALSE;
}
if (serial) {
file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) {
LOGW("Could not strdup serial");
SDL_DestroyCond(file_handler->event_cond);
LOGW("Cannot strdup serial");
SDL_DestroyMutex(file_handler->mutex);
return false;
return SDL_FALSE;
}
} else {
file_handler->serial = NULL;
}
// lazy initialization
file_handler->initialized = false;
file_handler->initialized = SDL_FALSE;
file_handler->stopped = false;
file_handler->stopped = SDL_FALSE;
file_handler->current_process = PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
return true;
return SDL_TRUE;
}
void
file_handler_destroy(struct file_handler *file_handler) {
void file_handler_destroy(struct file_handler *file_handler) {
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex);
SDL_free(file_handler->serial);
struct file_handler_request req;
while (cbuf_take(&file_handler->queue, &req)) {
file_handler_request_destroy(&req);
}
request_queue_destroy(&file_handler->queue);
SDL_free((void *) file_handler->serial);
}
static process_t
install_apk(const char *serial, const char *file) {
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, const char *push_target) {
return adb_push(serial, file, push_target);
static process_t push_file(const char *serial, const char *file) {
return adb_push(serial, file, DEVICE_SDCARD_PATH);
}
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, char *file) {
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 false;
return SDL_FALSE;
}
file_handler->initialized = true;
file_handler->initialized = SDL_TRUE;
}
LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push",
file);
struct file_handler_request req = {
.action = action,
.file = file,
};
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);
bool was_empty = cbuf_is_empty(&file_handler->queue);
bool res = cbuf_push(&file_handler->queue, req);
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);
}
@ -103,14 +152,13 @@ file_handler_request(struct file_handler *file_handler,
return res;
}
static int
run_file_handler(void *data) {
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 && cbuf_is_empty(&file_handler->queue)) {
while (!file_handler->stopped && request_queue_is_empty(&file_handler->queue)) {
cond_wait(file_handler->event_cond, file_handler->mutex);
}
if (file_handler->stopped) {
@ -118,65 +166,59 @@ run_file_handler(void *data) {
mutex_unlock(file_handler->mutex);
break;
}
struct file_handler_request req;
bool non_empty = cbuf_take(&file_handler->queue, &req);
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);
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->push_target);
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 (req->action == ACTION_INSTALL_APK) {
if (process_check_success(process, "adb install")) {
LOGI("%s successfully installed", req.file);
LOGI("%s successfully installed", req->file);
} else {
LOGE("Failed to install %s", req.file);
LOGE("Failed to install %s", req->file);
}
} else {
if (process_check_success(process, "adb push")) {
LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target);
LOGI("%s successfully pushed to /sdcard/", req->file);
} else {
LOGE("Failed to push %s to %s", req.file,
file_handler->push_target);
LOGE("Failed to push %s to /sdcard/", req->file);
}
}
file_handler_request_destroy(&req);
request_free(req);
}
return 0;
}
bool
file_handler_start(struct file_handler *file_handler) {
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);
file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler", file_handler);
if (!file_handler->thread) {
LOGC("Could not start file_handler thread");
return false;
return SDL_FALSE;
}
return true;
return SDL_TRUE;
}
void
file_handler_stop(struct file_handler *file_handler) {
void file_handler_stop(struct file_handler *file_handler) {
mutex_lock(file_handler->mutex);
file_handler->stopped = true;
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("Could not terminate install process");
LOGW("Cannot terminate install process");
}
cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE;
@ -184,7 +226,6 @@ file_handler_stop(struct file_handler *file_handler) {
mutex_unlock(file_handler->mutex);
}
void
file_handler_join(struct file_handler *file_handler) {
void file_handler_join(struct file_handler *file_handler) {
SDL_WaitThread(file_handler->thread, NULL);
}

View File

@ -1,57 +1,44 @@
#ifndef FILE_HANDLER_H
#define FILE_HANDLER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h>
#include "cbuf.h"
#include "command.h"
#define REQUEST_QUEUE_SIZE 16
typedef enum {
ACTION_INSTALL_APK,
ACTION_PUSH_FILE,
} file_handler_action_t;
struct file_handler_request {
file_handler_action_t action;
char *file;
struct request_queue {
struct request *reqs[REQUEST_QUEUE_SIZE];
int tail;
int head;
};
struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler {
char *serial;
const char *push_target;
const char *serial;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *event_cond;
bool stopped;
bool initialized;
SDL_bool stopped;
SDL_bool initialized;
process_t current_process;
struct file_handler_request_queue queue;
struct request_queue queue;
};
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target);
SDL_bool file_handler_init(struct file_handler *file_handler, const char *serial);
void file_handler_destroy(struct file_handler *file_handler);
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);
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);
// take ownership of file, and will SDL_free() it
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
char *file);
SDL_bool file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
const char *file);
#endif

View File

@ -1,169 +1,62 @@
#include "fps_counter.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "lock_util.h"
#include "log.h"
#define FPS_COUNTER_INTERVAL_MS 1000
bool
fps_counter_init(struct fps_counter *counter) {
counter->mutex = SDL_CreateMutex();
if (!counter->mutex) {
return false;
}
counter->state_cond = SDL_CreateCond();
if (!counter->state_cond) {
SDL_DestroyMutex(counter->mutex);
return false;
}
counter->thread = NULL;
SDL_AtomicSet(&counter->started, 0);
// no need to initialize the other fields, they are unused until started
return true;
void fps_counter_init(struct fps_counter *counter) {
counter->started = SDL_FALSE;
// no need to initialize the other fields, they are meaningful only when
// started is true
}
void
fps_counter_destroy(struct fps_counter *counter) {
SDL_DestroyCond(counter->state_cond);
SDL_DestroyMutex(counter->mutex);
void fps_counter_start(struct fps_counter *counter) {
counter->started = SDL_TRUE;
counter->slice_start = SDL_GetTicks();
counter->nr_rendered = 0;
#ifdef SKIP_FRAMES
counter->nr_skipped = 0;
#endif
}
// must be called with mutex locked
static void
display_fps(struct fps_counter *counter) {
unsigned rendered_per_second =
counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS;
void fps_counter_stop(struct fps_counter *counter) {
counter->started = SDL_FALSE;
}
static void display_fps(struct fps_counter *counter) {
#ifdef SKIP_FRAMES
if (counter->nr_skipped) {
LOGI("%u fps (+%u frames skipped)", rendered_per_second,
counter->nr_skipped);
LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, counter->nr_skipped);
} else {
LOGI("%u fps", rendered_per_second);
#endif
LOGI("%d fps", counter->nr_rendered);
#ifdef SKIP_FRAMES
}
#endif
}
static void check_expired(struct fps_counter *counter) {
Uint32 now = SDL_GetTicks();
if (now - counter->slice_start >= 1000) {
display_fps(counter);
// add a multiple of one second
Uint32 elapsed_slices = (now - counter->slice_start) / 1000;
counter->slice_start += 1000 * elapsed_slices;
counter->nr_rendered = 0;
#ifdef SKIP_FRAMES
counter->nr_skipped = 0;
#endif
}
}
// must be called with mutex locked
static void
check_interval_expired(struct fps_counter *counter, uint32_t now) {
if (now < counter->next_timestamp) {
return;
}
display_fps(counter);
counter->nr_rendered = 0;
counter->nr_skipped = 0;
// add a multiple of the interval
uint32_t elapsed_slices =
(now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1;
counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices;
}
static int
run_fps_counter(void *data) {
struct fps_counter *counter = data;
mutex_lock(counter->mutex);
while (!counter->interrupted) {
while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) {
cond_wait(counter->state_cond, counter->mutex);
}
while (!counter->interrupted && SDL_AtomicGet(&counter->started)) {
uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now);
SDL_assert(counter->next_timestamp > now);
uint32_t remaining = counter->next_timestamp - now;
// ignore the reason (timeout or signaled), we just loop anyway
cond_wait_timeout(counter->state_cond, counter->mutex, remaining);
}
}
mutex_unlock(counter->mutex);
return 0;
}
bool
fps_counter_start(struct fps_counter *counter) {
mutex_lock(counter->mutex);
counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS;
counter->nr_rendered = 0;
counter->nr_skipped = 0;
mutex_unlock(counter->mutex);
SDL_AtomicSet(&counter->started, 1);
cond_signal(counter->state_cond);
// counter->thread is always accessed from the same thread, no need to lock
if (!counter->thread) {
counter->thread =
SDL_CreateThread(run_fps_counter, "fps counter", counter);
if (!counter->thread) {
LOGE("Could not start FPS counter thread");
return false;
}
}
return true;
}
void
fps_counter_stop(struct fps_counter *counter) {
SDL_AtomicSet(&counter->started, 0);
cond_signal(counter->state_cond);
}
bool
fps_counter_is_started(struct fps_counter *counter) {
return SDL_AtomicGet(&counter->started);
}
void
fps_counter_interrupt(struct fps_counter *counter) {
if (!counter->thread) {
return;
}
mutex_lock(counter->mutex);
counter->interrupted = true;
mutex_unlock(counter->mutex);
// wake up blocking wait
cond_signal(counter->state_cond);
}
void
fps_counter_join(struct fps_counter *counter) {
if (counter->thread) {
SDL_WaitThread(counter->thread, NULL);
}
}
void
fps_counter_add_rendered_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) {
return;
}
mutex_lock(counter->mutex);
uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now);
void fps_counter_add_rendered_frame(struct fps_counter *counter) {
check_expired(counter);
++counter->nr_rendered;
mutex_unlock(counter->mutex);
}
void
fps_counter_add_skipped_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) {
return;
}
mutex_lock(counter->mutex);
uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now);
#ifdef SKIP_FRAMES
void fps_counter_add_skipped_frame(struct fps_counter *counter) {
check_expired(counter);
++counter->nr_skipped;
mutex_unlock(counter->mutex);
}
#endif

View File

@ -1,55 +1,26 @@
#ifndef FPSCOUNTER_H
#define FPSCOUNTER_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_stdinc.h>
#include "config.h"
struct fps_counter {
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *state_cond;
// atomic so that we can check without locking the mutex
// if the FPS counter is disabled, we don't want to lock unnecessarily
SDL_atomic_t started;
// the following fields are protected by the mutex
bool interrupted;
unsigned nr_rendered;
unsigned nr_skipped;
uint32_t next_timestamp;
SDL_bool started;
Uint32 slice_start; // initialized by SDL_GetTicks()
int nr_rendered;
#ifdef SKIP_FRAMES
int nr_skipped;
#endif
};
bool
fps_counter_init(struct fps_counter *counter);
void fps_counter_init(struct fps_counter *counter);
void fps_counter_start(struct fps_counter *counter);
void fps_counter_stop(struct fps_counter *counter);
void
fps_counter_destroy(struct fps_counter *counter);
bool
fps_counter_start(struct fps_counter *counter);
void
fps_counter_stop(struct fps_counter *counter);
bool
fps_counter_is_started(struct fps_counter *counter);
// request to stop the thread (on quit)
// must be called before fps_counter_join()
void
fps_counter_interrupt(struct fps_counter *counter);
void
fps_counter_join(struct fps_counter *counter);
void
fps_counter_add_rendered_frame(struct fps_counter *counter);
void
fps_counter_add_skipped_frame(struct fps_counter *counter);
void fps_counter_add_rendered_frame(struct fps_counter *counter);
#ifdef SKIP_FRAMES
void fps_counter_add_skipped_frame(struct fps_counter *counter);
#endif
#endif

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

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

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

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

View File

@ -5,13 +5,11 @@
#include "lock_util.h"
#include "log.h"
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
// coordinates (as provided in SDL mouse events)
// 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) {
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);
@ -20,8 +18,7 @@ convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
*y = (int) (*y / scale_y) - viewport.y;
}
static struct point
get_mouse_point(struct screen *screen) {
static struct point get_mouse_point(struct screen *screen) {
int x;
int y;
SDL_GetMouseState(&x, &y);
@ -35,163 +32,84 @@ get_mouse_point(struct screen *screen) {
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) {
static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) {
// send DOWN event
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
msg.inject_keycode.keycode = keycode;
msg.inject_keycode.metastate = 0;
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0;
if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject %s (DOWN)'", name);
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) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject %s (UP)'", name);
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) {
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) {
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) {
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) {
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) {
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) {
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) {
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_msg msg;
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
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_msg(controller, &msg)) {
LOGW("Could not request 'turn screen on'");
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on");
}
}
static void
expand_notification_panel(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'expand notification panel'");
}
}
static void
collapse_notification_panel(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'collapse notification panel'");
}
}
static void
request_device_clipboard(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device clipboard");
}
}
static void
set_device_clipboard(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
// empty text
SDL_free(text);
return;
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
msg.set_clipboard.text = text;
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Could not request 'set device clipboard'");
}
}
static void
set_screen_power_mode(struct controller *controller,
enum screen_power_mode mode) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = mode;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
static void
switch_fps_counter_state(struct fps_counter *fps_counter) {
// the started state can only be written from the current thread, so there
// is no ToCToU issue
if (fps_counter_is_started(fps_counter)) {
fps_counter_stop(fps_counter);
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 {
if (fps_counter_start(fps_counter)) {
LOGI("FPS counter started");
} else {
LOGE("FPS counter starting failed");
}
LOGI("FPS counter started");
fps_counter_start(&frames->fps_counter);
}
mutex_unlock(frames->mutex);
}
static void
clipboard_paste(struct controller *controller) {
static void clipboard_paste(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
LOGW("Cannot get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
@ -200,167 +118,129 @@ clipboard_paste(struct controller *controller) {
return;
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = text;
if (!controller_push_msg(controller, &msg)) {
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("Could not request 'paste clipboard'");
LOGW("Cannot send clipboard paste event");
}
}
void
input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *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_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text);
if (!msg.inject_text.text) {
LOGW("Could not strdup input text");
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_msg(input_manager->controller, &msg)) {
SDL_free(msg.inject_text.text);
LOGW("Could not request 'inject text'");
if (!controller_push_event(input_manager->controller, &control_event)) {
SDL_free(control_event.text_event.text);
LOGW("Cannot send text event");
}
}
void
input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event,
bool control) {
// control: indicates the state of the command-line option --no-control
// ctrl: the Ctrl key
bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
// use Cmd on macOS, Ctrl on other platforms
#ifdef __APPLE__
bool cmd = !ctrl && meta;
#else
if (meta) {
// no shortcuts involve Meta on platforms other than macOS, and it must
// not be forwarded to the device
return;
}
bool cmd = ctrl; // && !meta, already guaranteed
#endif
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 shortcuts involve Alt, and it must not be forwarded to the device
// no shortcut involves Alt or Meta, and they should not be forwarded
// to the device
return;
}
struct controller *controller = input_manager->controller;
// capture all Ctrl events
if (ctrl || cmd) {
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;
bool down = event->type == SDL_KEYDOWN;
int action = down ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP;
SDL_bool repeat = event->repeat;
switch (keycode) {
case SDLK_h:
// Ctrl+h on all platform, since Cmd+h is already captured by
// the system on macOS to hide the window
if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action);
if (ctrl && !meta && !repeat) {
action_home(input_manager->controller, action);
}
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
if (control && cmd && !shift && !repeat) {
action_back(controller, action);
if (ctrl && !meta && !repeat) {
action_back(input_manager->controller, action);
}
return;
case SDLK_s:
if (control && cmd && !shift && !repeat) {
action_app_switch(controller, action);
if (ctrl && !meta && !repeat) {
action_app_switch(input_manager->controller, action);
}
return;
case SDLK_m:
// Ctrl+m on all platform, since Cmd+m is already captured by
// the system on macOS to minimize the window
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action);
if (ctrl && !meta && !repeat) {
action_menu(input_manager->controller, action);
}
return;
case SDLK_p:
if (control && cmd && !shift && !repeat) {
action_power(controller, action);
}
return;
case SDLK_o:
if (control && cmd && !shift && down) {
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
if (ctrl && !meta && !repeat) {
action_power(input_manager->controller, action);
}
return;
case SDLK_DOWN:
if (control && cmd && !shift) {
#ifdef __APPLE__
if (!ctrl && meta) {
#else
if (ctrl && !meta) {
#endif
// forward repeated events
action_volume_down(controller, action);
action_volume_down(input_manager->controller, action);
}
return;
case SDLK_UP:
if (control && cmd && !shift) {
#ifdef __APPLE__
if (!ctrl && meta) {
#else
if (ctrl && !meta) {
#endif
// forward repeated events
action_volume_up(controller, action);
}
return;
case SDLK_c:
if (control && cmd && !shift && !repeat && down) {
request_device_clipboard(controller);
action_volume_up(input_manager->controller, action);
}
return;
case SDLK_v:
if (control && cmd && !repeat && down) {
if (shift) {
// store the text in the device clipboard
set_device_clipboard(controller);
} else {
// inject the text as input events
clipboard_paste(controller);
}
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
clipboard_paste(input_manager->controller);
}
return;
case SDLK_f:
if (!shift && cmd && !repeat && down) {
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
screen_switch_fullscreen(input_manager->screen);
}
return;
case SDLK_x:
if (!shift && cmd && !repeat && down) {
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_fit(input_manager->screen);
}
return;
case SDLK_g:
if (!shift && cmd && !repeat && down) {
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
screen_resize_to_pixel_perfect(input_manager->screen);
}
return;
case SDLK_i:
if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter =
input_manager->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter);
}
return;
case SDLK_n:
if (control && cmd && !repeat && down) {
if (shift) {
collapse_notification_panel(controller);
} else {
expand_notification_panel(controller);
}
if (ctrl && !meta && !repeat && event->type == SDL_KEYDOWN) {
switch_fps_counter_state(input_manager->frames);
}
return;
}
@ -368,59 +248,51 @@ input_manager_process_key(struct input_manager *input_manager,
return;
}
if (!control) {
return;
}
struct control_msg msg;
if (input_key_from_sdl_to_android(event, &msg)) {
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'");
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) {
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_msg msg;
if (mouse_motion_from_sdl_to_android(event,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'");
struct control_event control_event;
if (mouse_motion_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse motion event");
}
}
}
static bool
is_outside_device_screen(struct input_manager *input_manager, int x, int y)
static SDL_bool is_outside_device_screen(struct input_manager *input_manager,
int x, int y)
{
return x < 0 || x >= input_manager->screen->frame_size.width ||
y < 0 || y >= input_manager->screen->frame_size.height;
}
void
input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event,
bool control) {
void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event) {
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) {
if (event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
return;
}
if (control && event->button == SDL_BUTTON_MIDDLE) {
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) {
bool outside =
is_outside_device_screen(input_manager, event->x, event->y);
SDL_bool outside= is_outside_device_screen(input_manager,
event->x,
event->y);
if (outside) {
screen_resize_to_fit(input_manager->screen);
return;
@ -429,31 +301,24 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
// otherwise, send the click event to the device
}
if (!control) {
return;
}
struct control_msg msg;
if (mouse_button_from_sdl_to_android(event,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'");
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) {
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_msg msg;
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'");
struct control_event control_event;
if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse wheel event");
}
}
}

View File

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

34
app/src/lock_util.c Normal file
View File

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

View File

@ -1,51 +1,13 @@
#ifndef LOCKUTIL_H
#define LOCKUTIL_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
// forward declarations
typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
#include "log.h"
static inline void
mutex_lock(SDL_mutex *mutex) {
if (SDL_LockMutex(mutex)) {
LOGC("Could not lock mutex");
abort();
}
}
static inline void
mutex_unlock(SDL_mutex *mutex) {
if (SDL_UnlockMutex(mutex)) {
LOGC("Could not unlock mutex");
abort();
}
}
static inline void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
if (SDL_CondWait(cond, mutex)) {
LOGC("Could not wait on condition");
abort();
}
}
static inline int
cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) {
int r = SDL_CondWaitTimeout(cond, mutex, ms);
if (r < 0) {
LOGC("Could not wait on condition with timeout");
abort();
}
return r;
}
static inline void
cond_signal(SDL_cond *cond) {
if (SDL_CondSignal(cond)) {
LOGC("Could not signal a condition");
abort();
}
}
void mutex_lock(SDL_mutex *mutex);
void mutex_unlock(SDL_mutex *mutex);
void cond_wait(SDL_cond *cond, SDL_mutex *mutex);
void cond_signal(SDL_cond *cond);
#endif

View File

@ -1,45 +1,28 @@
#include "scrcpy.h"
#include <getopt.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
#include "compat.h"
#include "config.h"
#include "log.h"
#include "recorder.h"
struct args {
const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
bool fullscreen;
bool no_control;
bool no_display;
bool help;
bool version;
bool show_touches;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
SDL_bool fullscreen;
SDL_bool help;
SDL_bool version;
SDL_bool show_touches;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
SDL_bool always_on_top;
};
static void usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr,
"Usage: %s [options]\n"
"\n"
@ -59,9 +42,6 @@ static void usage(const char *arg0) {
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
" -F, --record-format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" -h, --help\n"
" Print this help.\n"
"\n"
@ -71,40 +51,17 @@ static void usage(const char *arg0) {
" is preserved.\n"
" Default is %d%s.\n"
"\n"
" -n, --no-control\n"
" Disable device control (mirror the device in read-only).\n"
"\n"
" -N, --no-display\n"
" Do not display device (only when screen recording is\n"
" enabled).\n"
"\n"
" -p, --port port\n"
" Set the TCP port the client listens on.\n"
" Default is %d.\n"
"\n"
" --push-target path\n"
" Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n"
" Default is \"/sdcard/\".\n"
"\n"
" -r, --record file.mp4\n"
" Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n"
"\n"
" --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" -s, --serial serial\n"
" -s, --serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
" -S, --turn-screen-off\n"
" Turn the device screen off immediately.\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"
@ -115,67 +72,50 @@ static void usage(const char *arg0) {
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
" --window-title text\n"
" Set a custom window title.\n"
"\n"
"Shortcuts:\n"
"\n"
" " CTRL_OR_CMD "+f\n"
" Ctrl+f\n"
" switch fullscreen mode\n"
"\n"
" " CTRL_OR_CMD "+g\n"
" Ctrl+g\n"
" resize window to 1:1 (pixel-perfect)\n"
"\n"
" " CTRL_OR_CMD "+x\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_OR_CMD "+b\n"
" " CTRL_OR_CMD "+Backspace\n"
" Ctrl+b\n"
" Ctrl+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n"
"\n"
" " CTRL_OR_CMD "+s\n"
" Ctrl+s\n"
" click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" click on MENU\n"
"\n"
" " CTRL_OR_CMD "+Up\n"
" Ctrl+Up\n"
" click on VOLUME_UP\n"
"\n"
" " CTRL_OR_CMD "+Down\n"
" Ctrl+Down\n"
" click on VOLUME_DOWN\n"
"\n"
" " CTRL_OR_CMD "+p\n"
" Ctrl+p\n"
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" power on\n"
" turn screen on\n"
"\n"
" " CTRL_OR_CMD "+o\n"
" turn device screen off (keep mirroring)\n"
"\n"
" " CTRL_OR_CMD "+n\n"
" expand notification panel\n"
"\n"
" " CTRL_OR_CMD "+Shift+n\n"
" collapse notification panel\n"
"\n"
" " CTRL_OR_CMD "+c\n"
" copy device clipboard to computer\n"
"\n"
" " CTRL_OR_CMD "+v\n"
" Ctrl+v\n"
" paste computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+Shift+v\n"
" copy computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+i\n"
" Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
@ -187,37 +127,28 @@ static void usage(const char *arg0) {
DEFAULT_LOCAL_PORT);
}
static void
print_version(void) {
static void print_version(void) {
fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION);
fprintf(stderr, "dependencies:\n");
fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION,
SDL_PATCHLEVEL);
fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR,
LIBAVCODEC_VERSION_MINOR,
LIBAVCODEC_VERSION_MICRO);
fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR,
LIBAVFORMAT_VERSION_MINOR,
LIBAVFORMAT_VERSION_MICRO);
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
LIBAVUTIL_VERSION_MINOR,
LIBAVUTIL_VERSION_MICRO);
fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL);
fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, LIBAVCODEC_VERSION_MICRO);
fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO);
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO);
}
static bool
parse_bit_rate(char *optarg, uint32_t *bit_rate) {
static SDL_bool parse_bit_rate(char *optarg, Uint32 *bit_rate) {
char *endptr;
if (*optarg == '\0') {
LOGE("Bit-rate parameter is empty");
return false;
return SDL_FALSE;
}
long value = strtol(optarg, &endptr, 0);
int mul = 1;
if (*endptr != '\0') {
if (optarg == endptr) {
LOGE("Invalid bit-rate: %s", optarg);
return false;
return SDL_FALSE;
}
if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') {
mul = 1000000;
@ -225,157 +156,98 @@ parse_bit_rate(char *optarg, uint32_t *bit_rate) {
mul = 1000;
} else {
LOGE("Invalid bit-rate unit: %s", optarg);
return false;
return SDL_FALSE;
}
}
if (value < 0 || ((uint32_t) -1) / mul < value) {
if (value < 0 || ((Uint32) -1) / mul < value) {
LOGE("Bitrate must be positive and less than 2^32: %s", optarg);
return false;
return SDL_FALSE;
}
*bit_rate = (uint32_t) value * mul;
return true;
*bit_rate = (Uint32) value * mul;
return SDL_TRUE;
}
static bool
parse_max_size(char *optarg, uint16_t *max_size) {
static SDL_bool parse_max_size(char *optarg, Uint16 *max_size) {
char *endptr;
if (*optarg == '\0') {
LOGE("Max size parameter is empty");
return false;
return SDL_FALSE;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid max size: %s", optarg);
return false;
return SDL_FALSE;
}
if (value & ~0xffff) {
LOGE("Max size must be between 0 and 65535: %ld", value);
return false;
return SDL_FALSE;
}
*max_size = (uint16_t) value;
return true;
*max_size = (Uint16) value;
return SDL_TRUE;
}
static bool
parse_port(char *optarg, uint16_t *port) {
static SDL_bool parse_port(char *optarg, Uint16 *port) {
char *endptr;
if (*optarg == '\0') {
LOGE("Invalid port parameter is empty");
return false;
return SDL_FALSE;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid port: %s", optarg);
return false;
return SDL_FALSE;
}
if (value & ~0xffff) {
LOGE("Port out of range: %ld", value);
return false;
return SDL_FALSE;
}
*port = (uint16_t) value;
return true;
*port = (Uint16) value;
return SDL_TRUE;
}
static bool
parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) {
*format = RECORDER_FORMAT_MP4;
return true;
}
if (!strcmp(optarg, "mkv")) {
*format = RECORDER_FORMAT_MKV;
return true;
}
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
}
static enum recorder_format
guess_record_format(const char *filename) {
size_t len = strlen(filename);
if (len < 4) {
return 0;
}
const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) {
return RECORDER_FORMAT_MP4;
}
if (!strcmp(ext, ".mkv")) {
return RECORDER_FORMAT_MKV;
}
return 0;
}
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
static bool
parse_args(struct args *args, int argc, char *argv[]) {
static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, 'T'},
{"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'},
{"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL,
OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'},
{"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL,
OPT_WINDOW_TITLE},
{NULL, 0, NULL, 0 },
{"always-on-top", no_argument, NULL, 'T'},
{"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 },
};
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) {
while ((c = getopt_long(argc, argv, "b:c:fhm:p:r:s:tTv", long_options, NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) {
return false;
return SDL_FALSE;
}
break;
case 'c':
args->crop = optarg;
break;
case 'f':
args->fullscreen = true;
break;
case 'F':
if (!parse_record_format(optarg, &args->record_format)) {
return false;
}
args->fullscreen = SDL_TRUE;
break;
case 'h':
args->help = true;
args->help = SDL_TRUE;
break;
case 'm':
if (!parse_max_size(optarg, &args->max_size)) {
return false;
return SDL_FALSE;
}
break;
case 'n':
args->no_control = true;
break;
case 'N':
args->no_display = true;
break;
case 'p':
if (!parse_port(optarg, &args->port)) {
return false;
return SDL_FALSE;
}
break;
case 'r':
@ -384,73 +256,30 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 's':
args->serial = optarg;
break;
case 'S':
args->turn_screen_off = true;
break;
case 't':
args->show_touches = true;
args->show_touches = SDL_TRUE;
break;
case 'T':
args->always_on_top = true;
args->always_on_top = SDL_TRUE;
break;
case 'v':
args->version = true;
break;
case OPT_RENDER_EXPIRED_FRAMES:
args->render_expired_frames = true;
break;
case OPT_WINDOW_TITLE:
args->window_title = optarg;
break;
case OPT_PUSH_TARGET:
args->push_target = optarg;
args->version = SDL_TRUE;
break;
default:
// getopt prints the error message on stderr
return false;
return SDL_FALSE;
}
}
if (args->no_display && !args->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
if (args->no_display && args->fullscreen) {
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
return false;
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return false;
return SDL_FALSE;
}
if (args->record_format && !args->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (args->record_filename && !args->record_format) {
args->record_format = guess_record_format(args->record_filename);
if (!args->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)",
args->record_filename);
return false;
}
}
if (args->no_control && args->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
return true;
return SDL_TRUE;
}
int
main(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
@ -461,20 +290,13 @@ main(int argc, char *argv[]) {
.serial = NULL,
.crop = NULL,
.record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0,
.help = false,
.version = false,
.show_touches = false,
.help = SDL_FALSE,
.version = SDL_FALSE,
.show_touches = SDL_FALSE,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
.always_on_top = false,
.no_control = false,
.no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
.always_on_top = SDL_FALSE,
};
if (!parse_args(&args, argc, argv)) {
return 1;
@ -490,9 +312,7 @@ main(int argc, char *argv[]) {
return 0;
}
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
av_register_all();
#endif
@ -509,18 +329,11 @@ main(int argc, char *argv[]) {
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
.always_on_top = args.always_on_top,
.control = !args.no_control,
.display = !args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
};
int res = scrcpy(&options) ? 0 : 1;

View File

@ -18,8 +18,7 @@
typedef struct in_addr IN_ADDR;
#endif
socket_t
net_connect(uint32_t addr, uint16_t port) {
socket_t net_connect(Uint32 addr, Uint16 port) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
@ -33,15 +32,13 @@ net_connect(uint32_t addr, uint16_t port) {
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("connect");
net_close(sock);
return INVALID_SOCKET;
}
return sock;
}
socket_t
net_listen(uint32_t addr, uint16_t port, int backlog) {
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
@ -49,8 +46,7 @@ net_listen(uint32_t addr, uint16_t port, int backlog) {
}
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse,
sizeof(reuse)) == -1) {
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) {
perror("setsockopt(SO_REUSEADDR)");
}
@ -61,43 +57,36 @@ net_listen(uint32_t addr, uint16_t port, int backlog) {
if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("bind");
net_close(sock);
return INVALID_SOCKET;
}
if (listen(sock, backlog) == SOCKET_ERROR) {
perror("listen");
net_close(sock);
return INVALID_SOCKET;
}
return sock;
}
socket_t
net_accept(socket_t server_socket) {
socket_t net_accept(socket_t server_socket) {
SOCKADDR_IN csin;
socklen_t sinsize = sizeof(csin);
return accept(server_socket, (SOCKADDR *) &csin, &sinsize);
}
ssize_t
net_recv(socket_t socket, void *buf, size_t len) {
ssize_t net_recv(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, 0);
}
ssize_t
net_recv_all(socket_t socket, void *buf, size_t len) {
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, const 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 net_send_all(socket_t socket, const void *buf, size_t len) {
ssize_t w = 0;
while (len > 0) {
w = send(socket, buf, len, 0);
@ -110,7 +99,6 @@ net_send_all(socket_t socket, const void *buf, size_t len) {
return w;
}
bool
net_shutdown(socket_t socket, int how) {
SDL_bool net_shutdown(socket_t socket, int how) {
return !shutdown(socket, how);
}

View File

@ -1,9 +1,8 @@
#ifndef NET_H
#define NET_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_platform.h>
#include <SDL2/SDL_stdinc.h>
#ifdef __WINDOWS__
# include <winsock2.h>
@ -17,39 +16,20 @@
typedef int socket_t;
#endif
bool
net_init(void);
SDL_bool net_init(void);
void net_cleanup(void);
void
net_cleanup(void);
socket_t
net_connect(uint32_t addr, uint16_t port);
socket_t
net_listen(uint32_t addr, uint16_t port, int backlog);
socket_t
net_accept(socket_t server_socket);
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_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);
// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
bool
net_shutdown(socket_t socket, int how);
bool
net_close(socket_t socket);
SDL_bool net_shutdown(socket_t socket, int how);
SDL_bool net_close(socket_t socket);
#endif

View File

@ -1,107 +0,0 @@
#include "receiver.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_clipboard.h>
#include "config.h"
#include "device_msg.h"
#include "lock_util.h"
#include "log.h"
bool
receiver_init(struct receiver *receiver, socket_t control_socket) {
if (!(receiver->mutex = SDL_CreateMutex())) {
return false;
}
receiver->control_socket = control_socket;
return true;
}
void
receiver_destroy(struct receiver *receiver) {
SDL_DestroyMutex(receiver->mutex);
}
static void
process_msg(struct receiver *receiver, struct device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD:
LOGI("Device clipboard copied");
SDL_SetClipboardText(msg->clipboard.text);
break;
}
}
static ssize_t
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
size_t head = 0;
for (;;) {
struct device_msg msg;
ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg);
if (r == -1) {
return -1;
}
if (r == 0) {
return head;
}
process_msg(receiver, &msg);
device_msg_destroy(&msg);
head += r;
SDL_assert(head <= len);
if (head == len) {
return head;
}
}
}
static int
run_receiver(void *data) {
struct receiver *receiver = data;
unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE];
size_t head = 0;
for (;;) {
SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf,
DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
if (r <= 0) {
LOGD("Receiver stopped");
break;
}
ssize_t consumed = process_msgs(receiver, buf, r);
if (consumed == -1) {
// an error occurred
break;
}
if (consumed) {
// shift the remaining data in the buffer
memmove(buf, &buf[consumed], r - consumed);
head = r - consumed;
}
}
return 0;
}
bool
receiver_start(struct receiver *receiver) {
LOGD("Starting receiver thread");
receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver);
if (!receiver->thread) {
LOGC("Could not start receiver thread");
return false;
}
return true;
}
void
receiver_join(struct receiver *receiver) {
SDL_WaitThread(receiver->thread, NULL);
}

View File

@ -1,32 +0,0 @@
#ifndef RECEIVER_H
#define RECEIVER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
// receive events from the device
// managed by the controller
struct receiver {
socket_t control_socket;
SDL_Thread *thread;
SDL_mutex *mutex;
};
bool
receiver_init(struct receiver *receiver, socket_t control_socket);
void
receiver_destroy(struct receiver *receiver);
bool
receiver_start(struct receiver *receiver);
// no receiver_stop(), it will automatically stop on control_socket shutdown
void
receiver_join(struct receiver *receiver);
#endif

View File

@ -1,189 +1,80 @@
#include "recorder.h"
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include "compat.h"
#include "config.h"
#include "lock_util.h"
#include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
// In ffmpeg/doc/APIchanges:
// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h
// Add AVStream.codecpar, deprecate AVStream.codec.
#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \
|| (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0))
# define LAVF_NEW_CODEC_API
#endif
static const AVOutputFormat *
find_muxer(const char *name) {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
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 {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
#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, name));
} while (oformat && strcmp(oformat->name, "mp4"));
return oformat;
}
static struct record_packet *
record_packet_new(const AVPacket *packet) {
struct record_packet *rec = SDL_malloc(sizeof(*rec));
if (!rec) {
return NULL;
}
if (av_packet_ref(&rec->packet, packet)) {
SDL_free(rec);
return NULL;
}
rec->next = NULL;
return rec;
}
static void
record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet);
SDL_free(rec);
}
static void
recorder_queue_init(struct recorder_queue *queue) {
queue->first = NULL;
// queue->last is undefined if queue->first == NULL
}
static inline bool
recorder_queue_is_empty(struct recorder_queue *queue) {
return !queue->first;
}
static bool
recorder_queue_push(struct recorder_queue *queue, const AVPacket *packet) {
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
return false;
}
rec->next = NULL;
if (recorder_queue_is_empty(queue)) {
queue->first = queue->last = rec;
} else {
// chain rec after the (current) last packet
queue->last->next = rec;
// the last packet is now rec
queue->last = rec;
}
return true;
}
static inline struct record_packet *
recorder_queue_take(struct recorder_queue *queue) {
SDL_assert(!recorder_queue_is_empty(queue));
struct record_packet *rec = queue->first;
SDL_assert(rec);
queue->first = rec->next;
// no need to update queue->last if the queue is left empty:
// queue->last is undefined if queue->first == NULL
return rec;
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
struct record_packet *rec = queue->first;
while (rec) {
struct record_packet *current = rec;
rec = rec->next;
record_packet_delete(current);
}
queue->first = NULL;
}
bool
recorder_init(struct recorder *recorder,
const char *filename,
enum recorder_format format,
struct size declared_frame_size) {
SDL_bool recorder_init(struct recorder *recorder, const char *filename,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
return false;
LOGE("Cannot strdup filename");
return SDL_FALSE;
}
recorder->mutex = SDL_CreateMutex();
if (!recorder->mutex) {
LOGC("Could not create mutex");
SDL_free(recorder->filename);
return false;
}
recorder->queue_cond = SDL_CreateCond();
if (!recorder->queue_cond) {
LOGC("Could not create cond");
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
return false;
}
recorder_queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
recorder->header_written = SDL_FALSE;
return true;
return SDL_TRUE;
}
void
recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
void recorder_destroy(struct recorder *recorder) {
SDL_free(recorder->filename);
}
static const char *
recorder_get_format_name(enum recorder_format format) {
switch (format) {
case RECORDER_FORMAT_MP4: return "mp4";
case RECORDER_FORMAT_MKV: return "matroska";
default: return NULL;
}
}
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
const char *format_name = recorder_get_format_name(recorder->format);
SDL_assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
return false;
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 false;
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 *) format;
recorder->ctx->oformat = (AVOutputFormat *) mp4;
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
avformat_free_context(recorder->ctx);
return false;
return SDL_FALSE;
}
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
#ifdef LAVF_NEW_CODEC_API
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
@ -196,6 +87,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
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);
@ -203,46 +95,34 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
avformat_free_context(recorder->ctx);
return false;
return SDL_FALSE;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
return SDL_TRUE;
}
void
recorder_close(struct recorder *recorder) {
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);
recorder->failed = true;
}
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
}
static bool
recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
SDL_bool recorder_write_header(struct recorder *recorder, AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
uint8_t *extradata = SDL_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
return false;
LOGC("Cannot allocate extradata");
return SDL_FALSE;
}
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
#ifdef LAVF_NEW_CODEC_API
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
#else
@ -253,125 +133,23 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
return false;
SDL_free(extradata);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return SDL_FALSE;
}
return true;
return SDL_TRUE;
}
static void
recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
bool
recorder_write(struct recorder *recorder, AVPacket *packet) {
SDL_bool recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = recorder_write_header(recorder, packet);
SDL_bool ok = recorder_write_header(recorder, packet);
if (!ok) {
return false;
return SDL_FALSE;
}
recorder->header_written = true;
return true;
recorder->header_written = SDL_TRUE;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
}
static int
run_recorder(void *data) {
struct recorder *recorder = data;
for (;;) {
mutex_lock(recorder->mutex);
while (!recorder->stopped &&
recorder_queue_is_empty(&recorder->queue)) {
cond_wait(recorder->queue_cond, recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && recorder_queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex);
break;
}
struct record_packet *rec = recorder_queue_take(&recorder->queue);
mutex_unlock(recorder->mutex);
bool ok = recorder_write(recorder, &rec->packet);
record_packet_delete(rec);
if (!ok) {
LOGE("Could not record packet");
mutex_lock(recorder->mutex);
recorder->failed = true;
// discard pending packets
recorder_queue_clear(&recorder->queue);
mutex_unlock(recorder->mutex);
break;
}
}
LOGD("Recorder thread ended");
return 0;
}
bool
recorder_start(struct recorder *recorder) {
LOGD("Starting recorder thread");
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder);
if (!recorder->thread) {
LOGC("Could not start recorder thread");
return false;
}
return true;
}
void
recorder_stop(struct recorder *recorder) {
mutex_lock(recorder->mutex);
recorder->stopped = true;
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
}
void
recorder_join(struct recorder *recorder) {
SDL_WaitThread(recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex);
SDL_assert(!recorder->stopped);
if (recorder->failed) {
// reject any new packet (this will stop the stream)
return false;
}
bool ok = recorder_queue_push(&recorder->queue, packet);
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
return ok;
}

View File

@ -1,66 +1,25 @@
#ifndef RECORDER_H
#define RECORDER_H
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_stdinc.h>
#include "common.h"
enum recorder_format {
RECORDER_FORMAT_MP4 = 1,
RECORDER_FORMAT_MKV,
};
struct record_packet {
AVPacket packet;
struct record_packet *next;
};
struct recorder_queue {
struct record_packet *first;
struct record_packet *last; // undefined if first is NULL
};
struct recorder {
char *filename;
enum recorder_format format;
AVFormatContext *ctx;
struct size declared_frame_size;
bool header_written;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *queue_cond;
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
SDL_bool header_written;
};
bool
recorder_init(struct recorder *recorder, const char *filename,
enum recorder_format format, 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);
void
recorder_destroy(struct recorder *recorder);
SDL_bool recorder_open(struct recorder *recorder, AVCodec *input_codec);
void recorder_close(struct recorder *recorder);
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec);
void
recorder_close(struct recorder *recorder);
bool
recorder_start(struct recorder *recorder);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
SDL_bool recorder_write(struct recorder *recorder, AVPacket *packet);
#endif

View File

@ -9,12 +9,12 @@
#include "command.h"
#include "common.h"
#include "compat.h"
#include "controller.h"
#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 "log.h"
@ -23,72 +23,22 @@
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "stream.h"
#include "tiny_xpm.h"
#include "video_buffer.h"
static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER;
static struct fps_counter fps_counter;
static struct video_buffer video_buffer;
static struct stream stream;
static struct frames frames;
static struct decoder decoder;
static struct recorder recorder;
static struct controller controller;
static struct file_handler file_handler;
static struct recorder recorder;
static struct input_manager input_manager = {
.controller = &controller,
.video_buffer = &video_buffer,
.frames = &frames,
.screen = &screen,
};
// init SDL and set appropriate hints
static bool
sdl_init_and_configure(bool display) {
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
if (SDL_Init(flags)) {
LOGC("Could not initialize SDL: %s", SDL_GetError());
return false;
}
atexit(SDL_Quit);
if (!display) {
return true;
}
// Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
LOGW("Could not enable bilinear filtering");
}
#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
// Handle a click to gain focus as any other click
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
LOGW("Could not enable mouse focus clickthrough");
}
#endif
#ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
// Disable compositor bypassing on X11
if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) {
LOGW("Could not disable X11 compositor bypass");
}
#endif
// Do not minimize on focus loss
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
LOGW("Could not disable minimize on focus loss");
}
// Do not disable the screensaver when scrcpy is running
SDL_EnableScreenSaver();
return true;
}
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@ -99,10 +49,8 @@ sdl_init_and_configure(bool display) {
//
// <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) {
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);
}
@ -110,117 +58,75 @@ event_watcher(void *data, SDL_Event *event) {
}
#endif
static bool
is_apk(const char *file) {
static SDL_bool is_apk(const char *file) {
const char *ext = strrchr(file, '.');
return ext && !strcmp(ext, ".apk");
}
enum event_result {
EVENT_RESULT_CONTINUE,
EVENT_RESULT_STOPPED_BY_USER,
EVENT_RESULT_STOPPED_BY_EOS,
};
static enum event_result
handle_event(SDL_Event *event, bool control) {
switch (event->type) {
case EVENT_STREAM_STOPPED:
LOGD("Video stream stopped");
return EVENT_RESULT_STOPPED_BY_EOS;
case SDL_QUIT:
LOGD("User requested to quit");
return EVENT_RESULT_STOPPED_BY_USER;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = true;
// this is the very first frame, show the window
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &video_buffer)) {
return EVENT_RESULT_CONTINUE;
}
break;
case SDL_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
break;
case SDL_TEXTINPUT:
if (!control) {
break;
}
input_manager_process_text_input(&input_manager, &event->text);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
// some key events do not interact with the device, so process the
// event even if control is disabled
input_manager_process_key(&input_manager, &event->key, control);
break;
case SDL_MOUSEMOTION:
if (!control) {
break;
}
input_manager_process_mouse_motion(&input_manager, &event->motion);
break;
case SDL_MOUSEWHEEL:
if (!control) {
break;
}
input_manager_process_mouse_wheel(&input_manager, &event->wheel);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process
// the event even if control is disabled
input_manager_process_mouse_button(&input_manager, &event->button,
control);
break;
case SDL_DROPFILE: {
if (!control) {
break;
}
file_handler_action_t action;
if (is_apk(event->drop.file)) {
action = ACTION_INSTALL_APK;
} else {
action = ACTION_PUSH_FILE;
}
file_handler_request(&file_handler, action, event->drop.file);
break;
}
}
return EVENT_RESULT_CONTINUE;
}
static bool
event_loop(bool display, bool control) {
static SDL_bool event_loop(void) {
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (display) {
SDL_AddEventWatch(event_watcher, NULL);
}
SDL_AddEventWatch(event_watcher, NULL);
#endif
SDL_Event event;
while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, control);
switch (result) {
case EVENT_RESULT_STOPPED_BY_USER:
return true;
case EVENT_RESULT_STOPPED_BY_EOS:
return false;
case EVENT_RESULT_CONTINUE:
switch (event.type) {
case EVENT_DECODER_STOPPED:
LOGD("Video decoder stopped");
return SDL_FALSE;
case SDL_QUIT:
LOGD("User requested to quit");
return SDL_TRUE;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = SDL_TRUE;
// this is the very first frame, show the window
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &frames)) {
return SDL_FALSE;
}
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
break;
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);
break;
case SDL_MOUSEMOTION:
input_manager_process_mouse_motion(&input_manager, &event.motion);
break;
case SDL_MOUSEWHEEL:
input_manager_process_mouse_wheel(&input_manager, &event.wheel);
break;
case SDL_MOUSEBUTTONDOWN:
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 false;
return SDL_FALSE;
}
static process_t
set_show_touches_enabled(const char *serial, bool enabled) {
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
@ -228,14 +134,12 @@ set_show_touches_enabled(const char *serial, bool enabled) {
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void
wait_show_touches(process_t process) {
static void wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
static SDL_LogPriority sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
@ -259,7 +163,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
}
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
if (!local_fmt) {
LOGC("Could not allocate string");
LOGC("Cannot allocate string");
return;
}
// strcpy is safe here, the destination is large enough
@ -269,206 +173,139 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
SDL_free(local_fmt);
}
bool
scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
struct server_params params = {
.crop = options->crop,
.local_port = options->port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.control = options->control,
};
if (!server_start(&server, options->serial, &params)) {
return false;
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)) {
return SDL_FALSE;
}
process_t proc_show_touches = PROCESS_NONE;
bool show_touches_waited;
SDL_bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, true);
show_touches_waited = false;
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
show_touches_waited = SDL_FALSE;
}
bool ret = false;
SDL_bool ret = SDL_TRUE;
bool fps_counter_initialized = false;
bool video_buffer_initialized = false;
bool file_handler_initialized = false;
bool recorder_initialized = false;
bool stream_started = false;
bool controller_initialized = false;
bool controller_started = false;
if (!sdl_init_and_configure(options->display)) {
goto end;
if (!sdl_init_and_configure()) {
ret = SDL_FALSE;
goto finally_destroy_server;
}
if (!server_connect_to(&server)) {
goto end;
socket_t device_socket = server_connect_to(&server);
if (device_socket == INVALID_SOCKET) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
char device_name[DEVICE_NAME_FIELD_LENGTH];
struct size frame_size;
// screenrecord does not send frames when the screen content does not
// change therefore, we transmit the screen size before the video stream,
// to be able to init the window immediately
if (!device_read_info(server.video_socket, device_name, &frame_size)) {
goto end;
// screenrecord does not send frames when the screen content does not change
// 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);
ret = SDL_FALSE;
goto finally_destroy_server;
}
struct decoder *dec = NULL;
if (options->display) {
if (!fps_counter_init(&fps_counter)) {
goto end;
}
fps_counter_initialized = true;
if (!frames_init(&frames)) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
if (!video_buffer_init(&video_buffer, &fps_counter,
options->render_expired_frames)) {
goto end;
}
video_buffer_initialized = true;
if (options->control) {
if (!file_handler_init(&file_handler, server.serial,
options->push_target)) {
goto end;
}
file_handler_initialized = true;
}
decoder_init(&decoder, &video_buffer);
dec = &decoder;
if (!file_handler_init(&file_handler, server.serial)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_frames;
}
struct recorder *rec = NULL;
if (record) {
if (!recorder_init(&recorder,
options->record_filename,
options->record_format,
frame_size)) {
goto end;
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;
recorder_initialized = true;
}
av_log_set_callback(av_log_callback);
stream_init(&stream, server.video_socket, dec, rec);
decoder_init(&decoder, &frames, device_socket, rec);
// now we consumed the header values, the socket receives the video stream
// start the stream
if (!stream_start(&stream)) {
goto end;
// start the decoder
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_recorder;
}
stream_started = true;
if (options->display) {
if (options->control) {
if (!controller_init(&controller, server.control_socket)) {
goto end;
}
controller_initialized = true;
if (!controller_init(&controller, device_socket)) {
ret = SDL_FALSE;
goto finally_stop_decoder;
}
if (!controller_start(&controller)) {
goto end;
}
controller_started = true;
}
if (!controller_start(&controller)) {
ret = SDL_FALSE;
goto finally_destroy_controller;
}
const char *window_title =
options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size,
options->always_on_top)) {
goto end;
}
if (options->turn_screen_off) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
if (!controller_push_msg(&controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
if (!screen_init_rendering(&screen, device_name, frame_size, options->always_on_top)) {
ret = SDL_FALSE;
goto finally_stop_and_join_controller;
}
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = true;
show_touches_waited = SDL_TRUE;
}
ret = event_loop(options->display, options->control);
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
ret = event_loop();
LOGD("quit...");
screen_destroy(&screen);
end:
// stop stream and controller so that they don't continue once their socket
// is shutdown
if (stream_started) {
stream_stop(&stream);
}
if (controller_started) {
controller_stop(&controller);
}
if (file_handler_initialized) {
file_handler_stop(&file_handler);
}
if (fps_counter_initialized) {
fps_counter_interrupt(&fps_counter);
}
// shutdown the sockets and kill the server
finally_stop_and_join_controller:
controller_stop(&controller);
controller_join(&controller);
finally_destroy_controller:
controller_destroy(&controller);
finally_stop_decoder:
decoder_stop(&decoder);
// stop the server before decoder_join() to wake up the decoder
server_stop(&server);
// now that the sockets are shutdown, the stream and controller are
// interrupted, we can join them
if (stream_started) {
stream_join(&stream);
}
if (controller_started) {
controller_join(&controller);
}
if (controller_initialized) {
controller_destroy(&controller);
}
if (recorder_initialized) {
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);
}
if (file_handler_initialized) {
file_handler_join(&file_handler);
file_handler_destroy(&file_handler);
}
if (video_buffer_initialized) {
video_buffer_destroy(&video_buffer);
}
if (fps_counter_initialized) {
fps_counter_join(&fps_counter);
fps_counter_destroy(&fps_counter);
}
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, false);
proc_show_touches = set_show_touches_enabled(options->serial, SDL_FALSE);
wait_show_touches(proc_show_touches);
}

View File

@ -1,30 +1,20 @@
#ifndef SCRCPY_H
#define SCRCPY_H
#include <stdbool.h>
#include <stdint.h>
#include <recorder.h>
#include <SDL2/SDL_stdinc.h>
struct scrcpy_options {
const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool show_touches;
bool fullscreen;
bool always_on_top;
bool control;
bool display;
bool turn_screen_off;
bool render_expired_frames;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
SDL_bool show_touches;
SDL_bool fullscreen;
SDL_bool always_on_top;
};
bool
scrcpy(const struct scrcpy_options *options);
SDL_bool scrcpy(const struct scrcpy_options *options);
#endif

View File

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

View File

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

View File

@ -2,149 +2,105 @@
#include <errno.h>
#include <inttypes.h>
#include <libgen.h>
#include <stdint.h>
#include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h"
#include "command.h"
#include "log.h"
#include "net.h"
#define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
static const char *
get_server_path(void) {
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
if (server_path_env) {
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env);
// if the envvar is set, use it
return server_path_env;
}
#ifndef PORTABLE
LOGD("Using server: " DEFAULT_SERVER_PATH);
// the absolute path is hardcoded
return DEFAULT_SERVER_PATH;
#ifdef OVERRIDE_SERVER_PATH
# define DEFAULT_SERVER_PATH OVERRIDE_SERVER_PATH
#else
// use scrcpy-server.jar in the same directory as the executable
char *executable_path = get_executable_path();
if (!executable_path) {
LOGE("Could not get executable path, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return SERVER_FILENAME;
}
char *dir = dirname(executable_path);
size_t dirlen = strlen(dir);
// sizeof(SERVER_FILENAME) gives statically the size including the null byte
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
char *server_path = SDL_malloc(len);
if (!server_path) {
LOGE("Could not alloc server path string, "
"using " SERVER_FILENAME " from current directory");
SDL_free(executable_path);
return SERVER_FILENAME;
}
memcpy(server_path, dir, dirlen);
server_path[dirlen] = PATH_SEPARATOR;
memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME));
// the final null byte has been copied with SERVER_FILENAME
SDL_free(executable_path);
LOGD("Using server (portable): %s", server_path);
return server_path;
# define DEFAULT_SERVER_PATH PREFIX PREFIXED_SERVER_PATH
#endif
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static const char *get_server_path(void) {
const char *server_path = getenv("SCRCPY_SERVER_PATH");
if (!server_path) {
server_path = DEFAULT_SERVER_PATH;
}
return server_path;
}
static bool
push_server(const char *serial) {
static SDL_bool push_server(const char *serial) {
process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH);
return process_check_success(process, "adb push");
}
static bool
enable_tunnel_reverse(const char *serial, uint16_t local_port) {
static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse");
}
static bool
disable_tunnel_reverse(const char *serial) {
static SDL_bool disable_tunnel_reverse(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove");
}
static bool
enable_tunnel_forward(const char *serial, uint16_t local_port) {
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 bool
disable_tunnel_forward(const char *serial, uint16_t local_port) {
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 bool
enable_tunnel(struct server *server) {
static SDL_bool enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return true;
return SDL_TRUE;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = true;
server->tunnel_forward = SDL_TRUE;
return enable_tunnel_forward(server->serial, server->local_port);
}
static bool
disable_tunnel(struct server *server) {
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(struct server *server, const struct server_params *params) {
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) {
char max_size_string[6];
char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_size_string, "%"PRIu16, max_size);
sprintf(bit_rate_string, "%"PRIu32, bit_rate);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
"CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"app_process",
"/", // unused
"com.genymobile.scrcpy.Server",
max_size_string,
bit_rate_string,
server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
tunnel_forward ? "true" : "false",
crop ? crop : "-",
send_frame_meta ? "true" : "false",
};
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
#define IPV4_LOCALHOST 0x7F000001
static socket_t
listen_on_port(uint16_t port) {
static socket_t listen_on_port(Uint16 port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t
connect_and_read_byte(uint16_t port) {
static socket_t connect_and_read_byte(Uint16 port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
if (socket == INVALID_SOCKET) {
return INVALID_SOCKET;
@ -153,16 +109,14 @@ connect_and_read_byte(uint16_t port) {
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(socket, &byte, 1) != 1) {
if (net_recv_all(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
net_close(socket);
return INVALID_SOCKET;
}
return socket;
}
static socket_t
connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
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);
@ -177,42 +131,40 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
return INVALID_SOCKET;
}
static void
close_socket(socket_t *socket) {
static void close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
LOGW("Could not close socket");
LOGW("Cannot close socket");
return;
}
*socket = INVALID_SOCKET;
}
void
server_init(struct server *server) {
void server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER;
}
bool
server_start(struct server *server, const char *serial,
const struct server_params *params) {
server->local_port = params->local_port;
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 false;
return SDL_FALSE;
}
}
if (!push_server(serial)) {
SDL_free(server->serial);
return false;
SDL_free((void *) server->serial);
return SDL_FALSE;
}
if (!enable_tunnel(server)) {
SDL_free(server->serial);
return false;
SDL_free((void *) server->serial);
return SDL_FALSE;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
@ -221,92 +173,67 @@ server_start(struct server *server, const char *serial,
// 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.
// 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(params->local_port);
server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, params->local_port);
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
SDL_free(server->serial);
return false;
SDL_free((void *) server->serial);
return SDL_FALSE;
}
}
// server will connect to our server socket
server->process = execute_server(server, params);
server->process = execute_server(serial, max_size, bit_rate,
server->tunnel_forward, crop,
send_frame_meta);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
SDL_free(server->serial);
return false;
SDL_free((void *) server->serial);
return SDL_FALSE;
}
server->tunnel_enabled = true;
server->tunnel_enabled = SDL_TRUE;
return true;
return SDL_TRUE;
}
bool
server_connect_to(struct server *server) {
socket_t server_connect_to(struct server *server) {
if (!server->tunnel_forward) {
server->video_socket = net_accept(server->server_socket);
if (server->video_socket == INVALID_SOCKET) {
return false;
}
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);
}
server->control_socket = net_accept(server->server_socket);
if (server->control_socket == INVALID_SOCKET) {
// the video_socket will be clean up on destroy
return false;
}
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);
} else {
uint32_t attempts = 100;
uint32_t delay = 100; // ms
server->video_socket =
connect_to_server(server->local_port, attempts, delay);
if (server->video_socket == INVALID_SOCKET) {
return false;
}
// we know that the device is listening, we don't need several attempts
server->control_socket =
net_connect(IPV4_LOCALHOST, server->local_port);
if (server->control_socket == INVALID_SOCKET) {
return false;
}
}
// we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure
server->tunnel_enabled = false;
server->tunnel_enabled = SDL_FALSE;
return true;
return server->device_socket;
}
void
server_stop(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
}
if (server->video_socket != INVALID_SOCKET) {
close_socket(&server->video_socket);
}
if (server->control_socket != INVALID_SOCKET) {
close_socket(&server->control_socket);
}
void server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
LOGW("Could not terminate server");
LOGW("Cannot terminate server");
}
cmd_simple_wait(server->process, NULL); // ignore exit code
@ -318,7 +245,12 @@ server_stop(struct server *server) {
}
}
void
server_destroy(struct server *server) {
SDL_free(server->serial);
void server_destroy(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
}
if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket);
}
SDL_free((void *) server->serial);
}

View File

@ -1,61 +1,46 @@
#ifndef SERVER_H
#define SERVER_H
#include <stdbool.h>
#include <stdint.h>
#include "command.h"
#include "net.h"
struct server {
char *serial;
const char *serial;
process_t process;
socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket;
socket_t control_socket;
uint16_t local_port;
bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
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
};
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
#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, \
}
struct server_params {
const char *crop;
uint16_t local_port;
uint16_t max_size;
uint32_t bit_rate;
bool control;
};
// init default values
void
server_init(struct server *server);
void server_init(struct server *server);
// push, enable tunnel et start the server
bool
server_start(struct server *server, const char *serial,
const struct server_params *params);
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);
// block until the communication with the server is established
bool
server_connect_to(struct server *server);
socket_t server_connect_to(struct server *server);
// disconnect and kill the server process
void
server_stop(struct server *server);
void server_stop(struct server *server);
// close and release sockets
void
server_destroy(struct server *server);
void server_destroy(struct server *server);
#endif

View File

@ -3,15 +3,7 @@
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
# include <windows.h>
# include <tchar.h>
#endif
#include <SDL2/SDL_stdinc.h>
size_t
xstrncpy(char *dest, const char *src, size_t n) {
size_t xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
for (i = 0; i < n - 1 && src[i] != '\0'; ++i)
dest[i] = src[i];
@ -20,8 +12,7 @@ xstrncpy(char *dest, const char *src, size_t n) {
return src[i] == '\0' ? i : n;
}
size_t
xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
const char *const *remaining = tokens;
const char *token = *remaining++;
size_t i = 0;
@ -44,10 +35,9 @@ truncated:
return n;
}
char *
strquote(const char *src) {
char *strquote(const char *src) {
size_t len = strlen(src);
char *quoted = SDL_malloc(len + 3);
char *quoted = malloc(len + 3);
if (!quoted) {
return NULL;
}
@ -57,55 +47,3 @@ strquote(const char *src) {
quoted[len + 2] = '\0';
return quoted;
}
size_t
utf8_truncation_index(const char *utf8, size_t max_len) {
size_t len = strlen(utf8);
if (len <= max_len) {
return len;
}
len = max_len;
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
// the next byte is not the start of a new UTF-8 codepoint
// so if we would cut there, the character would be truncated
len--;
}
return len;
}
#ifdef _WIN32
wchar_t *
utf8_to_wide_char(const char *utf8) {
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
if (!len) {
return NULL;
}
wchar_t *wide = SDL_malloc(len * sizeof(wchar_t));
if (!wide) {
return NULL;
}
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, len);
return wide;
}
char *
utf8_from_wide_char(const wchar_t *ws) {
int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
if (!len) {
return NULL;
}
char *utf8 = SDL_malloc(len);
if (!utf8) {
return NULL;
}
WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL);
return utf8;
}
#endif

View File

@ -9,32 +9,15 @@
// - it does not write useless bytes if strlen(src) < n
// - it returns the number of chars actually written (max n-1) if src has
// been copied completely, or n if src has been truncated
size_t
xstrncpy(char *dest, const char *src, size_t n);
size_t xstrncpy(char *dest, const char *src, size_t n);
// join tokens by sep into dst
// returns the number of chars actually written (max n-1) if no trucation
// occurred, or n if truncated
size_t
xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
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);
// return the index to truncate a UTF-8 string at a valid position
size_t
utf8_truncation_index(const char *utf8, size_t max_len);
#ifdef _WIN32
// convert a UTF-8 string to a wchar_t string
// returns the new allocated string, to be freed by the caller
wchar_t *
utf8_to_wide_char(const char *utf8);
char *
utf8_from_wide_char(const wchar_t *s);
#endif
char *strquote(const char *src);
#endif

View File

@ -1,301 +0,0 @@
#include "stream.h"
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "buffer_util.h"
#include "decoder.h"
#include "events.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h"
#define BUFSIZE 0x10000
#define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1)
static bool
stream_recv_packet(struct stream *stream, AVPacket *packet) {
// 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.
uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r < HEADER_SIZE) {
return false;
}
uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]);
SDL_assert(len);
if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet");
return false;
}
r = net_recv_all(stream->socket, packet->data, len);
if (r < len) {
av_packet_unref(packet);
return false;
}
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
return true;
}
static void
notify_stopped(void) {
SDL_Event stop_event;
stop_event.type = EVENT_STREAM_STOPPED;
SDL_PushEvent(&stop_event);
}
static bool
process_config_packet(struct stream *stream, AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
LOGE("Could not send config packet to recorder");
return false;
}
return true;
}
static bool
process_frame(struct stream *stream, AVPacket *packet) {
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
return false;
}
if (stream->recorder) {
packet->dts = packet->pts;
if (!recorder_push(stream->recorder, packet)) {
LOGE("Could not send packet to recorder");
return false;
}
}
return true;
}
static bool
stream_parse(struct stream *stream, AVPacket *packet) {
uint8_t *in_data = packet->data;
int in_len = packet->size;
uint8_t *out_data = NULL;
int out_len = 0;
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
&out_data, &out_len, in_data, in_len,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
SDL_assert(r == in_len);
SDL_assert(out_len == in_len);
if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;
}
bool ok = process_frame(stream, packet);
if (!ok) {
LOGE("Could not process frame");
return false;
}
return true;
}
static bool
stream_push_packet(struct stream *stream, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (stream->has_pending || is_config) {
size_t offset;
if (stream->has_pending) {
offset = stream->pending.size;
if (av_grow_packet(&stream->pending, packet->size)) {
LOGE("Could not grow packet");
return false;
}
} else {
offset = 0;
if (av_new_packet(&stream->pending, packet->size)) {
LOGE("Could not create packet");
return false;
}
stream->has_pending = true;
}
memcpy(stream->pending.data + offset, packet->data, packet->size);
if (!is_config) {
// prepare the concat packet to send to the decoder
stream->pending.pts = packet->pts;
stream->pending.dts = packet->dts;
stream->pending.flags = packet->flags;
packet = &stream->pending;
}
}
if (is_config) {
// config packet
bool ok = process_config_packet(stream, packet);
if (!ok) {
return false;
}
} else {
// data packet
bool ok = stream_parse(stream, packet);
if (stream->has_pending) {
// the pending packet must be discarded (consumed or error)
stream->has_pending = false;
av_packet_unref(&stream->pending);
}
if (!ok) {
return false;
}
}
return true;
}
static int
run_stream(void *data) {
struct stream *stream = data;
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto end;
}
stream->codec_ctx = avcodec_alloc_context3(codec);
if (!stream->codec_ctx) {
LOGC("Could not allocate codec context");
goto end;
}
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder");
goto finally_free_codec_ctx;
}
if (stream->recorder) {
if (!recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_decoder;
}
if (!recorder_start(stream->recorder)) {
LOGE("Could not start recorder");
goto finally_close_recorder;
}
}
stream->parser = av_parser_init(AV_CODEC_ID_H264);
if (!stream->parser) {
LOGE("Could not initialize parser");
goto finally_stop_and_join_recorder;
}
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
for (;;) {
AVPacket packet;
bool ok = stream_recv_packet(stream, &packet);
if (!ok) {
// end of stream
break;
}
ok = stream_push_packet(stream, &packet);
av_packet_unref(&packet);
if (!ok) {
// cannot process packet (error already logged)
break;
}
}
LOGD("End of frames");
if (stream->has_pending) {
av_packet_unref(&stream->pending);
}
av_parser_close(stream->parser);
finally_stop_and_join_recorder:
if (stream->recorder) {
recorder_stop(stream->recorder);
LOGI("Finishing recording...");
recorder_join(stream->recorder);
}
finally_close_recorder:
if (stream->recorder) {
recorder_close(stream->recorder);
}
finally_close_decoder:
if (stream->decoder) {
decoder_close(stream->decoder);
}
finally_free_codec_ctx:
avcodec_free_context(&stream->codec_ctx);
end:
notify_stopped();
return 0;
}
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder) {
stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
stream->has_pending = false;
}
bool
stream_start(struct stream *stream) {
LOGD("Starting stream thread");
stream->thread = SDL_CreateThread(run_stream, "stream", stream);
if (!stream->thread) {
LOGC("Could not start stream thread");
return false;
}
return true;
}
void
stream_stop(struct stream *stream) {
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}
}
void
stream_join(struct stream *stream) {
SDL_WaitThread(stream->thread, NULL);
}

View File

@ -1,41 +0,0 @@
#ifndef STREAM_H
#define STREAM_H
#include <stdbool.h>
#include <stdint.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
struct video_buffer;
struct stream {
socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread;
struct decoder *decoder;
struct recorder *recorder;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
bool has_pending;
AVPacket pending;
};
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder);
bool
stream_start(struct stream *stream);
void
stream_stop(struct stream *stream);
void
stream_join(struct stream *stream);
#endif

View File

@ -1,15 +1,9 @@
// for portability
#define _POSIX_SOURCE // for kill()
#define _BSD_SOURCE // for readlink()
// modern glibc will complain without this
#define _DEFAULT_SOURCE
#include "command.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
@ -17,8 +11,7 @@
#include <unistd.h>
#include "log.h"
enum process_result
cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
enum process_result cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
int fd[2];
if (pipe(fd) == -1) {
@ -79,22 +72,19 @@ end:
return ret;
}
bool
cmd_terminate(pid_t 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);
LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid);
abort();
}
return kill(pid, SIGTERM) != -1;
}
bool
cmd_simple_wait(pid_t pid, int *exit_code) {
SDL_bool cmd_simple_wait(pid_t pid, int *exit_code) {
int status;
int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
// could not wait, or exited unexpectedly, probably by a signal
// cannot wait, or exited unexpectedly, probably by a signal
code = -1;
} else {
code = WEXITSTATUS(status);
@ -104,23 +94,3 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
}
return !code;
}
char *
get_executable_path(void) {
// <https://stackoverflow.com/a/1024937/1987178>
#ifdef __linux__
char buf[PATH_MAX + 1]; // +1 for the null byte
ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX);
if (len == -1) {
perror("readlink");
return NULL;
}
buf[len] = '\0';
return SDL_strdup(buf);
#else
// in practice, we only need this feature for portable builds, only used on
// Windows, so we don't care implementing it for every platform
// (it's useful to have a working version on Linux for debugging though)
return NULL;
#endif
}

View File

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

View File

@ -4,8 +4,7 @@
#include "log.h"
#include "str_util.h"
static int
build_cmd(char *cmd, size_t len, const char *const argv[]) {
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
@ -18,9 +17,8 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
return 0;
}
enum process_result
cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
STARTUPINFOW si;
enum process_result cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
STARTUPINFO si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
@ -31,20 +29,12 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC;
}
wchar_t *wide = utf8_to_wide_char(cmd);
if (!wide) {
LOGC("Could not allocate wide char string");
return PROCESS_ERROR_GENERIC;
}
#ifdef WINDOWS_NOCONSOLE
int flags = CREATE_NO_WINDOW;
#else
int flags = 0;
#endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) {
SDL_free(wide);
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;
@ -52,22 +42,18 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC;
}
SDL_free(wide);
*handle = pi.hProcess;
return PROCESS_SUCCESS;
}
bool
cmd_terminate(HANDLE handle) {
SDL_bool cmd_terminate(HANDLE handle) {
return TerminateProcess(handle, 1) && CloseHandle(handle);
}
bool
cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
SDL_bool cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) {
// could not wait or retrieve the exit code
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) {
// cannot wait or retrieve the exit code
code = -1; // max value, it's unsigned
}
if (exit_code) {
@ -75,18 +61,3 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
}
return !code;
}
char *
get_executable_path(void) {
HMODULE hModule = GetModuleHandleW(NULL);
if (!hModule) {
return NULL;
}
WCHAR buf[MAX_PATH + 1]; // +1 for the null byte
int len = GetModuleFileNameW(hModule, buf, MAX_PATH);
if (!len) {
return NULL;
}
buf[len] = '\0';
return utf8_from_wide_char(buf);
}

View File

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

View File

@ -1,7 +1,5 @@
#include "tiny_xpm.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@ -9,20 +7,19 @@
struct index {
char c;
uint32_t color;
Uint32 color;
};
static bool
find_color(struct index *index, int len, char c, uint32_t *color) {
static SDL_bool find_color(struct index *index, int len, char c, Uint32 *color) {
// there are typically very few color, so it's ok to iterate over the array
for (int i = 0; i < len; ++i) {
if (index[i].c == c) {
*color = index[i].color;
return true;
return SDL_TRUE;
}
}
*color = 0;
return false;
return SDL_FALSE;
}
// We encounter some problems with SDL2_image on MSYS2 (Windows),
@ -33,8 +30,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) {
//
// Parameter is not "const char *" because XPM formats are generally stored in a
// (non-const) "char *"
SDL_Surface *
read_xpm(char *xpm[]) {
SDL_Surface *read_xpm(char *xpm[]) {
#if SDL_ASSERT_LEVEL >= 2
// patch the XPM to change the icon color in debug mode
xpm[2] = ". c #CC00CC";
@ -73,7 +69,7 @@ read_xpm(char *xpm[]) {
}
// parse image
uint32_t *pixels = SDL_malloc(4 * width * height);
Uint32 *pixels = SDL_malloc(4 * width * height);
if (!pixels) {
LOGE("Could not allocate icon memory");
return NULL;
@ -82,33 +78,29 @@ read_xpm(char *xpm[]) {
const char *line = xpm[1 + colors + y];
for (int x = 0; x < width; ++x) {
char c = line[x];
uint32_t color;
bool color_found = find_color(index, colors, c, &color);
Uint32 color;
SDL_bool color_found = find_color(index, colors, c, &color);
SDL_assert(color_found);
pixels[y * width + x] = color;
}
}
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
uint32_t amask = 0x000000ff;
uint32_t rmask = 0x0000ff00;
uint32_t gmask = 0x00ff0000;
uint32_t bmask = 0xff000000;
Uint32 amask = 0x000000ff;
Uint32 rmask = 0x0000ff00;
Uint32 gmask = 0x00ff0000;
Uint32 bmask = 0xff000000;
#else // little endian, like x86
uint32_t amask = 0xff000000;
uint32_t rmask = 0x00ff0000;
uint32_t gmask = 0x0000ff00;
uint32_t bmask = 0x000000ff;
Uint32 amask = 0xff000000;
Uint32 rmask = 0x00ff0000;
Uint32 gmask = 0x0000ff00;
Uint32 bmask = 0x000000ff;
#endif
SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels,
width, height,
32, 4 * width,
rmask, gmask, bmask, amask);
if (!surface) {
LOGE("Could not create icon surface");
return NULL;
}
// make the surface own the raw pixels
surface->flags &= ~SDL_PREALLOC;
return surface;

View File

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

View File

@ -1,113 +0,0 @@
#include "video_buffer.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "lock_util.h"
#include "log.h"
bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
bool render_expired_frames) {
vb->fps_counter = fps_counter;
if (!(vb->decoding_frame = av_frame_alloc())) {
goto error_0;
}
if (!(vb->rendering_frame = av_frame_alloc())) {
goto error_1;
}
if (!(vb->mutex = SDL_CreateMutex())) {
goto error_2;
}
vb->render_expired_frames = render_expired_frames;
if (render_expired_frames) {
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(vb->mutex);
goto error_2;
}
// interrupted is not used if expired frames are not rendered
// since offering a frame will never block
vb->interrupted = false;
}
// there is initially no rendering frame, so consider it has already been
// consumed
vb->rendering_frame_consumed = true;
return true;
error_2:
av_frame_free(&vb->rendering_frame);
error_1:
av_frame_free(&vb->decoding_frame);
error_0:
return false;
}
void
video_buffer_destroy(struct video_buffer *vb) {
if (vb->render_expired_frames) {
SDL_DestroyCond(vb->rendering_frame_consumed_cond);
}
SDL_DestroyMutex(vb->mutex);
av_frame_free(&vb->rendering_frame);
av_frame_free(&vb->decoding_frame);
}
static void
video_buffer_swap_frames(struct video_buffer *vb) {
AVFrame *tmp = vb->decoding_frame;
vb->decoding_frame = vb->rendering_frame;
vb->rendering_frame = tmp;
}
void
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped) {
mutex_lock(vb->mutex);
if (vb->render_expired_frames) {
// wait for the current (expired) frame to be consumed
while (!vb->rendering_frame_consumed && !vb->interrupted) {
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex);
}
} else if (!vb->rendering_frame_consumed) {
fps_counter_add_skipped_frame(vb->fps_counter);
}
video_buffer_swap_frames(vb);
*previous_frame_skipped = !vb->rendering_frame_consumed;
vb->rendering_frame_consumed = false;
mutex_unlock(vb->mutex);
}
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) {
SDL_assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true;
fps_counter_add_rendered_frame(vb->fps_counter);
if (vb->render_expired_frames) {
// unblock video_buffer_offer_decoded_frame()
cond_signal(vb->rendering_frame_consumed_cond);
}
return vb->rendering_frame;
}
void
video_buffer_interrupt(struct video_buffer *vb) {
if (vb->render_expired_frames) {
mutex_lock(vb->mutex);
vb->interrupted = true;
mutex_unlock(vb->mutex);
// wake up blocking wait
cond_signal(vb->rendering_frame_consumed_cond);
}
}

View File

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

View File

@ -1,73 +0,0 @@
#include <assert.h>
#include <string.h>
#include "cbuf.h"
struct int_queue CBUF(int, 32);
static void test_cbuf_empty(void) {
struct int_queue queue;
cbuf_init(&queue);
assert(cbuf_is_empty(&queue));
bool push_ok = cbuf_push(&queue, 42);
assert(push_ok);
assert(!cbuf_is_empty(&queue));
int item;
bool take_ok = cbuf_take(&queue, &item);
assert(take_ok);
assert(cbuf_is_empty(&queue));
bool take_empty_ok = cbuf_take(&queue, &item);
assert(!take_empty_ok); // the queue is empty
}
static void test_cbuf_full(void) {
struct int_queue queue;
cbuf_init(&queue);
assert(!cbuf_is_full(&queue));
// fill the queue
for (int i = 0; i < 32; ++i) {
bool ok = cbuf_push(&queue, i);
assert(ok);
}
bool ok = cbuf_push(&queue, 42);
assert(!ok); // the queue if full
int item;
bool take_ok = cbuf_take(&queue, &item);
assert(take_ok);
assert(!cbuf_is_full(&queue));
}
static void test_cbuf_push_take(void) {
struct int_queue queue;
cbuf_init(&queue);
bool push1_ok = cbuf_push(&queue, 42);
assert(push1_ok);
bool push2_ok = cbuf_push(&queue, 35);
assert(push2_ok);
int item;
bool take1_ok = cbuf_take(&queue, &item);
assert(take1_ok);
assert(item == 42);
bool take2_ok = cbuf_take(&queue, &item);
assert(take2_ok);
assert(item == 35);
}
int main(void) {
test_cbuf_empty();
test_cbuf_full();
test_cbuf_push_take();
return 0;
}

View File

@ -0,0 +1,95 @@
#include <assert.h>
#include <string.h>
#include "control_event.h"
static void test_control_event_queue_empty(void) {
struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
assert(control_event_queue_is_empty(&queue));
struct control_event dummy_event;
SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event);
assert(push_ok);
assert(!control_event_queue_is_empty(&queue));
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event);
assert(take_ok);
assert(control_event_queue_is_empty(&queue));
SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event);
assert(!take_empty_ok); // the queue is empty
control_event_queue_destroy(&queue);
}
static void test_control_event_queue_full(void) {
struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
assert(!control_event_queue_is_full(&queue));
struct control_event dummy_event;
// fill the queue
while (control_event_queue_push(&queue, &dummy_event));
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event);
assert(take_ok);
assert(!control_event_queue_is_full(&queue));
control_event_queue_destroy(&queue);
}
static void test_control_event_queue_push_take(void) {
struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
struct control_event event = {
.type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = {
.action = AKEY_EVENT_ACTION_DOWN,
.keycode = AKEYCODE_ENTER,
.metastate = AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON,
},
};
SDL_bool push1_ok = control_event_queue_push(&queue, &event);
assert(push1_ok);
event = (struct control_event) {
.type = CONTROL_EVENT_TYPE_TEXT,
.text_event = {
.text = "abc",
},
};
SDL_bool push2_ok = control_event_queue_push(&queue, &event);
assert(push2_ok);
// overwrite event
SDL_bool take1_ok = control_event_queue_take(&queue, &event);
assert(take1_ok);
assert(event.type == CONTROL_EVENT_TYPE_KEYCODE);
assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN);
assert(event.keycode_event.keycode == AKEYCODE_ENTER);
assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON));
// overwrite event
SDL_bool take2_ok = control_event_queue_take(&queue, &event);
assert(take2_ok);
assert(event.type == CONTROL_EVENT_TYPE_TEXT);
assert(!strcmp(event.text_event.text, "abc"));
control_event_queue_destroy(&queue);
}
int main(void) {
test_control_event_queue_empty();
test_control_event_queue_full();
test_control_event_queue_push_take();
return 0;
}

View File

@ -0,0 +1,141 @@
#include <assert.h>
#include <string.h>
#include "control_event.h"
static void test_serialize_keycode_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = {
.action = AKEY_EVENT_ACTION_UP,
.keycode = AKEYCODE_ENTER,
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 10);
const unsigned char expected[] = {
0x00, // CONTROL_EVENT_TYPE_KEYCODE
0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_text_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_TEXT,
.text_event = {
.text = "hello, world!",
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 16);
const unsigned char expected[] = {
0x01, // CONTROL_EVENT_TYPE_KEYCODE
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_long_text_event(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) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_MOUSE,
.mouse_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 14);
const unsigned char expected[] = {
0x02, // CONTROL_EVENT_TYPE_MOUSE
0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x01, 0x04, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_scroll_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_SCROLL,
.scroll_event = {
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
.hscroll = 1,
.vscroll = -1,
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 17);
const unsigned char expected[] = {
0x03, // CONTROL_EVENT_TYPE_SCROLL
0x01, 0x04, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1
0xFF, 0xFF, 0xFF, 0xFF, // -1
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) {
test_serialize_keycode_event();
test_serialize_text_event();
test_serialize_long_text_event();
test_serialize_mouse_event();
test_serialize_scroll_event();
}

View File

@ -1,248 +0,0 @@
#include <assert.h>
#include <string.h>
#include "control_msg.h"
static void test_serialize_inject_keycode(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_KEYCODE,
.inject_keycode = {
.action = AKEY_EVENT_ACTION_UP,
.keycode = AKEYCODE_ENTER,
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 10);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_text(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_TEXT,
.inject_text = {
.text = "hello, world!",
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 16);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_text_long(void) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1];
memset(text, 'a', sizeof(text));
text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text;
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH);
unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH];
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x01;
expected[2] = 0x2c; // text length (16 bits)
memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_mouse_event(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
.inject_mouse_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 18);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_scroll_event(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
.inject_scroll_event = {
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
.hscroll = 1,
.vscroll = -1,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 21);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1
0xFF, 0xFF, 0xFF, 0xFF, // -1
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_back_or_screen_on(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_expand_notification_panel(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_collapse_notification_panel(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_get_clipboard(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_GET_CLIPBOARD,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_set_clipboard(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
.inject_text = {
.text = "hello, world!",
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 16);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_set_screen_power_mode(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
.set_screen_power_mode = {
.mode = SCREEN_POWER_MODE_NORMAL,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 2);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x02, // SCREEN_POWER_MODE_NORMAL
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) {
test_serialize_inject_keycode();
test_serialize_inject_text();
test_serialize_inject_text_long();
test_serialize_inject_mouse_event();
test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel();
test_serialize_collapse_notification_panel();
test_serialize_get_clipboard();
test_serialize_set_clipboard();
test_serialize_set_screen_power_mode();
return 0;
}

View File

@ -1,28 +0,0 @@
#include <assert.h>
#include <string.h>
#include "device_msg.h"
#include <stdio.h>
static void test_deserialize_clipboard(void) {
const unsigned char input[] = {
DEVICE_MSG_TYPE_CLIPBOARD,
0x00, 0x03, // text length
0x41, 0x42, 0x43, // "ABC"
};
struct device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 6);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text);
assert(!strcmp("ABC", msg.clipboard.text));
device_msg_destroy(&msg);
}
int main(void) {
test_deserialize_clipboard();
return 0;
}

View File

@ -126,37 +126,6 @@ static void test_xstrjoin_truncated_after_sep(void) {
assert(!strcmp("abc de ", s));
}
static void test_utf8_truncate(void) {
const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
size_t count;
count = utf8_truncation_index(s, 1);
assert(count == 1);
count = utf8_truncation_index(s, 2);
assert(count == 1); // É is 2 bytes-wide
count = utf8_truncation_index(s, 3);
assert(count == 3);
count = utf8_truncation_index(s, 4);
assert(count == 4);
count = utf8_truncation_index(s, 5);
assert(count == 4); // Ô is 2 bytes-wide
count = utf8_truncation_index(s, 6);
assert(count == 6);
count = utf8_truncation_index(s, 7);
assert(count == 7);
count = utf8_truncation_index(s, 8);
assert(count == 7); // no more chars
}
int main(void) {
test_xstrncpy_simple();
test_xstrncpy_just_fit();
@ -166,6 +135,5 @@ int main(void) {
test_xstrjoin_truncated_in_token();
test_xstrjoin_truncated_before_sep();
test_xstrjoin_truncated_after_sep();
test_utf8_truncate();
return 0;
}

View File

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

View File

@ -15,6 +15,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32'
prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.9/i686-w64-mingw32'

View File

@ -15,6 +15,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32'
prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.9/x86_64-w64-mingw32'

View File

@ -1,6 +1,6 @@
#Thu Apr 18 11:45:59 CEST 2019
#Mon Jun 04 11:48:32 CEST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

@ -1,13 +1,13 @@
project('scrcpy', 'c',
version: '1.9',
version: '1.6',
meson_version: '>= 0.37',
default_options: 'c_std=c11')
if get_option('compile_app')
if get_option('build_app')
subdir('app')
endif
if get_option('compile_server')
if get_option('build_server')
subdir('server')
endif

View File

@ -1,7 +1,8 @@
option('compile_app', type: 'boolean', value: true, description: 'Build the client')
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
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('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
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

@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \
8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \
ffmpeg-4.1.3-win32-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1-win32-shared.zip \
e692b18c01745d262c03294b382fd64df68fabe3c66aa4546a3ad3935175cde3 \
ffmpeg-4.1-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \
e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \
ffmpeg-4.1.3-win32-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1-win32-dev.zip \
34bc5e471fb9160609abd6bc271e361050f3ff7376b1b8a0873cca02b38277c8 \
ffmpeg-4.1-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \
0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \
ffmpeg-4.1.3-win64-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1-win64-shared.zip \
c4908c97436c946509dc365e421159274fa4b1e66dce6fb5b63d82a6294d5357 \
ffmpeg-4.1-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \
334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \
ffmpeg-4.1.3-win64-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1-win64-dev.zip \
761ec79aa3dae66698c9791a2f0bb9da8794246f8356cadc741ddc0eabab0471 \
ffmpeg-4.1-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \
ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \
SDL2-2.0.8
@./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_r29.0.1-windows.zip \
2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \
db78f726d5dc653706dcd15a462ab1b946c643f598df76906c4c1858411c54df \
platform-tools

View File

@ -1,44 +0,0 @@
#!/bin/bash
set -e
# test locally
TESTDIR=build_test
rm -rf "$TESTDIR"
# run client tests with ASAN enabled
meson "$TESTDIR" -Db_sanitize=address
ninja -C"$TESTDIR" test
# test server
GRADLE=${GRADLE:-./gradlew}
$GRADLE -p server check
BUILDDIR=build_release
rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
cd "$BUILDDIR"
ninja
cd -
# build Windows releases
make -f Makefile.CrossWindows
# the generated server must be the same everywhere
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar
# get version name
TAG=$(git describe --tags --always)
# create release directory
mkdir -p "release-$TAG"
cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar"
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
# generate checksums
cd "release-$TAG"
sha256sum "scrcpy-server-$TAG.jar" \
"scrcpy-win32-$TAG.zip" \
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt
echo "Release generated in release-$TAG/"

View File

@ -1,13 +1,13 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 27
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 29
versionCode 10
versionName "1.9"
targetSdkVersion 27
versionCode 7
versionName "1.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@ -4,15 +4,12 @@ prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == ''
custom_target('scrcpy-server',
build_always: true, # gradle is responsible for tracking source changes
input: '.',
output: 'scrcpy-server.jar',
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
command: [find_program('./scripts/build-wrapper.sh'), '@INPUT@', '@OUTPUT@', get_option('buildtype')],
install: true,
install_dir: 'share/scrcpy')
else
if not prebuilt_server.startswith('/')
# relative path needs some trick
prebuilt_server = meson.source_root() + '/' + prebuilt_server
endif
custom_target('scrcpy-server-prebuilt',
input: prebuilt_server,
output: 'scrcpy-server.jar',

View File

@ -0,0 +1,105 @@
package com.genymobile.scrcpy;
/**
* Union of all supported event types, identified by their {@code type}.
*/
public final class ControlEvent {
public static final int TYPE_KEYCODE = 0;
public static final int TYPE_TEXT = 1;
public static final int TYPE_MOUSE = 2;
public static final int TYPE_SCROLL = 3;
public static final int TYPE_COMMAND = 4;
public static final int COMMAND_BACK_OR_SCREEN_ON = 0;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or COMMAND_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private Position position;
private int hScroll;
private int vScroll;
private ControlEvent() {
}
public static ControlEvent createKeycodeControlEvent(int action, int keycode, int metaState) {
ControlEvent event = new ControlEvent();
event.type = TYPE_KEYCODE;
event.action = action;
event.keycode = keycode;
event.metaState = metaState;
return event;
}
public static ControlEvent createTextControlEvent(String text) {
ControlEvent event = new ControlEvent();
event.type = TYPE_TEXT;
event.text = text;
return event;
}
public static ControlEvent createMotionControlEvent(int action, int buttons, Position position) {
ControlEvent event = new ControlEvent();
event.type = TYPE_MOUSE;
event.action = action;
event.buttons = buttons;
event.position = position;
return event;
}
public static ControlEvent createScrollControlEvent(Position position, int hScroll, int vScroll) {
ControlEvent event = new ControlEvent();
event.type = TYPE_SCROLL;
event.position = position;
event.hScroll = hScroll;
event.vScroll = vScroll;
return event;
}
public static ControlEvent createCommandControlEvent(int action) {
ControlEvent event = new ControlEvent();
event.type = TYPE_COMMAND;
event.action = action;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
public int getMetaState() {
return metaState;
}
public int getAction() {
return action;
}
public int getKeycode() {
return keycode;
}
public int getButtons() {
return buttons;
}
public Position getPosition() {
return position;
}
public int getHScroll() {
return hScroll;
}
public int getVScroll() {
return vScroll;
}
}

View File

@ -0,0 +1,151 @@
package com.genymobile.scrcpy;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ControlEventReader {
private static final int KEYCODE_PAYLOAD_LENGTH = 9;
private static final int MOUSE_PAYLOAD_LENGTH = 17;
private static final int SCROLL_PAYLOAD_LENGTH = 20;
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 final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH];
public ControlEventReader() {
// invariant: the buffer is always in "get" mode
buffer.limit(0);
}
public boolean isFull() {
return buffer.remaining() == rawBuffer.length;
}
public void readFrom(InputStream input) throws IOException {
if (isFull()) {
throw new IllegalStateException("Buffer full, call next() to consume");
}
buffer.compact();
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
throw new EOFException("Event controller socket closed");
}
buffer.position(head + r);
buffer.flip();
}
public ControlEvent next() {
if (!buffer.hasRemaining()) {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlEvent controlEvent;
switch (type) {
case ControlEvent.TYPE_KEYCODE:
controlEvent = parseKeycodeControlEvent();
break;
case ControlEvent.TYPE_TEXT:
controlEvent = parseTextControlEvent();
break;
case ControlEvent.TYPE_MOUSE:
controlEvent = parseMouseControlEvent();
break;
case ControlEvent.TYPE_SCROLL:
controlEvent = parseScrollControlEvent();
break;
case ControlEvent.TYPE_COMMAND:
controlEvent = parseCommandControlEvent();
break;
default:
Ln.w("Unknown event type: " + type);
controlEvent = null;
break;
}
if (controlEvent == null) {
// failure, reset savedPosition
buffer.position(savedPosition);
}
return controlEvent;
}
private ControlEvent parseKeycodeControlEvent() {
if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int keycode = buffer.getInt();
int metaState = buffer.getInt();
return ControlEvent.createKeycodeControlEvent(action, keycode, metaState);
}
private ControlEvent parseTextControlEvent() {
if (buffer.remaining() < 1) {
return null;
}
int len = toUnsigned(buffer.getShort());
if (buffer.remaining() < len) {
return null;
}
buffer.get(textBuffer, 0, len);
String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8);
return ControlEvent.createTextControlEvent(text);
}
private ControlEvent parseMouseControlEvent() {
if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int buttons = buffer.getInt();
Position position = readPosition(buffer);
return ControlEvent.createMotionControlEvent(action, buttons, position);
}
private ControlEvent parseScrollControlEvent() {
if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
int hScroll = buffer.getInt();
int vScroll = buffer.getInt();
return ControlEvent.createScrollControlEvent(position, hScroll, vScroll);
}
private ControlEvent parseCommandControlEvent() {
if (buffer.remaining() < COMMAND_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
return ControlEvent.createCommandControlEvent(action);
}
private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(short value) {
return value & 0xffff;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(byte value) {
return value & 0xff;
}
}

View File

@ -1,124 +0,0 @@
package com.genymobile.scrcpy;
/**
* Union of all supported event types, identified by their {@code type}.
*/
public final class ControlMessage {
public static final int TYPE_INJECT_KEYCODE = 0;
public static final int TYPE_INJECT_TEXT = 1;
public static final int TYPE_INJECT_MOUSE_EVENT = 2;
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private Position position;
private int hScroll;
private int vScroll;
private ControlMessage() {
}
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_KEYCODE;
event.action = action;
event.keycode = keycode;
event.metaState = metaState;
return event;
}
public static ControlMessage createInjectText(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_TEXT;
event.text = text;
return event;
}
public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_MOUSE_EVENT;
event.action = action;
event.buttons = buttons;
event.position = position;
return event;
}
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_SCROLL_EVENT;
event.position = position;
event.hScroll = hScroll;
event.vScroll = vScroll;
return event;
}
public static ControlMessage createSetClipboard(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_CLIPBOARD;
event.text = text;
return event;
}
/**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_SCREEN_POWER_MODE;
event.action = mode;
return event;
}
public static ControlMessage createEmpty(int type) {
ControlMessage event = new ControlMessage();
event.type = type;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
public int getMetaState() {
return metaState;
}
public int getAction() {
return action;
}
public int getKeycode() {
return keycode;
}
public int getButtons() {
return buttons;
}
public Position getPosition() {
return position;
}
public int getHScroll() {
return hScroll;
}
public int getVScroll() {
return vScroll;
}
}

View File

@ -1,176 +0,0 @@
package com.genymobile.scrcpy;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlMessageReader() {
// invariant: the buffer is always in "get" mode
buffer.limit(0);
}
public boolean isFull() {
return buffer.remaining() == rawBuffer.length;
}
public void readFrom(InputStream input) throws IOException {
if (isFull()) {
throw new IllegalStateException("Buffer full, call next() to consume");
}
buffer.compact();
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
throw new EOFException("Controller socket closed");
}
buffer.position(head + r);
buffer.flip();
}
public ControlMessage next() {
if (!buffer.hasRemaining()) {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlMessage msg;
switch (type) {
case ControlMessage.TYPE_INJECT_KEYCODE:
msg = parseInjectKeycode();
break;
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
msg = parseInjectMouseEvent();
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode();
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
msg = ControlMessage.createEmpty(type);
break;
default:
Ln.w("Unknown event type: " + type);
msg = null;
break;
}
if (msg == null) {
// failure, reset savedPosition
buffer.position(savedPosition);
}
return msg;
}
private ControlMessage parseInjectKeycode() {
if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int keycode = buffer.getInt();
int metaState = buffer.getInt();
return ControlMessage.createInjectKeycode(action, keycode, metaState);
}
private String parseString() {
if (buffer.remaining() < 2) {
return null;
}
int len = toUnsigned(buffer.getShort());
if (buffer.remaining() < len) {
return null;
}
buffer.get(textBuffer, 0, len);
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
}
private ControlMessage parseInjectText() {
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectMouseEvent() {
if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int buttons = buffer.getInt();
Position position = readPosition(buffer);
return ControlMessage.createInjectMouseEvent(action, buttons, position);
}
private ControlMessage parseInjectScrollEvent() {
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
int hScroll = buffer.getInt();
int vScroll = buffer.getInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
}
private ControlMessage parseSetClipboard() {
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(text);
}
private ControlMessage parseSetScreenPowerMode() {
if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) {
return null;
}
int mode = buffer.get();
return ControlMessage.createSetScreenPowerMode(mode);
}
private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(short value) {
return value & 0xffff;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(byte value) {
return value & 0xff;
}
}

View File

@ -8,7 +8,6 @@ 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 {
@ -17,22 +16,16 @@ public final class DesktopConnection implements Closeable {
private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket videoSocket;
private final FileDescriptor videoFd;
private final LocalSocket socket;
private final InputStream inputStream;
private final FileDescriptor fd;
private final LocalSocket controlSocket;
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
private final ControlEventReader reader = new ControlEventReader();
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
videoFd = videoSocket.getFileDescriptor();
private DesktopConnection(LocalSocket socket) throws IOException {
this.socket = socket;
inputStream = socket.getInputStream();
fd = socket.getFileDescriptor();
}
private static LocalSocket connect(String abstractName) throws IOException {
@ -41,47 +34,35 @@ 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 videoSocket;
LocalSocket controlSocket;
LocalSocket socket;
if (tunnelForward) {
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
try {
videoSocket = localServerSocket.accept();
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
try {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
} finally {
localServerSocket.close();
}
socket = listenAndAccept(SOCKET_NAME);
// send one byte so the client may read() to detect a connection error
socket.getOutputStream().write(0);
} else {
videoSocket = connect(SOCKET_NAME);
try {
controlSocket = connect(SOCKET_NAME);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
socket = connect(SOCKET_NAME);
}
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
DesktopConnection connection = new DesktopConnection(socket);
Size videoSize = device.getScreenInfo().getVideoSize();
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
}
public void close() throws IOException {
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
controlSocket.shutdownInput();
controlSocket.shutdownOutput();
controlSocket.close();
socket.shutdownInput();
socket.shutdownOutput();
socket.close();
}
@SuppressWarnings("checkstyle:MagicNumber")
@ -89,7 +70,7 @@ public final class DesktopConnection implements Closeable {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length);
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
@ -97,23 +78,19 @@ 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(videoFd, buffer, 0, buffer.length);
IO.writeFully(fd, buffer, 0, buffer.length);
}
public FileDescriptor getVideoFd() {
return videoFd;
public FileDescriptor getFd() {
return fd;
}
public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(controlInputStream);
msg = reader.next();
public ControlEvent receiveControlEvent() throws IOException {
ControlEvent event = reader.next();
while (event == null) {
reader.readFrom(inputStream);
event = reader.next();
}
return msg;
}
public void sendDeviceMessage(DeviceMessage msg) throws IOException {
writer.writeTo(msg, controlOutputStream);
return event;
}
}

View File

@ -1,20 +1,16 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.IRotationWatcher;
import android.view.InputEvent;
public final class Device {
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
public interface RotationListener {
void onRotationChanged(int rotation);
}
@ -111,8 +107,8 @@ public final class Device {
}
Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY);
}
@ -136,36 +132,6 @@ public final class Device {
this.rotationListener = rotationListener;
}
public void expandNotificationPanel() {
serviceManager.getStatusBarManager().expandNotificationsPanel();
}
public void collapsePanels() {
serviceManager.getStatusBarManager().collapsePanels();
}
public String getClipboardText() {
CharSequence s = serviceManager.getClipboardManager().getText();
if (s == null) {
return null;
}
return s.toString();
}
public void setClipboardText(String text) {
serviceManager.getClipboardManager().setText(text);
Ln.i("Device clipboard set");
}
/**
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
*/
public void setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay(0);
SurfaceControl.setDisplayPowerMode(d, mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}

View File

@ -1,27 +0,0 @@
package com.genymobile.scrcpy;
public final class DeviceMessage {
public static final int TYPE_CLIPBOARD = 0;
private int type;
private String text;
private DeviceMessage() {
}
public static DeviceMessage createClipboard(String text) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_CLIPBOARD;
event.text = text;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
}

View File

@ -1,34 +0,0 @@
package com.genymobile.scrcpy;
import java.io.IOException;
public final class DeviceMessageSender {
private final DesktopConnection connection;
private String clipboardText;
public DeviceMessageSender(DesktopConnection connection) {
this.connection = connection;
}
public synchronized void pushClipboardText(String text) {
clipboardText = text;
notify();
}
public void loop() throws IOException, InterruptedException {
while (true) {
String text;
synchronized (this) {
while (clipboardText == null) {
wait();
}
text = clipboardText;
clipboardText = null;
}
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
}
}

View File

@ -1,34 +0,0 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter {
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3;
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
@SuppressWarnings("checkstyle:MagicNumber")
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
buffer.clear();
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);
switch (msg.getType()) {
case DeviceMessage.TYPE_CLIPBOARD:
String text = msg.getText();
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
buffer.putShort((short) len);
buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position());
break;
default:
Ln.w("Unknown device message: " + msg.getType());
break;
}
}
}

View File

@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.graphics.Point;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.InputEvent;
@ -11,11 +12,11 @@ import android.view.MotionEvent;
import java.io.IOException;
public class Controller {
public class EventController {
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@ -23,11 +24,10 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
public Controller(Device device, DesktopConnection connection) {
public EventController(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
initPointer();
sender = new DeviceMessageSender(connection);
}
private void initPointer() {
@ -43,8 +43,8 @@ public class Controller {
private void setPointerCoords(Point point) {
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
coords.x = point.x;
coords.y = point.y;
}
private void setScroll(int hScroll, int vScroll) {
@ -53,64 +53,32 @@ public class Controller {
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
}
@SuppressWarnings("checkstyle:MagicNumber")
public void control() throws IOException {
// on start, power on the device
if (!device.isScreenOn()) {
injectKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on.
// Adding this delay prevents to handle the message before the device is actually
// powered on.
SystemClock.sleep(500);
}
// on start, turn screen on
turnScreenOn();
while (true) {
handleEvent();
}
}
public DeviceMessageSender getSender() {
return sender;
}
private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
ControlEvent controlEvent = connection.receiveControlEvent();
switch (controlEvent.getType()) {
case ControlEvent.TYPE_KEYCODE:
injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState());
break;
case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText());
case ControlEvent.TYPE_TEXT:
injectText(controlEvent.getText());
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
case ControlEvent.TYPE_MOUSE:
injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition());
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
case ControlEvent.TYPE_SCROLL:
injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll());
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
pressBackOrTurnScreenOn();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
sender.pushClipboardText(clipboardText);
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText());
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
case ControlEvent.TYPE_COMMAND:
executeCommand(controlEvent.getAction());
break;
default:
// do nothing
@ -123,7 +91,7 @@ public class Controller {
private boolean injectChar(char c) {
String decomposed = KeyComposition.decompose(c);
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c};
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
KeyEvent[] events = charMap.getEvents(chars);
if (events == null) {
return false;
@ -136,16 +104,13 @@ public class Controller {
return true;
}
private int injectText(String text) {
int successCount = 0;
private boolean injectText(String text) {
for (char c : text.toCharArray()) {
if (!injectChar(c)) {
Ln.w("Could not inject char u+" + String.format("%04x", (int) c));
continue;
return false;
}
successCount++;
}
return successCount;
return true;
}
private boolean injectMouse(int action, int buttons, Position position) {
@ -194,8 +159,22 @@ public class Controller {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
private boolean turnScreenOn() {
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();
default:
Ln.w("Unsupported command: " + action);
}
return false;
}
}

View File

@ -8,7 +8,7 @@ import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class IO {
public class IO {
private IO() {
// not instantiable
}

View File

@ -9,7 +9,6 @@ import android.util.Log;
public final class Ln {
private static final String TAG = "scrcpy";
private static final String PREFIX = "[server] ";
enum Level {
DEBUG,
@ -31,35 +30,29 @@ public final class Ln {
public static void d(String message) {
if (isEnabled(Level.DEBUG)) {
Log.d(TAG, message);
System.out.println(PREFIX + "DEBUG: " + message);
System.out.println("DEBUG: " + message);
}
}
public static void i(String message) {
if (isEnabled(Level.INFO)) {
Log.i(TAG, message);
System.out.println(PREFIX + "INFO: " + message);
System.out.println("INFO: " + message);
}
}
public static void w(String message) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message);
System.out.println(PREFIX + "WARN: " + message);
System.out.println("WARN: " + message);
}
}
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);
System.out.println(PREFIX + "ERROR: " + message);
if (throwable != null) {
throwable.printStackTrace();
}
System.out.println("ERROR: " + message);
throwable.printStackTrace();
}
}
public static void e(String message) {
e(message, null);
}
}

View File

@ -8,7 +8,6 @@ public class Options {
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
public int getMaxSize() {
return maxSize;
@ -49,12 +48,4 @@ public class Options {
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
public boolean getControl() {
return control;
}
public void setControl(boolean control) {
this.control = control;
}
}

View File

@ -1,47 +0,0 @@
package com.genymobile.scrcpy;
import java.util.Objects;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Point point = (Point) o;
return x == point.x
&& y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{"
+ "x=" + x
+ ", y=" + y
+ '}';
}
}

View File

@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
import android.graphics.Point;
import java.util.Objects;
public class Position {

View File

@ -3,6 +3,7 @@ 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;
@ -70,9 +71,8 @@ public class ScreenEncoder implements Device.RotationListener {
codec.start();
try {
alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} finally {
codec.stop();
destroyDisplay(display);
codec.release();
surface.release();
@ -87,6 +87,7 @@ public class ScreenEncoder implements Device.RotationListener {
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;

View File

@ -4,6 +4,7 @@ import android.graphics.Rect;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
public final class Server {
@ -19,17 +20,12 @@ public final class Server {
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
if (options.getControl()) {
Controller controller = new Controller(device, connection);
// asynchronous
startController(controller);
startDeviceMessageSender(controller.getSender());
}
// asynchronous
startEventController(device, connection);
try {
// synchronous
screenEncoder.streamScreen(device, connection.getVideoFd());
screenEncoder.streamScreen(device, connection.getFd());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
@ -37,29 +33,15 @@ public final class Server {
}
}
private static void startController(final Controller controller) {
private static void startEventController(final Device device, final DesktopConnection connection) {
new Thread(new Runnable() {
@Override
public void run() {
try {
controller.control();
new EventController(device, connection).control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
}
}).start();
}
private static void startDeviceMessageSender(final DeviceMessageSender sender) {
new Thread(new Runnable() {
@Override
public void run() {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
Ln.d("Event controller stopped");
}
}
}).start();
@ -67,9 +49,8 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) {
if (args.length != 6) {
throw new IllegalArgumentException("Expecting 6 parameters");
}
if (args.length != 5)
throw new IllegalArgumentException("Expecting 5 parameters");
Options options = new Options();
@ -89,13 +70,9 @@ public final class Server {
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[5]);
options.setControl(control);
return options;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static Rect parseCrop(String crop) {
if ("-".equals(crop)) {
return null;
@ -116,7 +93,7 @@ public final class Server {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
Ln.e("Cannot unlink server", e);
}
}

View File

@ -1,23 +0,0 @@
package com.genymobile.scrcpy;
public final class StringUtils {
private StringUtils() {
// not instantiable
}
@SuppressWarnings("checkstyle:MagicNumber")
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
int len = utf8.length;
if (len <= maxLength) {
return len;
}
len = maxLength;
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
// the next byte is not the start of a new UTF-8 codepoint
// so if we would cut there, the character would be truncated
len--;
}
return len;
}
}

View File

@ -1,44 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import android.content.ClipData;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClipboardManager {
private final IInterface manager;
private final Method getPrimaryClipMethod;
private final Method setPrimaryClipMethod;
public ClipboardManager(IInterface manager) {
this.manager = manager;
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
}
public CharSequence getText() {
try {
ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell");
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
public void setText(CharSequence text) {
ClipData clipData = ClipData.newPlainText(null, text);
try {
setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell");
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
}

View File

@ -14,8 +14,6 @@ public final class ServiceManager {
private DisplayManager displayManager;
private InputManager inputManager;
private PowerManager powerManager;
private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
public ServiceManager() {
try {
@ -62,18 +60,4 @@ public final class ServiceManager {
}
return powerManager;
}
public StatusBarManager getStatusBarManager() {
if (statusBarManager == null) {
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
}
return statusBarManager;
}
public ClipboardManager getClipboardManager() {
if (clipboardManager == null) {
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
}
return clipboardManager;
}
}

View File

@ -1,51 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class StatusBarManager {
private final IInterface manager;
private Method expandNotificationsPanelMethod;
private Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) {
this.manager = manager;
}
public void expandNotificationsPanel() {
if (expandNotificationsPanelMethod == null) {
try {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
} catch (NoSuchMethodException e) {
Ln.e("ServiceBarManager.expandNotificationsPanel() is not available on this device");
return;
}
}
try {
expandNotificationsPanelMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e);
}
}
public void collapsePanels() {
if (collapsePanelsMethod == null) {
try {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
} catch (NoSuchMethodException e) {
Ln.e("ServiceBarManager.collapsePanels() is not available on this device");
return;
}
}
try {
collapsePanelsMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e);
}
}
}

Some files were not shown because too many files have changed in this diff Show More