Compare commits

..

1 Commits

Author SHA1 Message Date
Romain Vimont
5b56c57795 Screen off PoC 2019-03-15 20:24:28 +01:00
95 changed files with 1986 additions and 3783 deletions

View File

@@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
## Requirements ## Requirements
You need [adb]. It is available in the [Android SDK platform 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 On Windows, download the [platform-tools][platform-tools-windows] and extract
the following files to a directory accessible from your `PATH`: the following files to a directory accessible from your `PATH`:
@@ -40,10 +40,10 @@ Install the required packages from your package manager.
```bash ```bash
# runtime dependencies # runtime dependencies
sudo apt install ffmpeg libsdl2-2.0-0 sudo apt install ffmpeg libsdl2-2.0.0
# client build dependencies # 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 \ libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-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 sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies # 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 ## Prebuilt server
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server] - [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
_(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_ _(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
[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.8/scrcpy-server-v1.8.jar
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: 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 args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device. `shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 [main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100
To run such a Java application, the classes must be [_dexed_][dex] (typically, To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@@ -65,20 +65,17 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl]. components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178 [hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers [wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view [aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view
### Threading ### Threading
The server uses 3 threads: The server uses 2 threads:
- the **main** thread, encoding and streaming the video to the client; - the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control messages_ (typically, - the **controller** thread, listening for _control events_ (typically,
keyboard and mouse events) from the client; 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).
Since the video encoding is typically hardware, there would be no benefit in Since the video encoding is typically hardware, there would be no benefit in
encoding and streaming in two different threads. 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 from a [surface] associated to the display, and writes the resulting H.264
stream to the provided output stream (the socket connected to the client). stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69 [surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70
On device [rotation], the codec, surface and display are reinitialized, and a On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced. new video stream is produced.
@@ -108,30 +105,31 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 [rotation]: https://github.com/Genymobile/scrcpy/blob/v1.8/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 [repeat]:
https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection ### Input events injection
_Control messages_ are received from the client by the [`Controller`] (run in a _Control events_ are received from the client by the [`EventController`] (run in
separate thread). There are several types of input events: a separate thread). There are 5 types of input events:
- keycode (cf [`KeyEvent`]), - keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly), - text (special characters may not be handled by keycodes directly),
- mouse motion/click, - mouse motion/click,
- mouse scroll, - 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 All of them may need to inject input events to the system. To do so, they use
_hidden_ method [`InputManager.injectInputEvent`] (exposed by our the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]). [`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.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 [`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@@ -148,8 +146,8 @@ The video stream is decoded by [libav] (FFmpeg).
### Initialization ### Initialization
On startup, in addition to _libav_ and _SDL_ initialization, the client must 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 push and start the server on the device, and open a socket so that they may
stream, one for control) so that they may communicate. communicate.
Note that the client-server roles are expressed at the application level: Note that the client-server roles are expressed at the application level:
@@ -182,18 +180,15 @@ the connection from the server (see commit [90a46b4]).
### Threading ### Threading
The client uses 4 threads: The client uses 3 threads:
- the **main** thread, executing the SDL event loop, - the **main** thread, executing the SDL event loop,
- the **stream** thread, receiving the video and used for decoding and - the **stream** thread, receiving the video and used for decoding and
recording, recording,
- the **controller** thread, sending _control messages_ to the server, - the **controller** thread, sending _control events_ 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 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 installation or file push requests (via drag&drop on the main window).
print the framerate regularly in the console.
@@ -217,10 +212,10 @@ 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 If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
H.264 packet to the output video file. H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h [stream]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h [decoder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h [video_buffer]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h [recorder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/recorder.h
``` ```
+----------+ +----------+ +----------+ +----------+
@@ -234,19 +229,20 @@ H.264 packet to the output video file.
### Controller ### Controller
The [controller] is responsible to send _control messages_ to the device. It The [controller] is responsible to send _control events_ to the device. It runs
runs in a separate thread, to avoid I/O on the main thread. in a separate thread, to avoid I/O on the main thread.
On SDL event, received on the main thread, the [input manager][inputmanager] 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 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 events_ to a blocking queue hold by the controller. On its own thread, the
takes messages from the queue, that it serializes and sends to the client. 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 [controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h
[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h [controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h [inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h [convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h
### UI and event loop ### UI and event loop
@@ -257,9 +253,10 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager]. delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c [scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201 [event loop]:
[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c#L187
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/screen.h
## Hack ## 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 [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 ### Mouse clicks do not work

View File

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

View File

@@ -44,7 +44,7 @@ clean:
build-server: build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \ meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dcompile_app=false ) --buildtype release -Dbuild_app=false )
ninja -C "$(SERVER_BUILD_DIR)" ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32: prepare-deps-win32:
@@ -56,8 +56,8 @@ build-win32: prepare-deps-win32
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dcompile_server=false \ -Dbuild_server=false \
-Dportable=true ) -Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_BUILD_DIR)" ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32 build-win32-noconsole: prepare-deps-win32
@@ -66,9 +66,9 @@ build-win32-noconsole: prepare-deps-win32
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dcompile_server=false \ -Dbuild_server=false \
-Dwindows_noconsole=true \ -Dwindows_noconsole=true \
-Dportable=true ) -Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64: prepare-deps-win64:
@@ -80,8 +80,8 @@ build-win64: prepare-deps-win64
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dcompile_server=false \ -Dbuild_server=false \
-Dportable=true ) -Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_BUILD_DIR)" ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64 build-win64-noconsole: prepare-deps-win64
@@ -90,9 +90,9 @@ build-win64-noconsole: prepare-deps-win64
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dcompile_server=false \ -Dbuild_server=false \
-Dwindows_noconsole=true \ -Dwindows_noconsole=true \
-Dportable=true ) -Doverride_server_path=scrcpy-server.jar )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole dist-win32: build-server build-win32 build-win32-noconsole
@@ -100,37 +100,36 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_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 "$(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-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-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-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/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(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/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.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 dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(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_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 "$(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-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-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-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-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/platform-tools/adb.exe "$(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/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.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 zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ cd "$(DIST)"; \
zip -r "../$(WIN32_TARGET)" . zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
zip-win64: dist-win64 zip-win64: dist-win64
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ cd "$(DIST)"; \
zip -r "../$(WIN64_TARGET)" . zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
sums: sums:
cd "$(DIST)"; \ cd "$(DIST)"; \

136
README.md
View File

@@ -1,15 +1,15 @@
# scrcpy (v1.9) # scrcpy (v1.8)
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB (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) ![screenshot](assets/screenshot-debian-600.jpg)
## Requirements ## 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). Make sure you [enabled adb debugging][enable-adb] on your device(s).
@@ -29,12 +29,6 @@ control it using keyboard and mouse.
On Linux, you typically need to [build the app manually][BUILD]. Don't worry, On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard. it's not that hard.
A [Snap] package is available: [`scrcpy`][snap-link].
[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]. For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
@@ -51,18 +45,18 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available: (including `adb`) are available:
- [`scrcpy-win32-v1.9.zip`][direct-win32] - [`scrcpy-win32-v1.8.zip`][direct-win32]
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_ _(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
- [`scrcpy-win64-v1.9.zip`][direct-win64] - [`scrcpy-win64-v1.8.zip`][direct-win64]
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_ _(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip
You can also [build the app manually][BUILD]. You can also [build the app manually][BUILD].
### macOS ### Mac OS
The application is available in [Homebrew]. Just install it: The application is available in [Homebrew]. Just install it:
@@ -101,16 +95,16 @@ scrcpy --help
### Reduce size ### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to 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 ```bash
scrcpy --max-size 1024 scrcpy --max-size 1024
scrcpy -m 1024 # short version 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. That way, a device in 1920×1080 will be mirrored at 1024×576.
@@ -128,7 +122,7 @@ scrcpy -b 2M # short version
The device screen may be cropped to mirror only part of the screen. 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 ```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
@@ -251,11 +245,6 @@ _scrcpy_ window.
There is no visual feedback, a log is printed to the console. 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 ### Read-only
@@ -267,92 +256,44 @@ scrcpy --no-control
scrcpy -n 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 ### 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 ## Shortcuts
| Action | Shortcut | Shortcut (macOS) | Action | Shortcut |
| -------------------------------------- |:----------------------------- |:----------------------------- | -------------------------------------- |:---------------------------- |
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` | switch fullscreen mode | `Ctrl`+`f` |
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` | resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ | resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ | click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ | click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` | click on `APP_SWITCH` | `Ctrl`+`s` |
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` | click on `MENU` | `Ctrl`+`m` |
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ | click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) |
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ | click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` | click on `POWER` | `Ctrl`+`p` |
| Power on | _Right-click²_ | _Right-click²_ | turn screen on | _Right-click²_ |
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` | expand notification panel | `Ctrl`+`n` |
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` | collapse notification panel | `Ctrl`+`Shift`+`n` |
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | paste computer clipboard to device | `Ctrl`+`v` |
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` | enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| 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`
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._ _²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_? ## Why _scrcpy_?
A colleague challenged me to find a name as unpronounceable as [gnirehtet]. A colleague challenged me to find a name as unpronounceable as [gnirehtet].
@@ -385,7 +326,6 @@ Read the [developers page].
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -1,17 +1,16 @@
src = [ src = [
'src/main.c', 'src/main.c',
'src/command.c', 'src/command.c',
'src/control_msg.c', 'src/control_event.c',
'src/controller.c', 'src/controller.c',
'src/convert.c', 'src/convert.c',
'src/decoder.c', 'src/decoder.c',
'src/device.c', 'src/device.c',
'src/device_msg.c',
'src/file_handler.c', 'src/file_handler.c',
'src/fps_counter.c', 'src/fps_counter.c',
'src/input_manager.c', 'src/input_manager.c',
'src/lock_util.c',
'src/net.c', 'src/net.c',
'src/receiver.c',
'src/recorder.c', 'src/recorder.c',
'src/scrcpy.c', 'src/scrcpy.c',
'src/screen.c', 'src/screen.c',
@@ -93,9 +92,21 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX) # the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix')) conf.set_quoted('PREFIX', get_option('prefix'))
# build a "portable" version (with scrcpy-server.jar accessible from the same # the path of the server, which will be appended to the prefix
# directory as the executable) # ignored if OVERRIDE_SERVER_PATH if defined
conf.set('PORTABLE', get_option('portable')) # 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 # the default client TCP port for the "adb reverse" tunnel
# overridden by option --port # overridden by option --port
@@ -109,6 +120,11 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# overridden by option --bit-rate # overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps 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 # enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
@@ -127,41 +143,18 @@ else
link_args = [] link_args = []
endif endif
executable('scrcpy', src, executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: c_args, link_args: link_args)
dependencies: dependencies,
include_directories: src_dir,
install: true,
c_args: c_args,
link_args: link_args)
### TESTS ### TESTS
tests = [ tests = [
['test_cbuf', [ ['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/control_event.c']],
'tests/test_cbuf.c', ['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/control_event.c']],
]], ['test_strutil', ['tests/test_strutil.c', 'src/str_util.c']],
['test_control_event_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_queue', [
'tests/test_queue.c',
]],
['test_strutil', [
'tests/test_strutil.c',
'src/str_util.c'
]],
] ]
foreach t : tests foreach t : tests
exe = executable(t[0], t[1], exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies)
include_directories: src_dir,
dependencies: dependencies)
test(t[0], exe) test(t[0], exe)
endforeach endforeach

View File

@@ -18,18 +18,13 @@ buffer_write32be(uint8_t *buf, uint32_t value) {
buf[3] = value; buf[3] = value;
} }
static inline uint16_t
buffer_read16be(const uint8_t *buf) {
return (buf[0] << 8) | buf[1];
}
static inline uint32_t static inline uint32_t
buffer_read32be(const uint8_t *buf) { buffer_read32be(uint8_t *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
} }
static inline static inline
uint64_t buffer_read64be(const uint8_t *buf) { uint64_t buffer_read64be(uint8_t *buf) {
uint32_t msb = buffer_read32be(buf); uint32_t msb = buffer_read32be(buf);
uint32_t lsb = buffer_read32be(&buf[4]); uint32_t lsb = buffer_read32be(&buf[4]);
return ((uint64_t) msb << 32) | lsb; return ((uint64_t) msb << 32) | lsb;

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:
// struct cbuf_int CBUF(int, 20);
//
// 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 "command.h"
#include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -21,61 +20,24 @@ get_adb_command(void) {
return adb_command; 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 static void
show_adb_err_msg(enum process_result err, const char *const argv[]) { show_adb_err_msg(enum process_result err) {
char buf[512];
switch (err) { switch (err) {
case PROCESS_ERROR_GENERIC: case PROCESS_ERROR_GENERIC:
argv_to_string(argv, buf, sizeof(buf)); LOGE("Failed to execute adb");
LOGE("Failed to execute: %s", buf);
break; break;
case PROCESS_ERROR_MISSING_BINARY: case PROCESS_ERROR_MISSING_BINARY:
argv_to_string(argv, buf, sizeof(buf)); LOGE("'adb' command not found (make it accessible from your PATH "
LOGE("Command not found: %s", buf); "or define its full path in the ADB environment variable)");
LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)");
break; break;
case PROCESS_SUCCESS: case PROCESS_SUCCESS:
// do nothing /* do nothing */
break; break;
} }
} }
process_t process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { adb_execute(const char *serial, const char *const adb_cmd[], int len) {
const char *cmd[len + 4]; const char *cmd[len + 4];
int i; int i;
process_t process; process_t process;
@@ -92,7 +54,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
cmd[len + i] = NULL; cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd[0], cmd, &process); enum process_result r = cmd_execute(cmd[0], cmd, &process);
if (r != PROCESS_SUCCESS) { if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, cmd); show_adb_err_msg(r);
return PROCESS_NONE; return PROCESS_NONE;
} }
return process; return process;
@@ -147,7 +109,7 @@ adb_push(const char *serial, const char *local, const char *remote) {
} }
remote = strquote(remote); remote = strquote(remote);
if (!remote) { if (!remote) {
SDL_free((void *) local); free((void *) local);
return PROCESS_NONE; return PROCESS_NONE;
} }
#endif #endif
@@ -156,8 +118,8 @@ adb_push(const char *serial, const char *local, const char *remote) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__ #ifdef __WINDOWS__
SDL_free((void *) remote); free((void *) remote);
SDL_free((void *) local); free((void *) local);
#endif #endif
return proc; return proc;
@@ -178,7 +140,7 @@ adb_install(const char *serial, const char *local) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__ #ifdef __WINDOWS__
SDL_free((void *) local); free((void *) local);
#endif #endif
return proc; return proc;

