Merge branch 'dev'

This commit is contained in:
Romain Vimont 2019-06-11 22:54:58 +02:00
commit c2df0228a3
83 changed files with 3000 additions and 1668 deletions

View file

@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String...
args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100
[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123
To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@ -65,17 +65,20 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view
[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view
### Threading
The server uses 2 threads:
The server uses 3 threads:
- the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control events_ (typically,
keyboard and mouse events) from the client.
- the **controller** thread, listening for _control messages_ (typically,
keyboard and mouse events) from the client;
- the **receiver** thread (managed by the controller), sending _device messges_
to the clients (currently, it is only used to send the device clipboard
content).
Since the video encoding is typically hardware, there would be no benefit in
encoding and streaming in two different threads.
@ -89,9 +92,9 @@ The video is encoded using the [`MediaCodec`] API. The codec takes its input
from a [surface] associated to the display, and writes the resulting H.264
stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70
[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69
On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced.
@ -105,31 +108,30 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]:
https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection
_Control events_ are received from the client by the [`EventController`] (run in
a separate thread). There are 5 types of input events:
_Control messages_ are received from the client by the [`Controller`] (run in a
separate thread). There are several types of input events:
- keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly),
- mouse motion/click,
- mouse scroll,
- custom command (e.g. to switch the screen on).
- other commands (e.g. to switch the screen on or to copy the clipboard).
All of them may need to inject input events to the system. To do so, they use
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
Some of them need to inject input events to the system. To do so, they use the
_hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66
[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@ -146,8 +148,8 @@ The video stream is decoded by [libav] (FFmpeg).
### Initialization
On startup, in addition to _libav_ and _SDL_ initialization, the client must
push and start the server on the device, and open a socket so that they may
communicate.
push and start the server on the device, and open two sockets (one for the video
stream, one for control) so that they may communicate.
Note that the client-server roles are expressed at the application level:
@ -180,15 +182,18 @@ the connection from the server (see commit [90a46b4]).
### Threading
The client uses 3 threads:
The client uses 4 threads:
- the **main** thread, executing the SDL event loop,
- the **stream** thread, receiving the video and used for decoding and
recording,
- the **controller** thread, sending _control events_ to the server.
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the client.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window).
installation or file push requests (via drag&drop on the main window) or to
print the framerate regularly in the console.
@ -212,10 +217,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
H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/recorder.h
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h
```
+----------+ +----------+
@ -229,20 +234,19 @@ H.264 packet to the output video file.
### Controller
The [controller] is responsible to send _control events_ to the device. It runs
in a separate thread, to avoid I/O on the main thread.
The [controller] is responsible to send _control messages_ to the device. It
runs in a separate thread, to avoid I/O on the main thread.
On SDL event, received on the main thread, the [input manager][inputmanager]
creates appropriate [_control events_][controlevent]. It is responsible to
creates appropriate [_control messages_][controlmsg]. It is responsible to
convert SDL events to Android events (using [convert]). It pushes the _control
events_ to a blocking queue hold by the controller. On its own thread, the
controller takes events from the queue, that it serializes and sends to the
client.
messages_ to a queue hold by the controller. On its own thread, the controller
takes messages from the queue, that it serializes and sends to the client.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h
[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h
[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h
### UI and event loop
@ -253,10 +257,9 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c
[event loop]:
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
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201
[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h
## Hack

4
FAQ.md
View file

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

View file

@ -57,7 +57,7 @@ build-win32: prepare-deps-win32
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
-Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32
@ -68,7 +68,7 @@ build-win32-noconsole: prepare-deps-win32
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar )
-Dportable=true )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64:
@ -81,7 +81,7 @@ build-win64: prepare-deps-win64
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Doverride_server_path=scrcpy-server.jar )
-Dportable=true )
ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64
@ -92,7 +92,7 @@ build-win64-noconsole: prepare-deps-win64
-Dcrossbuild_windows=true \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar )
-Dportable=true )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
@ -100,28 +100,29 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \

View file

@ -256,6 +256,33 @@ scrcpy --no-control
scrcpy -n
```
### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
command-line option:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
### Forward audio
@ -284,10 +311,13 @@ you are interested, see [issue 14].
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) |
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
| click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click²_ |
| power on | _Right-click²_ |
| turn device screen off (keep mirroring)| `Ctrl`+`o` |
| expand notification panel | `Ctrl`+`n` |
| collapse notification panel | `Ctrl`+`Shift`+`n` |
| copy device clipboard to computer | `Ctrl`+`c` |
| paste computer clipboard to device | `Ctrl`+`v` |
| copy computer clipboard to device | `Ctrl`+`Shift+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Double-click on black borders to remove them._
@ -301,8 +331,8 @@ To use a specific _adb_ binary, configure its path in the environment variable
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file (it can be [useful] on
Windows), configure its path in `SCRCPY_SERVER_PATH`.
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

View file

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

View file

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

50
app/src/cbuf.h Normal file
View file

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

View file

@ -69,7 +69,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
"path in the ADB environment variable)");
break;
case PROCESS_SUCCESS:
/* do nothing */
// do nothing
break;
}
}
@ -147,7 +147,7 @@ adb_push(const char *serial, const char *local, const char *remote) {
}
remote = strquote(remote);
if (!remote) {
free((void *) local);
SDL_free((void *) local);
return PROCESS_NONE;
}
#endif
@ -156,8 +156,8 @@ adb_push(const char *serial, const char *local, const char *remote) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) remote);
free((void *) local);
SDL_free((void *) remote);
SDL_free((void *) local);
#endif
return proc;
@ -178,7 +178,7 @@ adb_install(const char *serial, const char *local) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) local);
SDL_free((void *) local);
#endif
return proc;

View file

@ -9,6 +9,7 @@
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h>
# define PATH_SEPARATOR '\\'
# define PRIexitcode "lu"
// <https://stackoverflow.com/a/44383330/1987178>
# ifdef _WIN64
@ -23,6 +24,7 @@
#else
# include <sys/types.h>
# define PATH_SEPARATOR '/'
# define PRIsizet "zu"
# define PRIexitcode "d"
# define PROCESS_NONE -1
@ -74,6 +76,11 @@ adb_install(const char *serial, const char *local);
// convenience function to wait for a successful process execution
// automatically log process errors with the provided process name
bool
process_check_success(process_t process, const char *name);
process_check_success(process_t proc, const char *name);
// return the absolute path of the executable (the scrcpy binary)
// may be NULL on error; to be freed by SDL_free
char *
get_executable_path(void);
#endif

View file

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

View file

@ -1,91 +0,0 @@
#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

86
app/src/control_msg.c Normal file
View file

@ -0,0 +1,86 @@
#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;
}
}

74
app/src/control_msg.h Normal file
View file

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

View file

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

View file

@ -159,19 +159,19 @@ convert_mouse_buttons(uint32_t state) {
bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_KEYCODE;
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->keycode_event.action)) {
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode, mod)) {
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
return false;
}
to->keycode_event.metastate = convert_meta_state(mod);
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
@ -179,17 +179,18 @@ input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
if (!convert_mouse_action(from->type, &to->mouse_event.action)) {
if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) {
return false;
}
to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
to->inject_mouse_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
return true;
}
@ -197,13 +198,13 @@ mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->mouse_event.buttons = convert_mouse_buttons(from->state);
to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_mouse_event.buttons = convert_mouse_buttons(from->state);
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
return true;
}
@ -211,17 +212,17 @@ mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_SCROLL;
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->scroll_event.position = position;
to->inject_scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->scroll_event.hscroll = -mul * from->x;
to->scroll_event.vscroll = mul * from->y;
to->inject_scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y;
return true;
}

View file

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

View file

@ -11,6 +11,6 @@
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
bool
device_read_info(socket_t device_socket, char *name, struct size *frame_size);
device_read_info(socket_t device_socket, char *device_name, struct size *size);
#endif

48
app/src/device_msg.c Normal file
View file

@ -0,0 +1,48 @@
#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);
}
}

32
app/src/device_msg.h Normal file
View file

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

View file

@ -5,21 +5,21 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "cbuf.h"
#include "command.h"
#define REQUEST_QUEUE_SIZE 16
typedef enum {
ACTION_INSTALL_APK,
ACTION_PUSH_FILE,
} file_handler_action_t;
struct request_queue {
struct request *reqs[REQUEST_QUEUE_SIZE];
int tail;
int head;
struct file_handler_request {
file_handler_action_t action;
char *file;
};
struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler {
char *serial;
SDL_Thread *thread;
@ -28,7 +28,7 @@ struct file_handler {
bool stopped;
bool initialized;
process_t current_process;
struct request_queue queue;
struct file_handler_request_queue queue;
};
bool

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,8 @@ struct args {
uint16_t max_size;
uint32_t bit_rate;
bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
};
static void usage(const char *arg0) {
@ -78,10 +80,19 @@ static void usage(const char *arg0) {
" The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n"
"\n"
" --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" -s, --serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
" -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n"
"\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
@ -129,7 +140,10 @@ static void usage(const char *arg0) {
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" turn screen on\n"
" power on\n"
"\n"
" Ctrl+o\n"
" turn device screen off (keep mirroring)\n"
"\n"
" Ctrl+n\n"
" expand notification panel\n"
@ -137,9 +151,15 @@ static void usage(const char *arg0) {
" Ctrl+Shift+n\n"
" collapse notification panel\n"
"\n"
" Ctrl+c\n"
" copy device clipboard to computer\n"
"\n"
" Ctrl+v\n"
" paste computer clipboard to device\n"
"\n"
" Ctrl+Shift+v\n"
" copy computer clipboard to device\n"
"\n"
" Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n"
@ -274,6 +294,8 @@ guess_record_format(const char *filename) {
return 0;
}
#define OPT_RENDER_EXPIRED_FRAMES 1000
static bool
parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
@ -288,13 +310,16 @@ parse_args(struct args *args, int argc, char *argv[]) {
{"port", required_argument, NULL, 'p'},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'},
{"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options,
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) {
switch (c) {
case 'b':
@ -338,6 +363,9 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 's':
args->serial = optarg;
break;
case 'S':
args->turn_screen_off = true;
break;
case 't':
args->show_touches = true;
break;
@ -347,6 +375,9 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 'v':
args->version = true;
break;
case OPT_RENDER_EXPIRED_FRAMES:
args->render_expired_frames = true;
break;
default:
// getopt prints the error message on stderr
return false;
@ -408,6 +439,8 @@ main(int argc, char *argv[]) {
.always_on_top = false,
.no_control = false,
.no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
};
if (!parse_args(&args, argc, argv)) {
return 1;
@ -446,8 +479,10 @@ main(int argc, char *argv[]) {
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
.always_on_top = args.always_on_top,
.no_control = args.no_control,
.no_display = args.no_display,
.control = !args.no_control,
.display = !args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
};
int res = scrcpy(&options) ? 0 : 1;

View file

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

107
app/src/receiver.c Normal file
View file

@ -0,0 +1,107 @@
#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);
}

32
app/src/receiver.h Normal file
View file

@ -0,0 +1,32 @@
#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

@ -20,7 +20,7 @@ struct recorder {
};
bool
recorder_init(struct recorder *recoder, const char *filename,
recorder_init(struct recorder *recorder, const char *filename,
enum recorder_format format, struct size declared_frame_size);
void

View file

@ -29,6 +29,7 @@
static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER;
static struct fps_counter fps_counter;
static struct video_buffer video_buffer;
static struct stream stream;
static struct decoder decoder;
@ -76,6 +77,11 @@ sdl_init_and_configure(bool display) {
}
#endif
// Do not minimize on focus loss
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
LOGW("Could not disable minimize on focus loss");
}
// Do not disable the screensaver when scrcpy is running
SDL_EnableScreenSaver();
@ -132,7 +138,7 @@ handle_event(SDL_Event *event, bool control) {
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &video_buffer)) {
return false;
return EVENT_RESULT_CONTINUE;
}
break;
case SDL_WINDOWEVENT:
@ -266,9 +272,15 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
bool
scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
if (!server_start(&server, options->serial, options->port,
options->max_size, options->bit_rate, options->crop,
record)) {
struct server_params params = {
.crop = options->crop,
.local_port = options->port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.send_frame_meta = record,
.control = options->control,
};
if (!server_start(&server, options->serial, &params)) {
return false;
}
@ -280,21 +292,22 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = false;
}
bool ret = true;
bool ret = false;
bool display = !options->no_display;
bool control = !options->no_control;
bool fps_counter_initialized = false;
bool video_buffer_initialized = false;
bool file_handler_initialized = false;
bool recorder_initialized = false;
bool stream_started = false;
bool controller_initialized = false;
bool controller_started = false;
if (!sdl_init_and_configure(display)) {
ret = false;
goto finally_destroy_server;
if (!sdl_init_and_configure(options->display)) {
goto end;
}
socket_t device_socket = server_connect_to(&server);
if (device_socket == INVALID_SOCKET) {
server_stop(&server);
ret = false;
goto finally_destroy_server;
if (!server_connect_to(&server)) {
goto end;
}
char device_name[DEVICE_NAME_FIELD_LENGTH];
@ -303,24 +316,28 @@ scrcpy(const struct scrcpy_options *options) {
// screenrecord does not send frames when the screen content does not
// change therefore, we transmit the screen size before the video stream,
// to be able to init the window immediately
if (!device_read_info(device_socket, device_name, &frame_size)) {
server_stop(&server);
ret = false;
goto finally_destroy_server;
if (!device_read_info(server.video_socket, device_name, &frame_size)) {
goto end;
}
struct decoder *dec = NULL;
if (display) {
if (!video_buffer_init(&video_buffer)) {
server_stop(&server);
ret = false;
goto finally_destroy_server;
if (options->display) {
if (!fps_counter_init(&fps_counter)) {
goto end;
}
fps_counter_initialized = true;
if (control && !file_handler_init(&file_handler, server.serial)) {
ret = false;
server_stop(&server);
goto finally_destroy_video_buffer;
if (!video_buffer_init(&video_buffer, &fps_counter,
options->render_expired_frames)) {
goto end;
}
video_buffer_initialized = true;
if (options->control) {
if (!file_handler_init(&file_handler, server.serial)) {
goto end;
}
file_handler_initialized = true;
}
decoder_init(&decoder, &video_buffer);
@ -333,42 +350,49 @@ scrcpy(const struct scrcpy_options *options) {
options->record_filename,
options->record_format,
frame_size)) {
ret = false;
server_stop(&server);
goto finally_destroy_file_handler;
goto end;
}
rec = &recorder;
recorder_initialized = true;
}
av_log_set_callback(av_log_callback);
stream_init(&stream, device_socket, dec, rec);
stream_init(&stream, server.video_socket, dec, rec);
// now we consumed the header values, the socket receives the video stream
// start the stream
if (!stream_start(&stream)) {
ret = false;
server_stop(&server);
goto finally_destroy_recorder;
goto end;
}
stream_started = true;
if (display) {
if (control) {
if (!controller_init(&controller, device_socket)) {
ret = false;
goto finally_stop_stream;
if (options->display) {
if (options->control) {
if (!controller_init(&controller, server.control_socket)) {
goto end;
}
controller_initialized = true;
if (!controller_start(&controller)) {
ret = false;
goto finally_destroy_controller;
goto end;
}
controller_started = true;
}
if (!screen_init_rendering(&screen, device_name, frame_size,
options->always_on_top)) {
ret = false;
goto finally_stop_and_join_controller;
goto end;
}
if (options->turn_screen_off) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
if (!controller_push_msg(&controller, &msg)) {
LOGW("Cannot request 'set screen power mode'");
}
}
if (options->fullscreen) {
@ -381,48 +405,67 @@ scrcpy(const struct scrcpy_options *options) {
show_touches_waited = true;
}
ret = event_loop(display, control);
ret = event_loop(options->display, options->control);
LOGD("quit...");
screen_destroy(&screen);
finally_stop_and_join_controller:
if (display && control) {
end:
// stop stream and controller so that they don't continue once their socket
// is shutdown
if (stream_started) {
stream_stop(&stream);
}
if (controller_started) {
controller_stop(&controller);
}
if (file_handler_initialized) {
file_handler_stop(&file_handler);
}
if (fps_counter_initialized) {
fps_counter_interrupt(&fps_counter);
}
// shutdown the sockets and kill the server
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);
}
finally_destroy_controller:
if (display && control) {
if (controller_initialized) {
controller_destroy(&controller);
}
finally_stop_stream:
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) {
if (recorder_initialized) {
recorder_destroy(&recorder);
}
finally_destroy_file_handler:
if (display && control) {
file_handler_stop(&file_handler);
if (file_handler_initialized) {
file_handler_join(&file_handler);
file_handler_destroy(&file_handler);
}
finally_destroy_video_buffer:
if (display) {
if (video_buffer_initialized) {
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 (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial,
false);
proc_show_touches = set_show_touches_enabled(options->serial, false);
wait_show_touches(proc_show_touches);
}

View file

@ -16,8 +16,10 @@ struct scrcpy_options {
bool show_touches;
bool fullscreen;
bool always_on_top;
bool no_control;
bool no_display;
bool control;
bool display;
bool turn_screen_off;
bool render_expired_frames;
};
bool

View file

@ -2,31 +2,67 @@
#include <errno.h>
#include <inttypes.h>
#include <libgen.h>
#include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h"
#include "command.h"
#include "log.h"
#include "net.h"
#define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar"
#ifdef OVERRIDE_SERVER_PATH
# 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"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
static const char *
get_server_path(void) {
const char *server_path = getenv("SCRCPY_SERVER_PATH");
if (!server_path) {
server_path = DEFAULT_SERVER_PATH;
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
if (server_path_env) {
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env);
// if the envvar is set, use it
return server_path_env;
}
#ifndef PORTABLE
LOGD("Using server: " DEFAULT_SERVER_PATH);
// the absolute path is hardcoded
return DEFAULT_SERVER_PATH;
#else
// use scrcpy-server.jar in the same directory as the executable
char *executable_path = get_executable_path();
if (!executable_path) {
LOGE("Cannot get executable path, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return SERVER_FILENAME;
}
char *dir = dirname(executable_path);
size_t dirlen = strlen(dir);
// sizeof(SERVER_FILENAME) gives statically the size including the null byte
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
char *server_path = SDL_malloc(len);
if (!server_path) {
LOGE("Cannot alloc server path string, "
"using " SERVER_FILENAME " from current directory");
SDL_free(executable_path);
return SERVER_FILENAME;
}
memcpy(server_path, dir, dirlen);
server_path[dirlen] = PATH_SEPARATOR;
memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME));
// the final null byte has been copied with SERVER_FILENAME
SDL_free(executable_path);
LOGD("Using server (portable): %s", server_path);
return server_path;
#endif
}
static bool
@ -79,27 +115,25 @@ disable_tunnel(struct server *server) {
}
static process_t
execute_server(const char *serial,
uint16_t max_size, uint32_t bit_rate,
bool tunnel_forward, const char *crop,
bool send_frame_meta) {
execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size);
sprintf(bit_rate_string, "%"PRIu32, bit_rate);
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
"app_process",
"/", // unused
"com.genymobile.scrcpy.Server",
max_size_string,
bit_rate_string,
tunnel_forward ? "true" : "false",
crop ? crop : "-",
send_frame_meta ? "true" : "false",
server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-",
params->send_frame_meta ? "true" : "false",
params->control ? "true" : "false",
};
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
#define IPV4_LOCALHOST 0x7F000001
@ -119,7 +153,7 @@ connect_and_read_byte(uint16_t port) {
char byte;
// the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection
if (net_recv_all(socket, &byte, 1) != 1) {
if (net_recv(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
return INVALID_SOCKET;
}
@ -160,9 +194,8 @@ server_init(struct server *server) {
bool
server_start(struct server *server, const char *serial,
uint16_t local_port, uint16_t max_size, uint32_t bit_rate,
const char *crop, bool send_frame_meta) {
server->local_port = local_port;
const struct server_params *params) {
server->local_port = params->local_port;
if (serial) {
server->serial = SDL_strdup(serial);
@ -191,9 +224,9 @@ server_start(struct server *server, const char *serial,
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(local_port);
server->server_socket = listen_on_port(params->local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
LOGE("Could not listen on port %" PRIu16, params->local_port);
disable_tunnel(server);
SDL_free(server->serial);
return false;
@ -201,9 +234,7 @@ server_start(struct server *server, const char *serial,
}
// server will connect to our server socket
server->process = execute_server(serial, max_size, bit_rate,
server->tunnel_forward, crop,
send_frame_meta);
server->process = execute_server(server, params);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
@ -219,35 +250,58 @@ server_start(struct server *server, const char *serial,
return true;
}
socket_t
bool
server_connect_to(struct server *server) {
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
server->video_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 {
uint32_t attempts = 100;
uint32_t delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts,
delay);
server->video_socket =
connect_to_server(server->local_port, attempts, delay);
if (server->video_socket == INVALID_SOCKET) {
return false;
}
if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET;
// we know that the device is listening, we don't need several attempts
server->control_socket =
net_connect(IPV4_LOCALHOST, server->local_port);
if (server->control_socket == INVALID_SOCKET) {
return false;
}
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
disable_tunnel(server); // ignore failure
server->tunnel_enabled = false;
return server->device_socket;
return true;
}
void
server_stop(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
}
if (server->video_socket != INVALID_SOCKET) {
close_socket(&server->video_socket);
}
if (server->control_socket != INVALID_SOCKET) {
close_socket(&server->control_socket);
}
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
@ -265,11 +319,5 @@ server_stop(struct server *server) {
void
server_destroy(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
}
if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket);
}
SDL_free(server->serial);
}

View file

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

View file

@ -8,6 +8,8 @@
# include <tchar.h>
#endif
#include <SDL2/SDL_stdinc.h>
size_t
xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
@ -45,7 +47,7 @@ truncated:
char *
strquote(const char *src) {
size_t len = strlen(src);
char *quoted = malloc(len + 3);
char *quoted = SDL_malloc(len + 3);
if (!quoted) {
return NULL;
}
@ -56,6 +58,22 @@ strquote(const char *src) {
return quoted;
}
size_t
utf8_truncation_index(const char *utf8, size_t max_len) {
size_t len = strlen(utf8);
if (len <= max_len) {
return len;
}
len = max_len;
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
// the next byte is not the start of a new UTF-8 codepoint
// so if we would cut there, the character would be truncated
len--;
}
return len;
}
#ifdef _WIN32
wchar_t *
@ -65,7 +83,7 @@ utf8_to_wide_char(const char *utf8) {
return NULL;
}
wchar_t *wide = malloc(len * sizeof(wchar_t));
wchar_t *wide = SDL_malloc(len * sizeof(wchar_t));
if (!wide) {
return NULL;
}
@ -74,4 +92,20 @@ utf8_to_wide_char(const char *utf8) {
return wide;
}
char *
utf8_from_wide_char(const wchar_t *ws) {
int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
if (!len) {
return NULL;
}
char *utf8 = SDL_malloc(len);
if (!utf8) {
return NULL;
}
WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL);
return utf8;
}
#endif

View file

@ -23,11 +23,18 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
char *
strquote(const char *src);
// return the index to truncate a UTF-8 string at a valid position
size_t
utf8_truncation_index(const char *utf8, size_t max_len);
#ifdef _WIN32
// convert a UTF-8 string to a wchar_t string
// returns the new allocated string, to be freed by the caller
wchar_t *
utf8_to_wide_char(const char *utf8);
char *
utf8_from_wide_char(const wchar_t *s);
#endif
#endif

View file

@ -24,7 +24,7 @@
static struct frame_meta *
frame_meta_new(uint64_t pts) {
struct frame_meta *meta = malloc(sizeof(*meta));
struct frame_meta *meta = SDL_malloc(sizeof(*meta));
if (!meta) {
return meta;
}
@ -35,7 +35,7 @@ frame_meta_new(uint64_t pts) {
static void
frame_meta_delete(struct frame_meta *frame_meta) {
free(frame_meta);
SDL_free(frame_meta);
}
static bool
@ -113,7 +113,7 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
return errno ? AVERROR(errno) : AVERROR_EOF;
}
if (r == 0) {
return AVERROR_EOF;
@ -130,7 +130,7 @@ 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);
return errno ? AVERROR(errno) : AVERROR_EOF;
}
if (r == 0) {
return AVERROR_EOF;
@ -207,6 +207,13 @@ run_stream(void *data) {
packet.size = 0;
while (!av_read_frame(format_ctx, &packet)) {
if (SDL_AtomicGet(&stream->stopped)) {
// if the stream is stopped, the socket had been shutdown, so the
// last packet is probably corrupted (but not detected as such by
// FFmpeg) and will not be decoded correctly
av_packet_unref(&packet);
goto quit;
}
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
av_packet_unref(&packet);
goto quit;
@ -259,6 +266,7 @@ stream_init(struct stream *stream, socket_t socket,
stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
SDL_AtomicSet(&stream->stopped, 0);
}
bool
@ -275,6 +283,7 @@ stream_start(struct stream *stream) {
void
stream_stop(struct stream *stream) {
SDL_AtomicSet(&stream->stopped, 1);
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}

View file

@ -3,6 +3,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
@ -18,6 +19,7 @@ struct stream {
socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread;
SDL_atomic_t stopped;
struct decoder *decoder;
struct recorder *recorder;
struct receiver_state {

View file

@ -1,9 +1,15 @@
// for portability
#define _POSIX_SOURCE // for kill()
#define _BSD_SOURCE // for readlink()
// modern glibc will complain without this
#define _DEFAULT_SOURCE
#include "command.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
@ -98,3 +104,23 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
}
return !code;
}
char *
get_executable_path(void) {
// <https://stackoverflow.com/a/1024937/1987178>
#ifdef __linux__
char buf[PATH_MAX + 1]; // +1 for the null byte
ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX);
if (len == -1) {
perror("readlink");
return NULL;
}
buf[len] = '\0';
return SDL_strdup(buf);
#else
// in practice, we only need this feature for portable builds, only used on
// Windows, so we don't care implementing it for every platform
// (it's useful to have a working version on Linux for debugging though)
return NULL;
#endif
}

View file

@ -44,7 +44,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
#endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) {
free(wide);
SDL_free(wide);
*handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY;
@ -52,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC;
}
free(wide);
SDL_free(wide);
*handle = pi.hProcess;
return PROCESS_SUCCESS;
}
@ -75,3 +75,18 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
}
return !code;
}
char *
get_executable_path(void) {
HMODULE hModule = GetModuleHandleW(NULL);
if (!hModule) {
return NULL;
}
WCHAR buf[MAX_PATH + 1]; // +1 for the null byte
int len = GetModuleFileNameW(hModule, buf, MAX_PATH);
if (!len) {
return NULL;
}
buf[len] = '\0';
return utf8_from_wide_char(buf);
}

View file

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

View file

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

73
app/tests/test_cbuf.c Normal file
View file

@ -0,0 +1,73 @@
#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

@ -1,95 +0,0 @@
#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

@ -1,142 +0,0 @@
#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 + 1];
memset(text, 'a', sizeof(text));
text[TEXT_MAX_LENGTH] = '\0';
event.text_event.text = text;
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 3 + TEXT_MAX_LENGTH);
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

@ -0,0 +1,248 @@
#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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '1.8',
version: '1.9',
meson_version: '>= 0.37',
default_options: 'c_std=c11')

View file

@ -3,6 +3,6 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('override_server_path', type: 'string', description: 'Hardcoded path to find the server at runtime')
option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')

View file

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

View file

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

View file

@ -4,9 +4,8 @@ prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == ''
custom_target('scrcpy-server',
build_always: true, # gradle is responsible for tracking source changes
input: '.',
output: 'scrcpy-server.jar',
command: [find_program('./scripts/build-wrapper.sh'), '@INPUT@', '@OUTPUT@', get_option('buildtype')],
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
install: true,
install_dir: 'share/scrcpy')
else

View file

@ -1,107 +0,0 @@
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

@ -1,151 +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 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

@ -0,0 +1,124 @@
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

@ -0,0 +1,176 @@
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

@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.graphics.Point;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.InputEvent;
@ -12,11 +11,11 @@ import android.view.MotionEvent;
import java.io.IOException;
public class EventController {
public class Controller {
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@ -24,10 +23,11 @@ public class EventController {
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
public EventController(Device device, DesktopConnection connection) {
public Controller(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
initPointer();
sender = new DeviceMessageSender(connection);
}
private void initPointer() {
@ -43,8 +43,8 @@ public class EventController {
private void setPointerCoords(Point point) {
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.x;
coords.y = point.y;
coords.x = point.getX();
coords.y = point.getY();
}
private void setScroll(int hScroll, int vScroll) {
@ -53,32 +53,64 @@ public class EventController {
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
}
@SuppressWarnings("checkstyle:MagicNumber")
public void control() throws IOException {
// on start, turn screen on
turnScreenOn();
// on start, power on the device
if (!device.isScreenOn()) {
injectKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on.
// Adding this delay prevents to handle the message before the device is actually
// powered on.
SystemClock.sleep(500);
}
while (true) {
handleEvent();
}
}
public DeviceMessageSender getSender() {
return sender;
}
private void handleEvent() throws IOException {
ControlEvent controlEvent = connection.receiveControlEvent();
switch (controlEvent.getType()) {
case ControlEvent.TYPE_KEYCODE:
injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState());
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
break;
case ControlEvent.TYPE_TEXT:
injectText(controlEvent.getText());
case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText());
break;
case ControlEvent.TYPE_MOUSE:
injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition());
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
break;
case ControlEvent.TYPE_SCROLL:
injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll());
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
break;
case ControlEvent.TYPE_COMMAND:
executeCommand(controlEvent.getAction());
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
pressBackOrTurnScreenOn();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
sender.pushClipboardText(clipboardText);
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText());
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
break;
default:
// do nothing
@ -104,13 +136,16 @@ public class EventController {
return true;
}
private boolean injectText(String text) {
private int injectText(String text) {
int successCount = 0;
for (char c : text.toCharArray()) {
if (!injectChar(c)) {
return false;
Ln.w("Could not inject char u+" + String.format("%04x", (int) c));
continue;
}
successCount++;
}
return true;
return successCount;
}
private boolean injectMouse(int action, int buttons, Position position) {
@ -159,28 +194,8 @@ public class EventController {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
private boolean turnScreenOn() {
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
}
private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode);
}
private boolean executeCommand(int action) {
switch (action) {
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
return pressBackOrTurnScreenOn();
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,6 +8,7 @@ import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public final class DesktopConnection implements Closeable {
@ -16,16 +17,22 @@ public final class DesktopConnection implements Closeable {
private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket socket;
private final InputStream inputStream;
private final FileDescriptor fd;
private final LocalSocket videoSocket;
private final FileDescriptor videoFd;
private final ControlEventReader reader = new ControlEventReader();
private final LocalSocket controlSocket;
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
private DesktopConnection(LocalSocket socket) throws IOException {
this.socket = socket;
inputStream = socket.getInputStream();
fd = socket.getFileDescriptor();
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
videoFd = videoSocket.getFileDescriptor();
}
private static LocalSocket connect(String abstractName) throws IOException {
@ -34,35 +41,47 @@ public final class DesktopConnection implements Closeable {
return localSocket;
}
private static LocalSocket listenAndAccept(String abstractName) throws IOException {
LocalServerSocket localServerSocket = new LocalServerSocket(abstractName);
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket videoSocket;
LocalSocket controlSocket;
if (tunnelForward) {
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
try {
return localServerSocket.accept();
videoSocket = localServerSocket.accept();
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
try {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
} finally {
localServerSocket.close();
}
}
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);
videoSocket = connect(SOCKET_NAME);
try {
controlSocket = connect(SOCKET_NAME);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
}
DesktopConnection connection = new DesktopConnection(socket);
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
Size videoSize = device.getScreenInfo().getVideoSize();
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
}
public void close() throws IOException {
socket.shutdownInput();
socket.shutdownOutput();
socket.close();
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
controlSocket.shutdownInput();
controlSocket.shutdownOutput();
controlSocket.close();
}
@SuppressWarnings("checkstyle:MagicNumber")
@ -70,7 +89,7 @@ public final class DesktopConnection implements Closeable {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
int len = Math.min(DEVICE_NAME_FIELD_LENGTH - 1, deviceNameBytes.length);
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
@ -78,19 +97,23 @@ public final class DesktopConnection implements Closeable {
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
IO.writeFully(fd, buffer, 0, buffer.length);
IO.writeFully(videoFd, buffer, 0, buffer.length);
}
public FileDescriptor getFd() {
return fd;
public FileDescriptor getVideoFd() {
return videoFd;
}
public ControlEvent receiveControlEvent() throws IOException {
ControlEvent event = reader.next();
while (event == null) {
reader.readFrom(inputStream);
event = reader.next();
public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(controlInputStream);
msg = reader.next();
}
return event;
return msg;
}
public void sendDeviceMessage(DeviceMessage msg) throws IOException {
writer.writeTo(msg, controlOutputStream);
}
}

View file

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

View file

@ -0,0 +1,27 @@
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

@ -0,0 +1,34 @@
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

@ -0,0 +1,34 @@
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

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

View file

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

View file

@ -0,0 +1,47 @@
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,7 +1,5 @@
package com.genymobile.scrcpy;
import android.graphics.Point;
import java.util.Objects;
public class Position {

View file

@ -70,8 +70,9 @@ public class ScreenEncoder implements Device.RotationListener {
codec.start();
try {
alive = encode(codec, fd);
} finally {
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} finally {
destroyDisplay(display);
codec.release();
surface.release();

View file

@ -19,12 +19,17 @@ public final class Server {
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
if (options.getControl()) {
Controller controller = new Controller(device, connection);
// asynchronous
startEventController(device, connection);
startController(controller);
startDeviceMessageSender(controller.getSender());
}
try {
// synchronous
screenEncoder.streamScreen(device, connection.getFd());
screenEncoder.streamScreen(device, connection.getVideoFd());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
@ -32,15 +37,29 @@ public final class Server {
}
}
private static void startEventController(final Device device, final DesktopConnection connection) {
private static void startController(final Controller controller) {
new Thread(new Runnable() {
@Override
public void run() {
try {
new EventController(device, connection).control();
controller.control();
} catch (IOException e) {
// this is expected on close
Ln.d("Event controller stopped");
Ln.d("Controller stopped");
}
}
}).start();
}
private static void startDeviceMessageSender(final DeviceMessageSender sender) {
new Thread(new Runnable() {
@Override
public void run() {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
}
}).start();
@ -48,7 +67,7 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) {
if (args.length != 5) {
if (args.length != 6) {
throw new IllegalArgumentException("Expecting 5 parameters");
}
@ -70,6 +89,9 @@ public final class Server {
boolean sendFrameMeta = Boolean.parseBoolean(args[4]);
options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[5]);
options.setControl(control);
return options;
}

View file

@ -0,0 +1,23 @@
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

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

View file

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

View file

@ -10,6 +10,10 @@ public final class SurfaceControl {
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_NORMAL = 2;
static {
try {
CLASS = Class.forName("android.view.SurfaceControl");
@ -71,6 +75,22 @@ public final class SurfaceControl {
}
}
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
try {
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
try {
CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class).invoke(null, displayToken, mode);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void destroyDisplay(IBinder displayToken) {
try {
CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);

View file

@ -1,173 +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 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

@ -0,0 +1,304 @@
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

@ -0,0 +1,43 @@
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
}
}