View File

@@ -9,7 +9,6 @@
// not needed here, but winsock2.h must never be included AFTER windows.h // not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h> # include <winsock2.h>
# include <windows.h> # include <windows.h>
# define PATH_SEPARATOR '\\'
# define PRIexitcode "lu" # define PRIexitcode "lu"
// <https://stackoverflow.com/a/44383330/1987178> // <https://stackoverflow.com/a/44383330/1987178>
# ifdef _WIN64 # ifdef _WIN64
@@ -24,7 +23,6 @@
#else #else
# include <sys/types.h> # include <sys/types.h>
# define PATH_SEPARATOR '/'
# define PRIsizet "zu" # define PRIsizet "zu"
# define PRIexitcode "d" # define PRIexitcode "d"
# define PROCESS_NONE -1 # define PROCESS_NONE -1
@@ -51,7 +49,7 @@ bool
cmd_simple_wait(process_t pid, exit_code_t *exit_code); cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len); adb_execute(const char *serial, const char *const adb_cmd[], int len);
process_t process_t
adb_forward(const char *serial, uint16_t local_port, adb_forward(const char *serial, uint16_t local_port,
@@ -76,11 +74,6 @@ adb_install(const char *serial, const char *local);
// convenience function to wait for a successful process execution // convenience function to wait for a successful process execution
// automatically log process errors with the provided process name // automatically log process errors with the provided process name
bool bool
process_check_success(process_t proc, const char *name); process_check_success(process_t process, 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);
#endif #endif

View File

@@ -43,9 +43,4 @@
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP # define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
#endif #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 #endif

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

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

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

@@ -0,0 +1,91 @@
#ifndef CONTROLEVENT_H
#define CONTROLEVENT_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_mutex.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,
};
enum control_event_command {
CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON,
CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL,
CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL,
};
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;
int32_t hscroll;
int32_t vscroll;
} scroll_event;
struct {
enum control_event_command 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);
bool
control_event_queue_init(struct control_event_queue *queue);
void
control_event_queue_destroy(struct control_event_queue *queue);
bool
control_event_queue_is_empty(const struct control_event_queue *queue);
bool
control_event_queue_is_full(const struct control_event_queue *queue);
// event is copied, the queue does not use the event after the function returns
bool
control_event_queue_push(struct control_event_queue *queue,
const struct control_event *event);
bool
control_event_queue_take(struct control_event_queue *queue,
struct control_event *event);
void
control_event_destroy(struct control_event *event);
#endif

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

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

View File

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

View File

@@ -81,9 +81,9 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL); MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB); MAP(SDLK_TAB, AKEYCODE_TAB);
MAP(SDLK_HOME, AKEYCODE_HOME);
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
MAP(SDLK_HOME, AKEYCODE_MOVE_HOME);
MAP(SDLK_END, AKEYCODE_MOVE_END); MAP(SDLK_END, AKEYCODE_MOVE_END);
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
@@ -159,19 +159,19 @@ convert_mouse_buttons(uint32_t state) {
bool bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_msg *to) { struct control_event *to) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; to->type = CONTROL_EVENT_TYPE_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { if (!convert_keycode_action(from->type, &to->keycode_event.action)) {
return false; return false;
} }
uint16_t mod = from->keysym.mod; uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) { if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) {
return false; return false;
} }
to->inject_keycode.metastate = convert_meta_state(mod); to->keycode_event.metastate = convert_meta_state(mod);
return true; return true;
} }
@@ -179,18 +179,17 @@ input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
bool bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size, struct size screen_size,
struct control_msg *to) { struct control_event *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; to->type = CONTROL_EVENT_TYPE_MOUSE;
if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) { if (!convert_mouse_action(from->type, &to->mouse_event.action)) {
return false; return false;
} }
to->inject_mouse_event.buttons = to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
convert_mouse_buttons(SDL_BUTTON(from->button)); to->mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.screen_size = screen_size; to->mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.x = from->x; to->mouse_event.position.point.y = from->y;
to->inject_mouse_event.position.point.y = from->y;
return true; return true;
} }
@@ -198,13 +197,13 @@ mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
bool bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size, struct size screen_size,
struct control_msg *to) { struct control_event *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT; to->type = CONTROL_EVENT_TYPE_MOUSE;
to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE; to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_mouse_event.buttons = convert_mouse_buttons(from->state); to->mouse_event.buttons = convert_mouse_buttons(from->state);
to->inject_mouse_event.position.screen_size = screen_size; to->mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x; to->mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y; to->mouse_event.position.point.y = from->y;
return true; return true;
} }
@@ -212,17 +211,17 @@ mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
bool bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position, struct position position,
struct control_msg *to) { struct control_event *to) {
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; 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; int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling // SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal // so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks> // <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->inject_scroll_event.hscroll = -mul * from->x; to->scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y; to->scroll_event.vscroll = mul * from->y;
return true; return true;
} }

View File

@@ -4,7 +4,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include "control_msg.h" #include "control_event.h"
struct complete_mouse_motion_event { struct complete_mouse_motion_event {
SDL_MouseMotionEvent *mouse_motion_event; SDL_MouseMotionEvent *mouse_motion_event;
@@ -18,24 +18,24 @@ struct complete_mouse_wheel_event {
bool bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from, input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_msg *to); struct control_event *to);
bool bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from, mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size, struct size screen_size,
struct control_msg *to); struct control_event *to);
// the video size may be different from the real device size, so we need the // 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 // size to which the absolute position apply, to scale it accordingly
bool bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from, mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size, struct size screen_size,
struct control_msg *to); struct control_event *to);
// on Android, a scroll event requires the current mouse position // on Android, a scroll event requires the current mouse position
bool bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from, mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position, struct position position,
struct control_msg *to); struct control_event *to);
#endif #endif

View File

@@ -7,9 +7,10 @@
#include "net.h" #include "net.h"
#define DEVICE_NAME_FIELD_LENGTH 64 #define DEVICE_NAME_FIELD_LENGTH 64
#define DEVICE_SDCARD_PATH "/sdcard/"
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes // name must be at least DEVICE_NAME_FIELD_LENGTH bytes
bool bool
device_read_info(socket_t device_socket, char *device_name, struct size *size); device_read_info(socket_t device_socket, char *name, struct size *frame_size);
#endif #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

@@ -2,24 +2,90 @@
#include <string.h> #include <string.h>
#include <SDL2/SDL_assert.h> #include <SDL2/SDL_assert.h>
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "device.h"
#include "lock_util.h" #include "lock_util.h"
#include "log.h" #include "log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/" struct request {
file_handler_action_t action;
const char *file;
};
static struct request *
request_new(file_handler_action_t action, const char *file) {
struct request *req = SDL_malloc(sizeof(*req));
if (!req) {
return NULL;
}
req->action = action;
req->file = file;
return req;
}
static void static void
file_handler_request_destroy(struct file_handler_request *req) { request_free(struct request *req) {
SDL_free(req->file); if (!req) {
return;
}
SDL_free((void *) req->file);
SDL_free((void *) req);
}
static bool
request_queue_is_empty(const struct request_queue *queue) {
return queue->head == queue->tail;
}
static bool
request_queue_is_full(const struct request_queue *queue) {
return (queue->head + 1) % REQUEST_QUEUE_SIZE == queue->tail;
}
static bool
request_queue_init(struct request_queue *queue) {
queue->head = 0;
queue->tail = 0;
return true;
}
static void
request_queue_destroy(struct request_queue *queue) {
int i = queue->tail;
while (i != queue->head) {
request_free(queue->reqs[i]);
i = (i + 1) % REQUEST_QUEUE_SIZE;
}
}
static bool
request_queue_push(struct request_queue *queue, struct request *req) {
if (request_queue_is_full(queue)) {
return false;
}
queue->reqs[queue->head] = req;
queue->head = (queue->head + 1) % REQUEST_QUEUE_SIZE;
return true;
}
static bool
request_queue_take(struct request_queue *queue, struct request **req) {
if (request_queue_is_empty(queue)) {
return false;
}
// transfer ownership
*req = queue->reqs[queue->tail];
queue->tail = (queue->tail + 1) % REQUEST_QUEUE_SIZE;
return true;
} }
bool bool
file_handler_init(struct file_handler *file_handler, const char *serial, file_handler_init(struct file_handler *file_handler, const char *serial) {
const char *push_target) {
cbuf_init(&file_handler->queue); if (!request_queue_init(&file_handler->queue)) {
return false;
}
if (!(file_handler->mutex = SDL_CreateMutex())) { if (!(file_handler->mutex = SDL_CreateMutex())) {
return false; return false;
@@ -33,8 +99,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
if (serial) { if (serial) {
file_handler->serial = SDL_strdup(serial); file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) { if (!file_handler->serial) {
LOGW("Could not strdup serial"); LOGW("Cannot strdup serial");
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex); SDL_DestroyMutex(file_handler->mutex);
return false; return false;
} }
@@ -48,8 +113,6 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
file_handler->stopped = false; file_handler->stopped = false;
file_handler->current_process = PROCESS_NONE; file_handler->current_process = PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
return true; return true;
} }
@@ -57,12 +120,8 @@ void
file_handler_destroy(struct file_handler *file_handler) { file_handler_destroy(struct file_handler *file_handler) {
SDL_DestroyCond(file_handler->event_cond); SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex); SDL_DestroyMutex(file_handler->mutex);
SDL_free(file_handler->serial); request_queue_destroy(&file_handler->queue);
SDL_free((void *) file_handler->serial);
struct file_handler_request req;
while (cbuf_take(&file_handler->queue, &req)) {
file_handler_request_destroy(&req);
}
} }
static process_t static process_t
@@ -71,13 +130,16 @@ install_apk(const char *serial, const char *file) {
} }
static process_t static process_t
push_file(const char *serial, const char *file, const char *push_target) { push_file(const char *serial, const char *file) {
return adb_push(serial, file, push_target); return adb_push(serial, file, DEVICE_SDCARD_PATH);
} }
bool bool
file_handler_request(struct file_handler *file_handler, file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, char *file) { file_handler_action_t action,
const char *file) {
bool res;
// start file_handler if it's used for the first time // start file_handler if it's used for the first time
if (!file_handler->initialized) { if (!file_handler->initialized) {
if (!file_handler_start(file_handler)) { if (!file_handler_start(file_handler)) {
@@ -88,14 +150,15 @@ file_handler_request(struct file_handler *file_handler,
LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push",
file); file);
struct file_handler_request req = { struct request *req = request_new(action, file);
.action = action, if (!req) {
.file = file, LOGE("Could not create request");
}; return false;
}
mutex_lock(file_handler->mutex); mutex_lock(file_handler->mutex);
bool was_empty = cbuf_is_empty(&file_handler->queue); bool was_empty = request_queue_is_empty(&file_handler->queue);
bool res = cbuf_push(&file_handler->queue, req); res = request_queue_push(&file_handler->queue, req);
if (was_empty) { if (was_empty) {
cond_signal(file_handler->event_cond); cond_signal(file_handler->event_cond);
} }
@@ -110,7 +173,8 @@ run_file_handler(void *data) {
for (;;) { for (;;) {
mutex_lock(file_handler->mutex); mutex_lock(file_handler->mutex);
file_handler->current_process = PROCESS_NONE; 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); cond_wait(file_handler->event_cond, file_handler->mutex);
} }
if (file_handler->stopped) { if (file_handler->stopped) {
@@ -118,39 +182,36 @@ run_file_handler(void *data) {
mutex_unlock(file_handler->mutex); mutex_unlock(file_handler->mutex);
break; break;
} }
struct file_handler_request req; struct request *req;
bool non_empty = cbuf_take(&file_handler->queue, &req); bool non_empty = request_queue_take(&file_handler->queue, &req);
SDL_assert(non_empty); SDL_assert(non_empty);
process_t process; process_t process;
if (req.action == ACTION_INSTALL_APK) { if (req->action == ACTION_INSTALL_APK) {
LOGI("Installing %s...", req.file); LOGI("Installing %s...", req->file);
process = install_apk(file_handler->serial, req.file); process = install_apk(file_handler->serial, req->file);
} else { } else {
LOGI("Pushing %s...", req.file); LOGI("Pushing %s...", req->file);
process = push_file(file_handler->serial, req.file, process = push_file(file_handler->serial, req->file);
file_handler->push_target);
} }
file_handler->current_process = process; file_handler->current_process = process;
mutex_unlock(file_handler->mutex); mutex_unlock(file_handler->mutex);
if (req.action == ACTION_INSTALL_APK) { if (req->action == ACTION_INSTALL_APK) {
if (process_check_success(process, "adb install")) { if (process_check_success(process, "adb install")) {
LOGI("%s successfully installed", req.file); LOGI("%s successfully installed", req->file);
} else { } else {
LOGE("Failed to install %s", req.file); LOGE("Failed to install %s", req->file);
} }
} else { } else {
if (process_check_success(process, "adb push")) { if (process_check_success(process, "adb push")) {
LOGI("%s successfully pushed to %s", req.file, LOGI("%s successfully pushed to /sdcard/", req->file);
file_handler->push_target);
} else { } else {
LOGE("Failed to push %s to %s", req.file, LOGE("Failed to push %s to /sdcard/", req->file);
file_handler->push_target);
} }
} }
file_handler_request_destroy(&req); request_free(req);
} }
return 0; return 0;
} }
@@ -176,7 +237,7 @@ file_handler_stop(struct file_handler *file_handler) {
cond_signal(file_handler->event_cond); cond_signal(file_handler->event_cond);
if (file_handler->current_process != PROCESS_NONE) { if (file_handler->current_process != PROCESS_NONE) {
if (!cmd_terminate(file_handler->current_process)) { 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); cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE; file_handler->current_process = PROCESS_NONE;

View File

@@ -5,36 +5,34 @@
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "cbuf.h"
#include "command.h" #include "command.h"
#define REQUEST_QUEUE_SIZE 16
typedef enum { typedef enum {
ACTION_INSTALL_APK, ACTION_INSTALL_APK,
ACTION_PUSH_FILE, ACTION_PUSH_FILE,
} file_handler_action_t; } file_handler_action_t;
struct file_handler_request { struct request_queue {
file_handler_action_t action; struct request *reqs[REQUEST_QUEUE_SIZE];
char *file; int tail;
int head;
}; };
struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler { struct file_handler {
char *serial; const char *serial;
const char *push_target;
SDL_Thread *thread; SDL_Thread *thread;
SDL_mutex *mutex; SDL_mutex *mutex;
SDL_cond *event_cond; SDL_cond *event_cond;
bool stopped; bool stopped;
bool initialized; bool initialized;
process_t current_process; process_t current_process;
struct file_handler_request_queue queue; struct request_queue queue;
}; };
bool bool
file_handler_init(struct file_handler *file_handler, const char *serial, file_handler_init(struct file_handler *file_handler, const char *serial);
const char *push_target);
void void
file_handler_destroy(struct file_handler *file_handler); file_handler_destroy(struct file_handler *file_handler);
@@ -48,10 +46,9 @@ file_handler_stop(struct file_handler *file_handler);
void void
file_handler_join(struct file_handler *file_handler); file_handler_join(struct file_handler *file_handler);
// take ownership of file, and will SDL_free() it
bool bool
file_handler_request(struct file_handler *file_handler, file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, file_handler_action_t action,
char *file); const char *file);
#endif #endif

View File

@@ -1,169 +1,70 @@
#include "fps_counter.h" #include "fps_counter.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include "lock_util.h"
#include "log.h" #include "log.h"
#define FPS_COUNTER_INTERVAL_MS 1000 void
bool
fps_counter_init(struct fps_counter *counter) { fps_counter_init(struct fps_counter *counter) {
counter->mutex = SDL_CreateMutex(); counter->started = false;
if (!counter->mutex) { // no need to initialize the other fields, they are meaningful only when
return false; // started is true
}
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 void
fps_counter_destroy(struct fps_counter *counter) {
SDL_DestroyCond(counter->state_cond);
SDL_DestroyMutex(counter->mutex);
}
// 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;
if (counter->nr_skipped) {
LOGI("%u fps (+%u frames skipped)", rendered_per_second,
counter->nr_skipped);
} else {
LOGI("%u fps", rendered_per_second);
}
}
// 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) { fps_counter_start(struct fps_counter *counter) {
mutex_lock(counter->mutex); counter->started = true;
counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; counter->slice_start = SDL_GetTicks();
counter->nr_rendered = 0; counter->nr_rendered = 0;
#ifdef SKIP_FRAMES
counter->nr_skipped = 0; counter->nr_skipped = 0;
mutex_unlock(counter->mutex); #endif
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 void
fps_counter_stop(struct fps_counter *counter) { fps_counter_stop(struct fps_counter *counter) {
SDL_AtomicSet(&counter->started, 0); counter->started = false;
cond_signal(counter->state_cond);
} }
bool static void
fps_counter_is_started(struct fps_counter *counter) { display_fps(struct fps_counter *counter) {
return SDL_AtomicGet(&counter->started); #ifdef SKIP_FRAMES
if (counter->nr_skipped) {
LOGI("%d fps (+%d frames skipped)", counter->nr_rendered,
counter->nr_skipped);
} else {
#endif
LOGI("%d fps", counter->nr_rendered);
#ifdef SKIP_FRAMES
}
#endif
} }
void static void
fps_counter_interrupt(struct fps_counter *counter) { check_expired(struct fps_counter *counter) {
if (!counter->thread) { uint32_t now = SDL_GetTicks();
return; if (now - counter->slice_start >= 1000) {
} display_fps(counter);
// add a multiple of one second
mutex_lock(counter->mutex); uint32_t elapsed_slices = (now - counter->slice_start) / 1000;
counter->interrupted = true; counter->slice_start += 1000 * elapsed_slices;
mutex_unlock(counter->mutex); counter->nr_rendered = 0;
// wake up blocking wait #ifdef SKIP_FRAMES
cond_signal(counter->state_cond); counter->nr_skipped = 0;
} #endif
void
fps_counter_join(struct fps_counter *counter) {
if (counter->thread) {
SDL_WaitThread(counter->thread, NULL);
} }
} }
void void
fps_counter_add_rendered_frame(struct fps_counter *counter) { fps_counter_add_rendered_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) { check_expired(counter);
return;
}
mutex_lock(counter->mutex);
uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now);
++counter->nr_rendered; ++counter->nr_rendered;
mutex_unlock(counter->mutex);
} }
#ifdef SKIP_FRAMES
void void
fps_counter_add_skipped_frame(struct fps_counter *counter) { fps_counter_add_skipped_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) { check_expired(counter);
return;
}
mutex_lock(counter->mutex);
uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now);
++counter->nr_skipped; ++counter->nr_skipped;
mutex_unlock(counter->mutex);
} }
#endif

View File

@@ -3,53 +3,33 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_mutex.h> #include "config.h"
#include <SDL2/SDL_thread.h>
struct fps_counter { struct fps_counter {
SDL_Thread *thread; bool started;
SDL_mutex *mutex; uint32_t slice_start; // initialized by SDL_GetTicks()
SDL_cond *state_cond; int nr_rendered;
#ifdef SKIP_FRAMES
// atomic so that we can check without locking the mutex int nr_skipped;
// if the FPS counter is disabled, we don't want to lock unnecessarily #endif
SDL_atomic_t started;
// the following fields are protected by the mutex
bool interrupted;
unsigned nr_rendered;
unsigned nr_skipped;
uint32_t next_timestamp;
}; };
bool void
fps_counter_init(struct fps_counter *counter); fps_counter_init(struct fps_counter *counter);
void void
fps_counter_destroy(struct fps_counter *counter);
bool
fps_counter_start(struct fps_counter *counter); fps_counter_start(struct fps_counter *counter);
void void
fps_counter_stop(struct fps_counter *counter); 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 void
fps_counter_add_rendered_frame(struct fps_counter *counter); fps_counter_add_rendered_frame(struct fps_counter *counter);
#ifdef SKIP_FRAMES
void void
fps_counter_add_skipped_frame(struct fps_counter *counter); fps_counter_add_skipped_frame(struct fps_counter *counter);
#endif
#endif #endif

View File

@@ -39,23 +39,23 @@ static void
send_keycode(struct controller *controller, enum android_keycode keycode, send_keycode(struct controller *controller, enum android_keycode keycode,
int actions, const char *name) { int actions, const char *name) {
// send DOWN event // send DOWN event
struct control_msg msg; struct control_event control_event;
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
msg.inject_keycode.keycode = keycode; control_event.keycode_event.keycode = keycode;
msg.inject_keycode.metastate = 0; control_event.keycode_event.metastate = 0;
if (actions & ACTION_DOWN) { if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Could not request 'inject %s (DOWN)'", name); LOGW("Cannot send %s (DOWN)", name);
return; return;
} }
} }
if (actions & ACTION_UP) { if (actions & ACTION_UP) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Could not request 'inject %s (UP)'", name); LOGW("Cannot send %s (UP)", name);
} }
} }
} }
@@ -98,100 +98,58 @@ action_menu(struct controller *controller, int actions) {
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
static void static void
press_back_or_turn_screen_on(struct controller *controller) { press_back_or_turn_screen_on(struct controller *controller) {
struct control_msg msg; struct control_event control_event;
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; 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)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Could not request 'turn screen on'"); LOGW("Cannot turn screen on");
} }
} }
static void static void
expand_notification_panel(struct controller *controller) { expand_notification_panel(struct controller *controller) {
struct control_msg msg; struct control_event control_event;
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action =
CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Could not request 'expand notification panel'"); LOGW("Cannot expand notification panel");
} }
} }
static void static void
collapse_notification_panel(struct controller *controller) { collapse_notification_panel(struct controller *controller) {
struct control_msg msg; struct control_event control_event;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action =
CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Could not request 'collapse notification panel'"); LOGW("Cannot collapse notification panel");
} }
} }
static void static void
request_device_clipboard(struct controller *controller) { switch_fps_counter_state(struct video_buffer *vb) {
struct control_msg msg; mutex_lock(vb->mutex);
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; if (vb->fps_counter.started) {
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);
LOGI("FPS counter stopped"); LOGI("FPS counter stopped");
fps_counter_stop(&vb->fps_counter);
} else { } else {
if (fps_counter_start(fps_counter)) {
LOGI("FPS counter started"); LOGI("FPS counter started");
} else { fps_counter_start(&vb->fps_counter);
LOGE("FPS counter starting failed");
}
} }
mutex_unlock(vb->mutex);
} }
static void static void
clipboard_paste(struct controller *controller) { clipboard_paste(struct controller *controller) {
char *text = SDL_GetClipboardText(); char *text = SDL_GetClipboardText();
if (!text) { if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError()); LOGW("Cannot get clipboard text: %s", SDL_GetError());
return; return;
} }
if (!*text) { if (!*text) {
@@ -200,12 +158,12 @@ clipboard_paste(struct controller *controller) {
return; return;
} }
struct control_msg msg; struct control_event control_event;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; control_event.type = CONTROL_EVENT_TYPE_TEXT;
msg.inject_text.text = text; control_event.text_event.text = text;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_event(controller, &control_event)) {
SDL_free(text); SDL_free(text);
LOGW("Could not request 'paste clipboard'"); LOGW("Cannot send clipboard paste event");
} }
} }
@@ -218,16 +176,16 @@ input_manager_process_text_input(struct input_manager *input_manager,
// letters and space are handled as raw key event // letters and space are handled as raw key event
return; return;
} }
struct control_msg msg; struct control_event control_event;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; control_event.type = CONTROL_EVENT_TYPE_TEXT;
msg.inject_text.text = SDL_strdup(event->text); control_event.text_event.text = SDL_strdup(event->text);
if (!msg.inject_text.text) { if (!control_event.text_event.text) {
LOGW("Could not strdup input text"); LOGW("Cannot strdup input text");
return; return;
} }
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_event(input_manager->controller, &control_event)) {
SDL_free(msg.inject_text.text); SDL_free(control_event.text_event.text);
LOGW("Could not request 'inject text'"); LOGW("Cannot send text event");
} }
} }
@@ -235,131 +193,106 @@ void
input_manager_process_key(struct input_manager *input_manager, input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event,
bool control) { 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 ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); 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
if (alt) { 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; return;
} }
struct controller *controller = input_manager->controller;
// capture all Ctrl events // capture all Ctrl events
if (ctrl || cmd) { if (ctrl | meta) {
SDL_Keycode keycode = event->keysym.sym; SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN; int action = event->type == SDL_KEYDOWN ? ACTION_DOWN : ACTION_UP;
int action = down ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat; bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) { switch (keycode) {
case SDLK_h: 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) { if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action); action_home(input_manager->controller, action);
} }
return; return;
case SDLK_b: // fall-through case SDLK_b: // fall-through
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
if (control && cmd && !shift && !repeat) { if (control && ctrl && !meta && !shift && !repeat) {
action_back(controller, action); action_back(input_manager->controller, action);
} }
return; return;
case SDLK_s: case SDLK_s:
if (control && cmd && !shift && !repeat) { if (control && ctrl && !meta && !shift && !repeat) {
action_app_switch(controller, action); action_app_switch(input_manager->controller, action);
} }
return; return;
case SDLK_m: 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) { if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action); action_menu(input_manager->controller, action);
} }
return; return;
case SDLK_p: case SDLK_p:
if (control && cmd && !shift && !repeat) { if (control && ctrl && !meta && !shift && !repeat) {
action_power(controller, action); action_power(input_manager->controller, action);
}
return;
case SDLK_o:
if (control && cmd && !shift && down) {
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (control && cmd && !shift) { #ifdef __APPLE__
if (control && !ctrl && meta && !shift) {
#else
if (control && ctrl && !meta && !shift) {
#endif
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(input_manager->controller, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (control && cmd && !shift) { #ifdef __APPLE__
if (control && !ctrl && meta && !shift) {
#else
if (control && ctrl && !meta && !shift) {
#endif
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(input_manager->controller, action);
}
return;
case SDLK_c:
if (control && cmd && !shift && !repeat && down) {
request_device_clipboard(controller);
} }
return; return;
case SDLK_v: case SDLK_v:
if (control && cmd && !repeat && down) { if (control && ctrl && !meta && !shift && !repeat
if (shift) { && event->type == SDL_KEYDOWN) {
// store the text in the device clipboard clipboard_paste(input_manager->controller);
set_device_clipboard(controller);
} else {
// inject the text as input events
clipboard_paste(controller);
}
} }
return; return;
case SDLK_f: case SDLK_f:
if (!shift && cmd && !repeat && down) { if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
screen_switch_fullscreen(input_manager->screen); screen_switch_fullscreen(input_manager->screen);
} }
return; return;
case SDLK_x: case SDLK_x:
if (!shift && cmd && !repeat && down) { if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
screen_resize_to_fit(input_manager->screen); screen_resize_to_fit(input_manager->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && cmd && !repeat && down) { if (ctrl && !meta && !shift && !repeat
&& event->type == SDL_KEYDOWN) {
screen_resize_to_pixel_perfect(input_manager->screen); screen_resize_to_pixel_perfect(input_manager->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && cmd && !repeat && down) { if (ctrl && !meta && !shift && !repeat
struct fps_counter *fps_counter = && event->type == SDL_KEYDOWN) {
input_manager->video_buffer->fps_counter; switch_fps_counter_state(input_manager->video_buffer);
switch_fps_counter_state(fps_counter);
} }
return; return;
case SDLK_n: case SDLK_n:
if (control && cmd && !repeat && down) { if (control && ctrl && !meta
&& !repeat && event->type == SDL_KEYDOWN) {
if (shift) { if (shift) {
collapse_notification_panel(controller); collapse_notification_panel(input_manager->controller);
} else { } else {
expand_notification_panel(controller); expand_notification_panel(input_manager->controller);
} }
} }
return; return;
@@ -372,10 +305,10 @@ input_manager_process_key(struct input_manager *input_manager,
return; return;
} }
struct control_msg msg; struct control_event control_event;
if (input_key_from_sdl_to_android(event, &msg)) { if (input_key_from_sdl_to_android(event, &control_event)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Could not request 'inject keycode'"); LOGW("Cannot send control event");
} }
} }
} }
@@ -387,12 +320,12 @@ input_manager_process_mouse_motion(struct input_manager *input_manager,
// do not send motion events when no button is pressed // do not send motion events when no button is pressed
return; return;
} }
struct control_msg msg; struct control_event control_event;
if (mouse_motion_from_sdl_to_android(event, if (mouse_motion_from_sdl_to_android(event,
input_manager->screen->frame_size, input_manager->screen->frame_size,
&msg)) { &control_event)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Could not request 'inject mouse motion event'"); LOGW("Cannot send mouse motion event");
} }
} }
} }
@@ -419,8 +352,9 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
} }
// double-click on black borders resize to fit the device screen // double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside = bool outside = is_outside_device_screen(input_manager,
is_outside_device_screen(input_manager, event->x, event->y); event->x,
event->y);
if (outside) { if (outside) {
screen_resize_to_fit(input_manager->screen); screen_resize_to_fit(input_manager->screen);
return; return;
@@ -433,12 +367,12 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
return; return;
} }
struct control_msg msg; struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, if (mouse_button_from_sdl_to_android(event,
input_manager->screen->frame_size, input_manager->screen->frame_size,
&msg)) { &control_event)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Could not request 'inject mouse button event'"); LOGW("Cannot send mouse button event");
} }
} }
} }
@@ -450,10 +384,10 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager,
.screen_size = input_manager->screen->frame_size, .screen_size = input_manager->screen->frame_size,
.point = get_mouse_point(input_manager->screen), .point = get_mouse_point(input_manager->screen),
}; };
struct control_msg msg; struct control_event control_event;
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) { if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) {
if (!controller_push_msg(input_manager->controller, &msg)) { if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Could not request 'inject mouse wheel event'"); LOGW("Cannot send mouse wheel event");
} }
} }
} }

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

@@ -0,0 +1,38 @@
#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,20 @@
#ifndef LOCKUTIL_H #ifndef LOCKUTIL_H
#define LOCKUTIL_H #define LOCKUTIL_H
#include <stdint.h> // forward declarations
#include <SDL2/SDL_mutex.h> typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
#include "log.h" void
mutex_lock(SDL_mutex *mutex);
static inline void void
mutex_lock(SDL_mutex *mutex) { mutex_unlock(SDL_mutex *mutex);
if (SDL_LockMutex(mutex)) {
LOGC("Could not lock mutex");
abort();
}
}
static inline void void
mutex_unlock(SDL_mutex *mutex) { cond_wait(SDL_cond *cond, SDL_mutex *mutex);
if (SDL_UnlockMutex(mutex)) {
LOGC("Could not unlock mutex");
abort();
}
}
static inline void void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) { cond_signal(SDL_cond *cond);
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();
}
}
#endif #endif

View File

@@ -5,7 +5,6 @@
#include <stdint.h> #include <stdint.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "compat.h" #include "compat.h"
@@ -17,8 +16,6 @@ struct args {
const char *serial; const char *serial;
const char *crop; const char *crop;
const char *record_filename; const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format; enum recorder_format record_format;
bool fullscreen; bool fullscreen;
bool no_control; bool no_control;
@@ -30,16 +27,9 @@ struct args {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
bool always_on_top; bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
}; };
static void usage(const char *arg0) { static void usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr, fprintf(stderr,
"Usage: %s [options]\n" "Usage: %s [options]\n"
"\n" "\n"
@@ -82,29 +72,15 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n" " Set the TCP port the client listens on.\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" --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" " -r, --record file.mp4\n"
" Record screen to file.\n" " Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n" " The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n" " set, or by the file extension (.mp4 or .mkv).\n"
"\n" "\n"
" --render-expired-frames\n" " -s, --serial\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"
" The device serial number. Mandatory only if several devices\n" " The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n" " are connected to adb.\n"
"\n" "\n"
" -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n"
"\n"
" -t, --show-touches\n" " -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n" " Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n" " It only shows physical touches (not clicks from scrcpy).\n"
@@ -115,67 +91,56 @@ static void usage(const char *arg0) {
" -v, --version\n" " -v, --version\n"
" Print the version of scrcpy.\n" " Print the version of scrcpy.\n"
"\n" "\n"
" --window-title text\n"
" Set a custom window title.\n"
"\n"
"Shortcuts:\n" "Shortcuts:\n"
"\n" "\n"
" " CTRL_OR_CMD "+f\n" " Ctrl+f\n"
" switch fullscreen mode\n" " switch fullscreen mode\n"
"\n" "\n"
" " CTRL_OR_CMD "+g\n" " Ctrl+g\n"
" resize window to 1:1 (pixel-perfect)\n" " resize window to 1:1 (pixel-perfect)\n"
"\n" "\n"
" " CTRL_OR_CMD "+x\n" " Ctrl+x\n"
" Double-click on black borders\n" " Double-click on black borders\n"
" resize window to remove black borders\n" " resize window to remove black borders\n"
"\n" "\n"
" Ctrl+h\n" " Ctrl+h\n"
" Home\n"
" Middle-click\n" " Middle-click\n"
" click on HOME\n" " click on HOME\n"
"\n" "\n"
" " CTRL_OR_CMD "+b\n" " Ctrl+b\n"
" " CTRL_OR_CMD "+Backspace\n" " Ctrl+Backspace\n"
" Right-click (when screen is on)\n" " Right-click (when screen is on)\n"
" click on BACK\n" " click on BACK\n"
"\n" "\n"
" " CTRL_OR_CMD "+s\n" " Ctrl+s\n"
" click on APP_SWITCH\n" " click on APP_SWITCH\n"
"\n" "\n"
" Ctrl+m\n" " Ctrl+m\n"
" click on MENU\n" " click on MENU\n"
"\n" "\n"
" " CTRL_OR_CMD "+Up\n" " Ctrl+Up\n"
" click on VOLUME_UP\n" " click on VOLUME_UP\n"
"\n" "\n"
" " CTRL_OR_CMD "+Down\n" " Ctrl+Down\n"
" click on VOLUME_DOWN\n" " click on VOLUME_DOWN\n"
"\n" "\n"
" " CTRL_OR_CMD "+p\n" " Ctrl+p\n"
" click on POWER (turn screen on/off)\n" " click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click (when screen is off)\n" " Right-click (when screen is off)\n"
" power on\n" " turn screen on\n"
"\n" "\n"
" " CTRL_OR_CMD "+o\n" " Ctrl+n\n"
" turn device screen off (keep mirroring)\n"
"\n"
" " CTRL_OR_CMD "+n\n"
" expand notification panel\n" " expand notification panel\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+n\n" " Ctrl+Shift+n\n"
" collapse notification panel\n" " collapse notification panel\n"
"\n" "\n"
" " CTRL_OR_CMD "+c\n" " Ctrl+v\n"
" copy device clipboard to computer\n"
"\n"
" " CTRL_OR_CMD "+v\n"
" paste computer clipboard to device\n" " paste computer clipboard to device\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+v\n" " Ctrl+i\n"
" copy computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+i\n"
" enable/disable FPS counter (print frames/second in logs)\n" " enable/disable FPS counter (print frames/second in logs)\n"
"\n" "\n"
" Drag & drop APK file\n" " Drag & drop APK file\n"
@@ -309,10 +274,6 @@ guess_record_format(const char *filename) {
return 0; return 0;
} }
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
static bool static bool
parse_args(struct args *args, int argc, char *argv[]) { parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
@@ -325,22 +286,15 @@ parse_args(struct args *args, int argc, char *argv[]) {
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL,
OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'}, {"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'}, {"record-format", required_argument, NULL, 'f'},
{"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'}, {"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL,
OPT_WINDOW_TITLE},
{NULL, 0, NULL, 0 }, {NULL, 0, NULL, 0 },
}; };
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options, while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options,
NULL)) != -1) { NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b':
@@ -384,9 +338,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 's': case 's':
args->serial = optarg; args->serial = optarg;
break; break;
case 'S':
args->turn_screen_off = true;
break;
case 't': case 't':
args->show_touches = true; args->show_touches = true;
break; break;
@@ -396,15 +347,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 'v': case 'v':
args->version = true; args->version = true;
break; 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;
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@@ -441,11 +383,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
} }
} }
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 true;
} }
@@ -461,8 +398,6 @@ main(int argc, char *argv[]) {
.serial = NULL, .serial = NULL,
.crop = NULL, .crop = NULL,
.record_filename = NULL, .record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0, .record_format = 0,
.help = false, .help = false,
.version = false, .version = false,
@@ -473,8 +408,6 @@ main(int argc, char *argv[]) {
.always_on_top = false, .always_on_top = false,
.no_control = false, .no_control = false,
.no_display = false, .no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
}; };
if (!parse_args(&args, argc, argv)) { if (!parse_args(&args, argc, argv)) {
return 1; return 1;
@@ -490,8 +423,6 @@ main(int argc, char *argv[]) {
return 0; return 0;
} }
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all(); av_register_all();
#endif #endif
@@ -509,18 +440,14 @@ main(int argc, char *argv[]) {
.crop = args.crop, .crop = args.crop,
.port = args.port, .port = args.port,
.record_filename = args.record_filename, .record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format, .record_format = args.record_format,
.max_size = args.max_size, .max_size = args.max_size,
.bit_rate = args.bit_rate, .bit_rate = args.bit_rate,
.show_touches = args.show_touches, .show_touches = args.show_touches,
.fullscreen = args.fullscreen, .fullscreen = args.fullscreen,
.always_on_top = args.always_on_top, .always_on_top = args.always_on_top,
.control = !args.no_control, .no_control = args.no_control,
.display = !args.no_display, .no_display = args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
}; };
int res = scrcpy(&options) ? 0 : 1; int res = scrcpy(&options) ? 0 : 1;

View File

@@ -33,7 +33,6 @@ net_connect(uint32_t addr, uint16_t port) {
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("connect"); perror("connect");
net_close(sock);
return INVALID_SOCKET; return INVALID_SOCKET;
} }
@@ -61,13 +60,11 @@ net_listen(uint32_t addr, uint16_t port, int backlog) {
if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("bind"); perror("bind");
net_close(sock);
return INVALID_SOCKET; return INVALID_SOCKET;
} }
if (listen(sock, backlog) == SOCKET_ERROR) { if (listen(sock, backlog) == SOCKET_ERROR) {
perror("listen"); perror("listen");
net_close(sock);
return INVALID_SOCKET; return INVALID_SOCKET;
} }

View File

@@ -1,74 +0,0 @@
// generic intrusive FIFO queue
#ifndef QUEUE_H
#define QUEUE_H
#include <stdbool.h>
#include <SDL2/SDL_assert.h>
// To define a queue type of "struct foo":
// struct queue_foo QUEUE(struct foo);
#define QUEUE(TYPE) { \
TYPE *first; \
TYPE *last; \
}
#define queue_init(PQ) \
(void) ((PQ)->first = NULL)
#define queue_is_empty(PQ) \
!(PQ)->first
// NEXTFIELD is the field in the ITEM type used for intrusive linked-list
//
// For example:
// struct foo {
// int value;
// struct foo *next;
// };
//
// // define the type "struct my_queue"
// struct my_queue QUEUE(struct foo);
//
// struct my_queue queue;
// queue_init(&queue);
//
// struct foo v1 = { .value = 42 };
// struct foo v2 = { .value = 27 };
//
// queue_push(&queue, next, v1);
// queue_push(&queue, next, v2);
//
// struct foo *foo;
// queue_take(&queue, next, &foo);
// assert(foo->value == 42);
// queue_take(&queue, next, &foo);
// assert(foo->value == 27);
// assert(queue_is_empty(&queue));
//
// push a new item into the queue
#define queue_push(PQ, NEXTFIELD, ITEM) \
(void) ({ \
(ITEM)->NEXTFIELD = NULL; \
if (queue_is_empty(PQ)) { \
(PQ)->first = (PQ)->last = (ITEM); \
} else { \
(PQ)->last->NEXTFIELD = (ITEM); \
(PQ)->last = (ITEM); \
} \
})
// take the next item and remove it from the queue (the queue must not be empty)
// the result is stored in *(PITEM)
// (without typeof(), we could not store a local variable having the correct
// type so that we can "return" it)
#define queue_take(PQ, NEXTFIELD, PITEM) \
(void) ({ \
SDL_assert(!queue_is_empty(PQ)); \
*(PITEM) = (PQ)->first; \
(PQ)->first = (PQ)->first->NEXTFIELD; \
})
// no need to update (PQ)->last if the queue is left empty:
// (PQ)->last is undefined if !(PQ)->first anyway
#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

@@ -5,7 +5,6 @@
#include "compat.h" #include "compat.h"
#include "config.h" #include "config.h"
#include "lock_util.h"
#include "log.h" #include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@@ -27,34 +26,6 @@ find_muxer(const char *name) {
return oformat; 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;
}
return rec;
}
static void
record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet);
SDL_free(rec);
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
while (!queue_is_empty(queue)) {
struct record_packet *rec;
queue_take(queue, next, &rec);
record_packet_delete(rec);
}
}
bool bool
recorder_init(struct recorder *recorder, recorder_init(struct recorder *recorder,
const char *filename, const char *filename,
@@ -62,28 +33,10 @@ recorder_init(struct recorder *recorder,
struct size declared_frame_size) { struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename); recorder->filename = SDL_strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
LOGE("Could not strdup filename"); LOGE("Cannot strdup filename");
return false; return 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;
}
queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format; recorder->format = format;
recorder->declared_frame_size = declared_frame_size; recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false; recorder->header_written = false;
@@ -93,8 +46,6 @@ recorder_init(struct recorder *recorder,
void void
recorder_destroy(struct recorder *recorder) { recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename); SDL_free(recorder->filename);
} }
@@ -168,18 +119,13 @@ recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx); int ret = av_write_trailer(recorder->ctx);
if (ret < 0) { if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename); LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
} }
avio_close(recorder->ctx->pb); avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx); 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); const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename); LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
} }
}
static bool static bool
recorder_write_header(struct recorder *recorder, const AVPacket *packet) { recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
@@ -187,7 +133,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) { if (!extradata) {
LOGC("Could not allocate extradata"); LOGC("Cannot allocate extradata");
return false; return false;
} }
@@ -205,6 +151,9 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
int ret = avformat_write_header(recorder->ctx, NULL); int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) { if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename); LOGE("Failed to write header to %s", recorder->filename);
SDL_free(extradata);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return false; return false;
} }
@@ -220,116 +169,13 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
bool bool
recorder_write(struct recorder *recorder, AVPacket *packet) { recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) { 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); bool ok = recorder_write_header(recorder, packet);
if (!ok) { if (!ok) {
return false; return false;
} }
recorder->header_written = true; recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
} }
recorder_rescale_packet(recorder, packet); recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0; 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 && 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 && queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex);
break;
}
struct record_packet *rec;
queue_take(&recorder->queue, next, &rec);
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;
}
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
return false;
}
queue_push(&recorder->queue, next, rec);
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
return true;
}

View File

@@ -3,41 +3,24 @@
#include <stdbool.h> #include <stdbool.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "common.h" #include "common.h"
#include "queue.h"
enum recorder_format { enum recorder_format {
RECORDER_FORMAT_MP4 = 1, RECORDER_FORMAT_MP4 = 1,
RECORDER_FORMAT_MKV, RECORDER_FORMAT_MKV,
}; };
struct record_packet {
AVPacket packet;
struct record_packet *next;
};
struct recorder_queue QUEUE(struct record_packet);
struct recorder { struct recorder {
char *filename; char *filename;
enum recorder_format format; enum recorder_format format;
AVFormatContext *ctx; AVFormatContext *ctx;
struct size declared_frame_size; struct size declared_frame_size;
bool header_written; 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;
}; };
bool bool
recorder_init(struct recorder *recorder, const char *filename, recorder_init(struct recorder *recoder, const char *filename,
enum recorder_format format, struct size declared_frame_size); enum recorder_format format, struct size declared_frame_size);
void void
@@ -50,15 +33,6 @@ void
recorder_close(struct recorder *recorder); recorder_close(struct recorder *recorder);
bool bool
recorder_start(struct recorder *recorder); recorder_write(struct recorder *recorder, AVPacket *packet);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
#endif #endif

View File

@@ -9,7 +9,6 @@
#include "command.h" #include "command.h"
#include "common.h" #include "common.h"
#include "compat.h"
#include "controller.h" #include "controller.h"
#include "decoder.h" #include "decoder.h"
#include "device.h" #include "device.h"
@@ -29,7 +28,6 @@
static struct server server = SERVER_INITIALIZER; static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER; static struct screen screen = SCREEN_INITIALIZER;
static struct fps_counter fps_counter;
static struct video_buffer video_buffer; static struct video_buffer video_buffer;
static struct stream stream; static struct stream stream;
static struct decoder decoder; static struct decoder decoder;
@@ -70,18 +68,6 @@ sdl_init_and_configure(bool display) {
} }
#endif #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 // Do not disable the screensaver when scrcpy is running
SDL_EnableScreenSaver(); SDL_EnableScreenSaver();
@@ -138,7 +124,7 @@ handle_event(SDL_Event *event, bool control) {
screen_show_window(&screen); screen_show_window(&screen);
} }
if (!screen_update_frame(&screen, &video_buffer)) { if (!screen_update_frame(&screen, &video_buffer)) {
return EVENT_RESULT_CONTINUE; return false;
} }
break; break;
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
@@ -259,7 +245,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
} }
char *local_fmt = SDL_malloc(strlen(fmt) + 10); char *local_fmt = SDL_malloc(strlen(fmt) + 10);
if (!local_fmt) { if (!local_fmt) {
LOGC("Could not allocate string"); LOGC("Cannot allocate string");
return; return;
} }
// strcpy is safe here, the destination is large enough // strcpy is safe here, the destination is large enough
@@ -272,14 +258,9 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
bool bool
scrcpy(const struct scrcpy_options *options) { scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename; bool record = !!options->record_filename;
struct server_params params = { if (!server_start(&server, options->serial, options->port,
.crop = options->crop, options->max_size, options->bit_rate, options->crop,
.local_port = options->port, record)) {
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.control = options->control,
};
if (!server_start(&server, options->serial, &params)) {
return false; return false;
} }
@@ -291,22 +272,21 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = false; show_touches_waited = false;
} }
bool ret = false; bool ret = true;
bool fps_counter_initialized = false; bool display = !options->no_display;
bool video_buffer_initialized = false; bool control = !options->no_control;
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)) { if (!sdl_init_and_configure(display)) {
goto end; ret = false;
goto finally_destroy_server;
} }
if (!server_connect_to(&server)) { socket_t device_socket = server_connect_to(&server);
goto end; if (device_socket == INVALID_SOCKET) {
server_stop(&server);
ret = false;
goto finally_destroy_server;
} }
char device_name[DEVICE_NAME_FIELD_LENGTH]; char device_name[DEVICE_NAME_FIELD_LENGTH];
@@ -315,29 +295,24 @@ scrcpy(const struct scrcpy_options *options) {
// screenrecord does not send frames when the screen content does not // screenrecord does not send frames when the screen content does not
// change therefore, we transmit the screen size before the video stream, // change therefore, we transmit the screen size before the video stream,
// to be able to init the window immediately // to be able to init the window immediately
if (!device_read_info(server.video_socket, device_name, &frame_size)) { if (!device_read_info(device_socket, device_name, &frame_size)) {
goto end; server_stop(&server);
ret = false;
goto finally_destroy_server;
} }
struct decoder *dec = NULL; struct decoder *dec = NULL;
if (options->display) { if (display) {
if (!fps_counter_init(&fps_counter)) { if (!video_buffer_init(&video_buffer)) {
goto end; server_stop(&server);
ret = false;
goto finally_destroy_server;
} }
fps_counter_initialized = true;
if (!video_buffer_init(&video_buffer, &fps_counter, if (control && !file_handler_init(&file_handler, server.serial)) {
options->render_expired_frames)) { ret = false;
goto end; server_stop(&server);
} goto finally_destroy_video_buffer;
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); decoder_init(&decoder, &video_buffer);
@@ -350,52 +325,42 @@ scrcpy(const struct scrcpy_options *options) {
options->record_filename, options->record_filename,
options->record_format, options->record_format,
frame_size)) { frame_size)) {
goto end; ret = false;
server_stop(&server);
goto finally_destroy_file_handler;
} }
rec = &recorder; rec = &recorder;
recorder_initialized = true;
} }
av_log_set_callback(av_log_callback); av_log_set_callback(av_log_callback);
stream_init(&stream, server.video_socket, dec, rec); stream_init(&stream, device_socket, dec, rec);
// now we consumed the header values, the socket receives the video stream // now we consumed the header values, the socket receives the video stream
// start the stream // start the stream
if (!stream_start(&stream)) { if (!stream_start(&stream)) {
goto end; ret = false;
server_stop(&server);
goto finally_destroy_recorder;
} }
stream_started = true;
if (options->display) { if (display) {
if (options->control) { if (control) {
if (!controller_init(&controller, server.control_socket)) { if (!controller_init(&controller, device_socket)) {
goto end; ret = false;
goto finally_stop_stream;
} }
controller_initialized = true;
if (!controller_start(&controller)) { if (!controller_start(&controller)) {
goto end; ret = false;
goto finally_destroy_controller;
} }
controller_started = true;
} }
const char *window_title = if (!screen_init_rendering(&screen, device_name, frame_size,
options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size,
options->always_on_top)) { options->always_on_top)) {
goto end; ret = false;
} goto finally_stop_and_join_controller;
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) { if (options->fullscreen) {
@@ -408,67 +373,48 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = true; show_touches_waited = true;
} }
ret = event_loop(options->display, options->control); ret = event_loop(display, control);
LOGD("quit..."); LOGD("quit...");
screen_destroy(&screen); screen_destroy(&screen);
end: finally_stop_and_join_controller:
// stop stream and controller so that they don't continue once their socket if (display && control) {
// is shutdown
if (stream_started) {
stream_stop(&stream);
}
if (controller_started) {
controller_stop(&controller); 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
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); controller_join(&controller);
} }
if (controller_initialized) { finally_destroy_controller:
if (display && control) {
controller_destroy(&controller); controller_destroy(&controller);
} }
finally_stop_stream:
if (recorder_initialized) { stream_stop(&stream);
// stop the server before stream_join() to wake up the stream
server_stop(&server);
stream_join(&stream);
finally_destroy_recorder:
if (record) {
recorder_destroy(&recorder); recorder_destroy(&recorder);
} }
finally_destroy_file_handler:
if (file_handler_initialized) { if (display && control) {
file_handler_stop(&file_handler);
file_handler_join(&file_handler); file_handler_join(&file_handler);
file_handler_destroy(&file_handler); file_handler_destroy(&file_handler);
} }
finally_destroy_video_buffer:
if (video_buffer_initialized) { if (display) {
video_buffer_destroy(&video_buffer); video_buffer_destroy(&video_buffer);
} }
finally_destroy_server:
if (fps_counter_initialized) {
fps_counter_join(&fps_counter);
fps_counter_destroy(&fps_counter);
}
if (options->show_touches) { if (options->show_touches) {
if (!show_touches_waited) { if (!show_touches_waited) {
// wait the process which enabled "show touches" // wait the process which enabled "show touches"
wait_show_touches(proc_show_touches); wait_show_touches(proc_show_touches);
} }
LOGI("Disable 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,
false);
wait_show_touches(proc_show_touches); wait_show_touches(proc_show_touches);
} }

View File

@@ -9,8 +9,6 @@ struct scrcpy_options {
const char *serial; const char *serial;
const char *crop; const char *crop;
const char *record_filename; const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format; enum recorder_format record_format;
uint16_t port; uint16_t port;
uint16_t max_size; uint16_t max_size;
@@ -18,10 +16,8 @@ struct scrcpy_options {
bool show_touches; bool show_touches;
bool fullscreen; bool fullscreen;
bool always_on_top; bool always_on_top;
bool control; bool no_control;
bool display; bool no_display;
bool turn_screen_off;
bool render_expired_frames;
}; };
bool bool

View File

@@ -85,7 +85,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
uint32_t h; uint32_t h;
if (!get_preferred_display_bounds(&display_size)) { 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; w = current_size.width;
h = current_size.height; h = current_size.height;
} else { } else {
@@ -134,7 +134,7 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
} }
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top) { struct size frame_size, bool always_on_top) {
screen->frame_size = frame_size; screen->frame_size = frame_size;
@@ -152,7 +152,7 @@ screen_init_rendering(struct screen *screen, const char *window_title,
#endif #endif
} }
screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height, window_size.width, window_size.height,
window_flags); window_flags);
@@ -177,12 +177,13 @@ screen_init_rendering(struct screen *screen, const char *window_title,
} }
SDL_Surface *icon = read_xpm(icon_xpm); SDL_Surface *icon = read_xpm(icon_xpm);
if (icon) { if (!icon) {
LOGE("Could not load icon: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
SDL_SetWindowIcon(screen->window, icon); SDL_SetWindowIcon(screen->window, icon);
SDL_FreeSurface(icon); SDL_FreeSurface(icon);
} else {
LOGW("Could not load icon");
}
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
frame_size.height); frame_size.height);

View File

@@ -44,7 +44,7 @@ screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden) // initialize screen, create window, renderer and texture (window is hidden)
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top); struct size frame_size, bool always_on_top);
// show the window // show the window

View File

@@ -2,67 +2,31 @@
#include <errno.h> #include <errno.h>
#include <inttypes.h> #include <inttypes.h>
#include <libgen.h>
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_assert.h> #include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include "config.h" #include "config.h"
#include "command.h"
#include "log.h" #include "log.h"
#include "net.h" #include "net.h"
#define SOCKET_NAME "scrcpy" #define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #ifdef OVERRIDE_SERVER_PATH
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME # define DEFAULT_SERVER_PATH OVERRIDE_SERVER_PATH
#else
# define DEFAULT_SERVER_PATH PREFIX PREFIXED_SERVER_PATH
#endif
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static const char * static const char *
get_server_path(void) { get_server_path(void) {
const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); const char *server_path = 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;
#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) { if (!server_path) {
LOGE("Could not alloc server path string, " server_path = DEFAULT_SERVER_PATH;
"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; return server_path;
#endif
} }
static bool static bool
@@ -115,25 +79,27 @@ disable_tunnel(struct server *server) {
} }
static process_t static process_t
execute_server(struct server *server, const struct server_params *params) { execute_server(const char *serial,
uint16_t max_size, uint32_t bit_rate,
bool tunnel_forward, const char *crop,
bool send_frame_meta) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, bit_rate);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME, "CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"app_process", "app_process",
"/", // unused "/", // unused
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
server->tunnel_forward ? "true" : "false", tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-", crop ? crop : "-",
"true", // always send frame meta (packet boundaries + timestamp) send_frame_meta ? "true" : "false",
params->control ? "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 #define IPV4_LOCALHOST 0x7F000001
@@ -153,9 +119,8 @@ connect_and_read_byte(uint16_t port) {
char byte; char byte;
// the connection may succeed even if the server behind the "adb tunnel" // the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection // 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 // the server is not listening yet behind the adb tunnel
net_close(socket);
return INVALID_SOCKET; return INVALID_SOCKET;
} }
return socket; return socket;
@@ -182,7 +147,7 @@ close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET); SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR); net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) { if (!net_close(*socket)) {
LOGW("Could not close socket"); LOGW("Cannot close socket");
return; return;
} }
*socket = INVALID_SOCKET; *socket = INVALID_SOCKET;
@@ -195,8 +160,9 @@ server_init(struct server *server) {
bool bool
server_start(struct server *server, const char *serial, server_start(struct server *server, const char *serial,
const struct server_params *params) { uint16_t local_port, uint16_t max_size, uint32_t bit_rate,
server->local_port = params->local_port; const char *crop, bool send_frame_meta) {
server->local_port = local_port;
if (serial) { if (serial) {
server->serial = SDL_strdup(serial); server->serial = SDL_strdup(serial);
@@ -225,9 +191,9 @@ server_start(struct server *server, const char *serial,
// need to try to connect until the server socket is listening on the // need to try to connect until the server socket is listening on the
// device. // device.
server->server_socket = listen_on_port(params->local_port); server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) { if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, params->local_port); LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server); disable_tunnel(server);
SDL_free(server->serial); SDL_free(server->serial);
return false; return false;
@@ -235,14 +201,16 @@ server_start(struct server *server, const char *serial,
} }
// server will connect to our server socket // 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->process == PROCESS_NONE) {
if (!server->tunnel_forward) { if (!server->tunnel_forward) {
close_socket(&server->server_socket); close_socket(&server->server_socket);
} }
disable_tunnel(server); disable_tunnel(server);
SDL_free(server->serial); SDL_free((void *) server->serial);
return false; return false;
} }
@@ -251,62 +219,39 @@ server_start(struct server *server, const char *serial,
return true; return true;
} }
bool socket_t
server_connect_to(struct server *server) { server_connect_to(struct server *server) {
if (!server->tunnel_forward) { if (!server->tunnel_forward) {
server->video_socket = net_accept(server->server_socket); server->device_socket = net_accept(server->server_socket);
if (server->video_socket == INVALID_SOCKET) {
return false;
}
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;
}
// we don't need the server socket anymore
close_socket(&server->server_socket);
} else { } else {
uint32_t attempts = 100; uint32_t attempts = 100;
uint32_t delay = 100; // ms uint32_t delay = 100; // ms
server->video_socket = server->device_socket = connect_to_server(server->local_port, attempts,
connect_to_server(server->local_port, attempts, delay); delay);
if (server->video_socket == INVALID_SOCKET) {
return false;
} }
// we know that the device is listening, we don't need several attempts if (server->device_socket == INVALID_SOCKET) {
server->control_socket = return INVALID_SOCKET;
net_connect(IPV4_LOCALHOST, server->local_port);
if (server->control_socket == INVALID_SOCKET) {
return false;
} }
if (!server->tunnel_forward) {
// we don't need the server socket anymore
close_socket(&server->server_socket);
} }
// we don't need the adb tunnel anymore // we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure disable_tunnel(server); // ignore failure
server->tunnel_enabled = false; server->tunnel_enabled = false;
return true; return server->device_socket;
} }
void void
server_stop(struct server *server) { 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);
}
SDL_assert(server->process != PROCESS_NONE); SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) { if (!cmd_terminate(server->process)) {
LOGW("Could not terminate server"); LOGW("Cannot terminate server");
} }
cmd_simple_wait(server->process, NULL); // ignore exit code cmd_simple_wait(server->process, NULL); // ignore exit code
@@ -320,5 +265,11 @@ server_stop(struct server *server) {
void void
server_destroy(struct server *server) { 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(server->serial); SDL_free(server->serial);
} }

View File

@@ -11,32 +11,24 @@ struct server {
char *serial; char *serial;
process_t process; process_t process;
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket; socket_t device_socket;
socket_t control_socket;
uint16_t local_port; uint16_t local_port;
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
bool send_frame_meta; // request frame PTS to be able to record properly
}; };
#define SERVER_INITIALIZER { \ #define SERVER_INITIALIZER { \
.serial = NULL, \ .serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \ .device_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
.local_port = 0, \ .local_port = 0, \
.tunnel_enabled = false, \ .tunnel_enabled = false, \
.tunnel_forward = false, \ .tunnel_forward = false, \
.send_frame_meta = false, \
} }
struct server_params {
const char *crop;
uint16_t local_port;
uint16_t max_size;
uint32_t bit_rate;
bool control;
};
// init default values // init default values
void void
server_init(struct server *server); server_init(struct server *server);
@@ -44,10 +36,11 @@ server_init(struct server *server);
// push, enable tunnel et start the server // push, enable tunnel et start the server
bool bool
server_start(struct server *server, const char *serial, server_start(struct server *server, const char *serial,
const struct server_params *params); uint16_t local_port, uint16_t max_size, uint32_t bit_rate,
const char *crop, bool send_frame_meta);
// block until the communication with the server is established // block until the communication with the server is established
bool socket_t
server_connect_to(struct server *server); server_connect_to(struct server *server);
// disconnect and kill the server process // disconnect and kill the server process

View File

@@ -8,8 +8,6 @@
# include <tchar.h> # include <tchar.h>
#endif #endif
#include <SDL2/SDL_stdinc.h>
size_t size_t
xstrncpy(char *dest, const char *src, size_t n) { xstrncpy(char *dest, const char *src, size_t n) {
size_t i; size_t i;
@@ -47,7 +45,7 @@ truncated:
char * char *
strquote(const char *src) { strquote(const char *src) {
size_t len = strlen(src); size_t len = strlen(src);
char *quoted = SDL_malloc(len + 3); char *quoted = malloc(len + 3);
if (!quoted) { if (!quoted) {
return NULL; return NULL;
} }
@@ -58,22 +56,6 @@ strquote(const char *src) {
return quoted; 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 #ifdef _WIN32
wchar_t * wchar_t *
@@ -83,7 +65,7 @@ utf8_to_wide_char(const char *utf8) {
return NULL; return NULL;
} }
wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); wchar_t *wide = malloc(len * sizeof(wchar_t));
if (!wide) { if (!wide) {
return NULL; return NULL;
} }
@@ -92,20 +74,4 @@ utf8_to_wide_char(const char *utf8) {
return wide; 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 #endif

View File

@@ -23,18 +23,11 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
char * char *
strquote(const char *src); 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 #ifdef _WIN32
// convert a UTF-8 string to a wchar_t string // convert a UTF-8 string to a wchar_t string
// returns the new allocated string, to be freed by the caller // returns the new allocated string, to be freed by the caller
wchar_t * wchar_t *
utf8_to_wide_char(const char *utf8); utf8_to_wide_char(const char *utf8);
char *
utf8_from_wide_char(const wchar_t *s);
#endif #endif
#endif #endif

View File

@@ -22,8 +22,54 @@
#define HEADER_SIZE 12 #define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1) #define NO_PTS UINT64_C(-1)
static struct frame_meta *
frame_meta_new(uint64_t pts) {
struct frame_meta *meta = malloc(sizeof(*meta));
if (!meta) {
return meta;
}
meta->pts = pts;
meta->next = NULL;
return meta;
}
static void
frame_meta_delete(struct frame_meta *frame_meta) {
free(frame_meta);
}
static bool static bool
stream_recv_packet(struct stream *stream, AVPacket *packet) { receiver_state_push_meta(struct receiver_state *state, uint64_t pts) {
struct frame_meta *frame_meta = frame_meta_new(pts);
if (!frame_meta) {
return false;
}
// append to the list
// (iterate to find the last item, in practice the list should be tiny)
struct frame_meta **p = &state->frame_meta_queue;
while (*p) {
p = &(*p)->next;
}
*p = frame_meta;
return true;
}
static uint64_t
receiver_state_take_meta(struct receiver_state *state) {
struct frame_meta *frame_meta = state->frame_meta_queue; // first item
SDL_assert(frame_meta); // must not be empty
uint64_t pts = frame_meta->pts;
state->frame_meta_queue = frame_meta->next; // remove the item
frame_meta_delete(frame_meta);
return pts;
}
static int
read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
struct receiver_state *state = &stream->receiver_state;
// The video stream contains raw packets, without time information. When we // The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header // record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet. // added by the server before each raw packet.
@@ -36,30 +82,60 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
// //
// It is followed by <packet_size> bytes containing the packet/frame. // It is followed by <packet_size> bytes containing the packet/frame.
if (!state->remaining) {
#define HEADER_SIZE 12
uint8_t header[HEADER_SIZE]; uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r < HEADER_SIZE) { if (r == -1) {
return false; 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); uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]); state->remaining = buffer_read32be(&header[8]);
SDL_assert(len);
if (av_new_packet(packet, len)) { if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
LOGE("Could not allocate packet"); LOGE("Could not store PTS for recording");
return false; // we cannot save the PTS, the recording would be broken
return AVERROR(ENOMEM);
}
} }
r = net_recv_all(stream->socket, packet->data, len); SDL_assert(state->remaining);
if (r < len) {
av_packet_unref(packet); if (buf_size > state->remaining) {
return false; buf_size = state->remaining;
} }
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE; ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return true; SDL_assert(state->remaining >= r);
state->remaining -= r;
return r;
}
static int
read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
} }
static void static void
@@ -69,199 +145,109 @@ notify_stopped(void) {
SDL_PushEvent(&stop_event); 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 static int
run_stream(void *data) { run_stream(void *data) {
struct stream *stream = data; struct stream *stream = data;
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto end;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto finally_free_format_ctx;
}
// initialize the receiver state
stream->receiver_state.frame_meta_queue = NULL;
stream->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets
int (*read_packet)(void *, uint8_t *, int) =
stream->recorder ? read_packet_with_meta : read_raw_packet;
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream,
read_packet, NULL, NULL);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto finally_free_format_ctx;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
goto finally_free_avio_ctx;
}
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) { if (!codec) {
LOGE("H.264 decoder not found"); LOGE("H.264 decoder not found");
goto end; 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)) { if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder"); LOGE("Could not open decoder");
goto finally_free_codec_ctx; goto finally_close_input;
}
if (stream->recorder && !recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_input;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet)) {
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
av_packet_unref(&packet);
goto quit;
} }
if (stream->recorder) { if (stream->recorder) {
if (!recorder_open(stream->recorder, codec)) { // we retrieve the PTS in order they were received, so they will
LOGE("Could not open recorder"); // be assigned to the correct frame
goto finally_close_decoder; uint64_t pts = receiver_state_take_meta(&stream->receiver_state);
} packet.pts = pts;
packet.dts = pts;
if (!recorder_start(stream->recorder)) { // no need to rescale with av_packet_rescale_ts(), the timestamps
LOGE("Could not start recorder"); // are in microseconds both in input and output
goto finally_close_recorder; if (!recorder_write(stream->recorder, &packet)) {
} LOGE("Could not write frame to output file");
}
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); av_packet_unref(&packet);
if (!ok) { goto quit;
// cannot process packet (error already logged) }
}
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
break; break;
} }
} }
LOGD("End of frames"); LOGD("End of frames");
if (stream->has_pending) { quit:
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) { if (stream->recorder) {
recorder_close(stream->recorder); recorder_close(stream->recorder);
} }
finally_close_decoder: finally_close_input:
if (stream->decoder) { avformat_close_input(&format_ctx);
decoder_close(stream->decoder); finally_free_avio_ctx:
} av_free(avio_ctx->buffer);
finally_free_codec_ctx: av_free(avio_ctx);
avcodec_free_context(&stream->codec_ctx); finally_free_format_ctx:
avformat_free_context(format_ctx);
end: end:
notify_stopped(); notify_stopped();
return 0; return 0;
@@ -273,7 +259,6 @@ stream_init(struct stream *stream, socket_t socket,
stream->socket = socket; stream->socket = socket;
stream->decoder = decoder, stream->decoder = decoder,
stream->recorder = recorder; stream->recorder = recorder;
stream->has_pending = false;
} }
bool bool

View File

@@ -3,26 +3,28 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "net.h" #include "net.h"
struct video_buffer; struct video_buffer;
struct frame_meta {
uint64_t pts;
struct frame_meta *next;
};
struct stream { struct stream {
socket_t socket; socket_t socket;
struct video_buffer *video_buffer; struct video_buffer *video_buffer;
SDL_Thread *thread; SDL_Thread *thread;
struct decoder *decoder; struct decoder *decoder;
struct recorder *recorder; struct recorder *recorder;
AVCodecContext *codec_ctx; struct receiver_state {
AVCodecParserContext *parser; // meta (in order) for frames not consumed yet
// successive packets may need to be concatenated, until a non-config struct frame_meta *frame_meta_queue;
// packet is available size_t remaining; // remaining bytes to receive for the current frame
bool has_pending; } receiver_state;
AVPacket pending;
}; };
void void

View File

@@ -1,15 +1,9 @@
// for portability
#define _POSIX_SOURCE // for kill() #define _POSIX_SOURCE // for kill()
#define _BSD_SOURCE // for readlink()
// modern glibc will complain without this
#define _DEFAULT_SOURCE
#include "command.h" #include "command.h"
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <limits.h>
#include <signal.h> #include <signal.h>
#include <stdlib.h> #include <stdlib.h>
#include <sys/types.h> #include <sys/types.h>
@@ -94,7 +88,7 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
int status; int status;
int code; int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
// could not wait, or exited unexpectedly, probably by a signal // cannot wait, or exited unexpectedly, probably by a signal
code = -1; code = -1;
} else { } else {
code = WEXITSTATUS(status); code = WEXITSTATUS(status);
@@ -104,23 +98,3 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
} }
return !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

@@ -33,7 +33,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
wchar_t *wide = utf8_to_wide_char(cmd); wchar_t *wide = utf8_to_wide_char(cmd);
if (!wide) { if (!wide) {
LOGC("Could not allocate wide char string"); LOGC("Cannot allocate wide char string");
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
@@ -44,7 +44,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
#endif #endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) { &pi)) {
SDL_free(wide); free(wide);
*handle = NULL; *handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) { if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY; return PROCESS_ERROR_MISSING_BINARY;
@@ -52,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
SDL_free(wide); free(wide);
*handle = pi.hProcess; *handle = pi.hProcess;
return PROCESS_SUCCESS; return PROCESS_SUCCESS;
} }
@@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code; DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) { || !GetExitCodeProcess(handle, &code)) {
// could not wait or retrieve the exit code // cannot wait or retrieve the exit code
code = -1; // max value, it's unsigned code = -1; // max value, it's unsigned
} }
if (exit_code) { if (exit_code) {
@@ -75,18 +75,3 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
} }
return !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

@@ -105,10 +105,6 @@ read_xpm(char *xpm[]) {
width, height, width, height,
32, 4 * width, 32, 4 * width,
rmask, gmask, bmask, amask); rmask, gmask, bmask, amask);
if (!surface) {
LOGE("Could not create icon surface");
return NULL;
}
// make the surface own the raw pixels // make the surface own the raw pixels
surface->flags &= ~SDL_PREALLOC; surface->flags &= ~SDL_PREALLOC;
return surface; return surface;

View File

@@ -10,10 +10,7 @@
#include "log.h" #include "log.h"
bool bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, video_buffer_init(struct video_buffer *vb) {
bool render_expired_frames) {
vb->fps_counter = fps_counter;
if (!(vb->decoding_frame = av_frame_alloc())) { if (!(vb->decoding_frame = av_frame_alloc())) {
goto error_0; goto error_0;
} }
@@ -26,20 +23,18 @@ video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
goto error_2; goto error_2;
} }
vb->render_expired_frames = render_expired_frames; #ifndef SKIP_FRAMES
if (render_expired_frames) {
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(vb->mutex); SDL_DestroyMutex(vb->mutex);
goto error_2; goto error_2;
} }
// interrupted is not used if expired frames are not rendered
// since offering a frame will never block
vb->interrupted = false; vb->interrupted = false;
} #endif
// there is initially no rendering frame, so consider it has already been // there is initially no rendering frame, so consider it has already been
// consumed // consumed
vb->rendering_frame_consumed = true; vb->rendering_frame_consumed = true;
fps_counter_init(&vb->fps_counter);
return true; return true;
@@ -53,9 +48,9 @@ error_0:
void void
video_buffer_destroy(struct video_buffer *vb) { video_buffer_destroy(struct video_buffer *vb) {
if (vb->render_expired_frames) { #ifndef SKIP_FRAMES
SDL_DestroyCond(vb->rendering_frame_consumed_cond); SDL_DestroyCond(vb->rendering_frame_consumed_cond);
} #endif
SDL_DestroyMutex(vb->mutex); SDL_DestroyMutex(vb->mutex);
av_frame_free(&vb->rendering_frame); av_frame_free(&vb->rendering_frame);
av_frame_free(&vb->decoding_frame); av_frame_free(&vb->decoding_frame);
@@ -72,14 +67,17 @@ void
video_buffer_offer_decoded_frame(struct video_buffer *vb, video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped) { bool *previous_frame_skipped) {
mutex_lock(vb->mutex); mutex_lock(vb->mutex);
if (vb->render_expired_frames) { #ifndef SKIP_FRAMES
// wait for the current (expired) frame to be consumed // if SKIP_FRAMES is disabled, then the decoder must wait for the current
// frame to be consumed
while (!vb->rendering_frame_consumed && !vb->interrupted) { while (!vb->rendering_frame_consumed && !vb->interrupted) {
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); cond_wait(vb->rendering_frame_consumed_cond, vb->mutex);
} }
} else if (!vb->rendering_frame_consumed) { #else
fps_counter_add_skipped_frame(vb->fps_counter); if (vb->fps_counter.started && !vb->rendering_frame_consumed) {
fps_counter_add_skipped_frame(&vb->fps_counter);
} }
#endif
video_buffer_swap_frames(vb); video_buffer_swap_frames(vb);
@@ -93,21 +91,26 @@ const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) { video_buffer_consume_rendered_frame(struct video_buffer *vb) {
SDL_assert(!vb->rendering_frame_consumed); SDL_assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true; vb->rendering_frame_consumed = true;
fps_counter_add_rendered_frame(vb->fps_counter); if (vb->fps_counter.started) {
if (vb->render_expired_frames) { fps_counter_add_rendered_frame(&vb->fps_counter);
// unblock video_buffer_offer_decoded_frame()
cond_signal(vb->rendering_frame_consumed_cond);
} }
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then notify the decoder the current frame is
// consumed, so that it may push a new one
cond_signal(vb->rendering_frame_consumed_cond);
#endif
return vb->rendering_frame; return vb->rendering_frame;
} }
void void
video_buffer_interrupt(struct video_buffer *vb) { video_buffer_interrupt(struct video_buffer *vb) {
if (vb->render_expired_frames) { #ifdef SKIP_FRAMES
(void) vb; // unused
#else
mutex_lock(vb->mutex); mutex_lock(vb->mutex);
vb->interrupted = true; vb->interrupted = true;
mutex_unlock(vb->mutex); mutex_unlock(vb->mutex);
// wake up blocking wait // wake up blocking wait
cond_signal(vb->rendering_frame_consumed_cond); cond_signal(vb->rendering_frame_consumed_cond);
} #endif
} }

View File

@@ -4,6 +4,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL_mutex.h> #include <SDL2/SDL_mutex.h>
#include "config.h"
#include "fps_counter.h" #include "fps_counter.h"
// forward declarations // forward declarations
@@ -13,16 +14,16 @@ struct video_buffer {
AVFrame *decoding_frame; AVFrame *decoding_frame;
AVFrame *rendering_frame; AVFrame *rendering_frame;
SDL_mutex *mutex; SDL_mutex *mutex;
bool render_expired_frames; #ifndef SKIP_FRAMES
bool interrupted; bool interrupted;
SDL_cond *rendering_frame_consumed_cond; SDL_cond *rendering_frame_consumed_cond;
#endif
bool rendering_frame_consumed; bool rendering_frame_consumed;
struct fps_counter *fps_counter; struct fps_counter fps_counter;
}; };
bool bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, video_buffer_init(struct video_buffer *vb);
bool render_expired_frames);
void void
video_buffer_destroy(struct video_buffer *vb); video_buffer_destroy(struct video_buffer *vb);

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 == 18);
const unsigned char expected[] = {
0x02, // CONTROL_EVENT_TYPE_MOUSE
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_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 == 21);
const unsigned char expected[] = {
0x03, // CONTROL_EVENT_TYPE_SCROLL
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)));
}
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

@@ -1,38 +0,0 @@
#include <assert.h>
#include <queue.h>
struct foo {
int value;
struct foo *next;
};
static void test_queue(void) {
struct my_queue QUEUE(struct foo) queue;
queue_init(&queue);
assert(queue_is_empty(&queue));
struct foo v1 = { .value = 42 };
struct foo v2 = { .value = 27 };
queue_push(&queue, next, &v1);
queue_push(&queue, next, &v2);
struct foo *foo;
assert(!queue_is_empty(&queue));
queue_take(&queue, next, &foo);
assert(foo->value == 42);
assert(!queue_is_empty(&queue));
queue_take(&queue, next, &foo);
assert(foo->value == 27);
assert(queue_is_empty(&queue));
}
int main(void) {
test_queue();
return 0;
}

View File

@@ -126,37 +126,6 @@ static void test_xstrjoin_truncated_after_sep(void) {
assert(!strcmp("abc de ", s)); 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) { int main(void) {
test_xstrncpy_simple(); test_xstrncpy_simple();
test_xstrncpy_just_fit(); test_xstrncpy_just_fit();
@@ -166,6 +135,5 @@ int main(void) {
test_xstrjoin_truncated_in_token(); test_xstrjoin_truncated_in_token();
test_xstrjoin_truncated_before_sep(); test_xstrjoin_truncated_before_sep();
test_xstrjoin_truncated_after_sep(); test_xstrjoin_truncated_after_sep();
test_utf8_truncate();
return 0; return 0;
} }

View File

@@ -7,7 +7,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

View File

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

View File

@@ -15,6 +15,6 @@ cpu = 'x86_64'
endian = 'little' endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32' 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

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

View File

@@ -1,7 +1,8 @@
option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('build_app', type: 'boolean', value: true, description: 'Build the client')
option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('build_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('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('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('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') 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-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32: prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1-win32-shared.zip \
8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \ e692b18c01745d262c03294b382fd64df68fabe3c66aa4546a3ad3935175cde3 \
ffmpeg-4.1.3-win32-shared ffmpeg-4.1-win32-shared
prepare-ffmpeg-dev-win32: prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1-win32-dev.zip \
e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \ 34bc5e471fb9160609abd6bc271e361050f3ff7376b1b8a0873cca02b38277c8 \
ffmpeg-4.1.3-win32-dev ffmpeg-4.1-win32-dev
prepare-ffmpeg-shared-win64: prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1-win64-shared.zip \
0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \ c4908c97436c946509dc365e421159274fa4b1e66dce6fb5b63d82a6294d5357 \
ffmpeg-4.1.3-win64-shared ffmpeg-4.1-win64-shared
prepare-ffmpeg-dev-win64: prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1-win64-dev.zip \
334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \ 761ec79aa3dae66698c9791a2f0bb9da8794246f8356cadc741ddc0eabab0471 \
ffmpeg-4.1.3-win64-dev ffmpeg-4.1-win64-dev
prepare-sdl2: prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz \
ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \ 0f9f00d0f2a9a95dfb5cce929718210c3f85432cc2e9d4abade4adcb7f6bb39d \
SDL2-2.0.8 SDL2-2.0.9
prepare-adb: prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \
2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \ db78f726d5dc653706dcd15a462ab1b946c643f598df76906c4c1858411c54df \
platform-tools platform-tools

View File

@@ -1,22 +1,13 @@
#!/bin/bash #!/bin/bash
set -e set -e
# test locally # build and 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 BUILDDIR=build_release
rm -rf "$BUILDDIR" rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
cd "$BUILDDIR" cd "$BUILDDIR"
ninja ninja
ninja test
cd - cd -
# build Windows releases # build Windows releases

View File

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

View File

@@ -4,8 +4,9 @@ prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == '' if prebuilt_server == ''
custom_target('scrcpy-server', custom_target('scrcpy-server',
build_always: true, # gradle is responsible for tracking source changes build_always: true, # gradle is responsible for tracking source changes
input: '.',
output: 'scrcpy-server.jar', 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: true,
install_dir: 'share/scrcpy') install_dir: 'share/scrcpy')
else else

View File

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

View File

@@ -1,20 +1,16 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import android.view.InputEvent; import android.view.InputEvent;
public final class Device { 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 { public interface RotationListener {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
} }
@@ -111,8 +107,8 @@ public final class Device {
} }
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint(); Point point = position.getPoint();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); int scaledX = contentRect.left + point.x * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); int scaledY = contentRect.top + point.y * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY); return new Point(scaledX, scaledY);
} }
@@ -144,28 +140,6 @@ public final class Device {
serviceManager.getStatusBarManager().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) { static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right); 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

@@ -1,7 +1,9 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Point;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.InputEvent; import android.view.InputEvent;
@@ -11,11 +13,11 @@ import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
public class Controller {
public class EventController {
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@@ -23,11 +25,10 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
public Controller(Device device, DesktopConnection connection) { public EventController(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
initPointer(); initPointer();
sender = new DeviceMessageSender(connection);
} }
private void initPointer() { private void initPointer() {
@@ -43,8 +44,8 @@ public class Controller {
private void setPointerCoords(Point point) { private void setPointerCoords(Point point) {
MotionEvent.PointerCoords coords = pointerCoords[0]; MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX(); coords.x = point.x;
coords.y = point.getY(); coords.y = point.y;
} }
private void setScroll(int hScroll, int vScroll) { private void setScroll(int hScroll, int vScroll) {
@@ -53,64 +54,32 @@ public class Controller {
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
} }
@SuppressWarnings("checkstyle:MagicNumber")
public void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, turn screen on
if (!device.isScreenOn()) { turnScreenOn();
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);
}
while (true) { while (true) {
handleEvent(); handleEvent();
} }
} }
public DeviceMessageSender getSender() {
return sender;
}
private void handleEvent() throws IOException { private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage(); ControlEvent controlEvent = connection.receiveControlEvent();
switch (msg.getType()) { switch (controlEvent.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlEvent.TYPE_KEYCODE:
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState());
break; break;
case ControlMessage.TYPE_INJECT_TEXT: case ControlEvent.TYPE_TEXT:
injectText(msg.getText()); injectText(controlEvent.getText());
break; break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT: case ControlEvent.TYPE_MOUSE:
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition()); injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition());
break; break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlEvent.TYPE_SCROLL:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll());
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlEvent.TYPE_COMMAND:
pressBackOrTurnScreenOn(); executeCommand(controlEvent.getAction());
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());
break; break;
default: default:
// do nothing // do nothing
@@ -136,16 +105,13 @@ public class Controller {
return true; return true;
} }
private int injectText(String text) { private boolean injectText(String text) {
int successCount = 0;
for (char c : text.toCharArray()) { for (char c : text.toCharArray()) {
if (!injectChar(c)) { if (!injectChar(c)) {
Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); return false;
continue;
} }
successCount++;
} }
return successCount; return true;
} }
private boolean injectMouse(int action, int buttons, Position position) { private boolean injectMouse(int action, int buttons, Position position) {
@@ -194,8 +160,28 @@ public class Controller {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} }
private boolean turnScreenOn() {
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
}
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode); return injectKeycode(keycode);
} }
private boolean executeCommand(int action) {
switch (action) {
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
return pressBackOrTurnScreenOn();
case ControlEvent.COMMAND_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
return true;
case ControlEvent.COMMAND_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
return true;
default:
Ln.w("Unsupported command: " + action);
}
return false;
}
} }

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ public class Options {
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean sendFrameMeta; // send PTS so that the client may record properly
private boolean control;
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
@@ -49,12 +48,4 @@ public class Options {
public void setSendFrameMeta(boolean sendFrameMeta) { public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = 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; package com.genymobile.scrcpy;
import android.graphics.Point;
import java.util.Objects; import java.util.Objects;
public class Position { public class Position {

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.MediaMuxer;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
@@ -56,6 +57,8 @@ public class ScreenEncoder implements Device.RotationListener {
public void streamScreen(Device device, FileDescriptor fd) throws IOException { public void streamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval); MediaFormat format = createFormat(bitRate, frameRate, iFrameInterval);
device.setRotationListener(this); device.setRotationListener(this);
IBinder d = SurfaceControl.getBuiltInDisplay(0);
SurfaceControl.setDisplayPowerMode(d, SurfaceControl.POWER_MODE_OFF);
boolean alive; boolean alive;
try { try {
do { do {
@@ -70,9 +73,8 @@ public class ScreenEncoder implements Device.RotationListener {
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} finally { } finally {
codec.stop();
destroyDisplay(display); destroyDisplay(display);
codec.release(); codec.release();
surface.release(); surface.release();

View File

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

@@ -15,7 +15,6 @@ public final class ServiceManager {
private InputManager inputManager; private InputManager inputManager;
private PowerManager powerManager; private PowerManager powerManager;
private StatusBarManager statusBarManager; private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
public ServiceManager() { public ServiceManager() {
try { try {
@@ -69,11 +68,4 @@ public final class ServiceManager {
} }
return statusBarManager; return statusBarManager;
} }
public ClipboardManager getClipboardManager() {
if (clipboardManager == null) {
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
}
return clipboardManager;
}
} }

View File

@@ -1,8 +1,8 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln; import android.annotation.SuppressLint;
import android.os.IInterface; import android.os.IInterface;
import android.view.InputEvent;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@@ -10,42 +10,32 @@ import java.lang.reflect.Method;
public class StatusBarManager { public class StatusBarManager {
private final IInterface manager; private final IInterface manager;
private Method expandNotificationsPanelMethod; private final Method expandNotificationsPanelMethod;
private Method collapsePanelsMethod; private final Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) { public StatusBarManager(IInterface manager) {
this.manager = manager; this.manager = manager;
try {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
} }
public void expandNotificationsPanel() { 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 { try {
expandNotificationsPanelMethod.invoke(manager); expandNotificationsPanelMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e); throw new AssertionError(e);
} }
} }
public void collapsePanels() { 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 { try {
collapsePanelsMethod.invoke(manager); collapsePanelsMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) { } catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e); throw new AssertionError(e);
} }
} }
} }

View File

@@ -2,7 +2,6 @@ package com.genymobile.scrcpy.wrappers;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
@@ -11,9 +10,10 @@ public final class SurfaceControl {
private static final Class<?> CLASS; private static final Class<?> CLASS;
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
public static final int POWER_MODE_OFF = 0; public static final int POWER_MODE_OFF = 0;
public static final int POWER_MODE_DOZE = 1;
public static final int POWER_MODE_NORMAL = 2; public static final int POWER_MODE_NORMAL = 2;
public static final int POWER_MODE_DOZE_SUSPEND = 3;
static { static {
try { try {
@@ -78,12 +78,7 @@ public final class SurfaceControl {
public static IBinder getBuiltInDisplay(int builtInDisplayId) { public static IBinder getBuiltInDisplay(int builtInDisplayId) {
try { try {
// the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId); return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
}
return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId);
} catch (Exception e) { } catch (Exception e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@@ -0,0 +1,173 @@
package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlEventReaderTest {
@Test
public void testParseKeycodeEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testParseTextEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
Assert.assertEquals("testé", event.getText());
}
@Test
public void testParseLongTextEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
}
@Test
public void testParseMouseEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testMultiEvents() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlEvent.TYPE_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testPartialEvents() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlEvent.TYPE_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertNull(event); // the event is not complete
bos.reset();
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
// the event is now complete
event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
}

View File

@@ -1,304 +0,0 @@
package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlMessageReaderTest {
@Test
public void testParseKeycodeEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testParseTextEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
Assert.assertEquals("testé", event.getText());
}
@Test
public void testParseLongTextEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = new byte[ControlMessageReader.TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
}
@Test
public void testParseMouseEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
@SuppressWarnings("checkstyle:MagicNumber")
public void testParseScrollEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT);
dos.writeInt(260);
dos.writeInt(1026);
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeInt(1);
dos.writeInt(-1);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType());
Assert.assertEquals(260, event.getPosition().getPoint().getX());
Assert.assertEquals(1026, event.getPosition().getPoint().getY());
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1, event.getHScroll());
Assert.assertEquals(-1, event.getVScroll());
}
@Test
public void testParseBackOrScreenOnEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
}
@Test
public void testParseExpandNotificationPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType());
}
@Test
public void testParseCollapseNotificationPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
}
@Test
public void testParseGetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
}
@Test
public void testParseSetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals("testé", event.getText());
}
@Test
public void testParseSetScreenPowerMode() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE);
dos.writeByte(Device.POWER_MODE_NORMAL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType());
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
}
@Test
public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testPartialEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertNull(event); // the event is not complete
bos.reset();
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(KeyEvent.META_CTRL_ON);
packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
// the event is now complete
event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
}

View File

@@ -1,35 +0,0 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriterTest {
@Test
public void testSerializeClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
String text = "aéûoç";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeShort(data.length);
dos.write(data);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createClipboard(text);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
byte[] actual = bos.toByteArray();
Assert.assertArrayEquals(expected, actual);
}
}

View File

@@ -1,43 +0,0 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class StringUtilsTest {
@Test
@SuppressWarnings("checkstyle:MagicNumber")
public void testUtf8Truncate() {
String s = "aÉbÔc";
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
Assert.assertEquals(7, utf8.length);
int count;
count = StringUtils.getUtf8TruncationIndex(utf8, 1);
Assert.assertEquals(1, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 2);
Assert.assertEquals(1, count); // É is 2 bytes-wide
count = StringUtils.getUtf8TruncationIndex(utf8, 3);
Assert.assertEquals(3, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 4);
Assert.assertEquals(4, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 5);
Assert.assertEquals(4, count); // Ô is 2 bytes-wide
count = StringUtils.getUtf8TruncationIndex(utf8, 6);
Assert.assertEquals(6, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 7);
Assert.assertEquals(7, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 8);
Assert.assertEquals(7, count); // no more chars
}
}