Merge branch 'dev'
This commit is contained in:
commit
c2df0228a3
83 changed files with 3000 additions and 1668 deletions
85
DEVELOP.md
85
DEVELOP.md
|
@ -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
4
FAQ.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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)"; \
|
||||
|
|
36
README.md
36
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
50
app/src/cbuf.h
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
86
app/src/control_msg.c
Normal 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
74
app/src/control_msg.h
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
48
app/src/device_msg.c
Normal 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
32
app/src/device_msg.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
bool
|
||||
fps_counter_is_started(struct fps_counter *counter) {
|
||||
return SDL_AtomicGet(&counter->started);
|
||||
}
|
||||
|
||||
void
|
||||
fps_counter_interrupt(struct fps_counter *counter) {
|
||||
if (!counter->thread) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
mutex_lock(counter->mutex);
|
||||
counter->interrupted = true;
|
||||
mutex_unlock(counter->mutex);
|
||||
// wake up blocking wait
|
||||
cond_signal(counter->state_cond);
|
||||
}
|
||||
|
||||
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_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);
|
||||
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);
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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,27 +294,32 @@ 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[] = {
|
||||
{"always-on-top", no_argument, NULL, 'T'},
|
||||
{"bit-rate", required_argument, NULL, 'b'},
|
||||
{"crop", required_argument, NULL, 'c'},
|
||||
{"fullscreen", no_argument, NULL, 'f'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"max-size", required_argument, NULL, 'm'},
|
||||
{"no-control", no_argument, NULL, 'n'},
|
||||
{"no-display", no_argument, NULL, 'N'},
|
||||
{"port", required_argument, NULL, 'p'},
|
||||
{"record", required_argument, NULL, 'r'},
|
||||
{"record-format", required_argument, NULL, 'f'},
|
||||
{"serial", required_argument, NULL, 's'},
|
||||
{"show-touches", no_argument, NULL, 't'},
|
||||
{"version", no_argument, NULL, 'v'},
|
||||
{NULL, 0, NULL, 0 },
|
||||
{"always-on-top", no_argument, NULL, 'T'},
|
||||
{"bit-rate", required_argument, NULL, 'b'},
|
||||
{"crop", required_argument, NULL, 'c'},
|
||||
{"fullscreen", no_argument, NULL, 'f'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"max-size", required_argument, NULL, 'm'},
|
||||
{"no-control", no_argument, NULL, 'n'},
|
||||
{"no-display", no_argument, NULL, 'N'},
|
||||
{"port", required_argument, NULL, 'p'},
|
||||
{"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;
|
||||
|
||||
|
|
|
@ -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
107
app/src/receiver.c
Normal 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
32
app/src/receiver.h
Normal 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
|
|
@ -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
|
||||
|
|
171
app/src/scrcpy.c
171
app/src/scrcpy.c
|
@ -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, ¶ms)) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
146
app/src/server.c
146
app/src/server.c
|
@ -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;
|
||||
}
|
||||
|
||||
if (!server->tunnel_forward) {
|
||||
// we don't need the server socket anymore
|
||||
close_socket(&server->server_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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -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, \
|
||||
.local_port = 0, \
|
||||
#define SERVER_INITIALIZER { \
|
||||
.serial = NULL, \
|
||||
.process = PROCESS_NONE, \
|
||||
.server_socket = INVALID_SOCKET, \
|
||||
.video_socket = INVALID_SOCKET, \
|
||||
.control_socket = INVALID_SOCKET, \
|
||||
.local_port = 0, \
|
||||
.tunnel_enabled = false, \
|
||||
.tunnel_forward = false, \
|
||||
.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
|
||||
SDL_DestroyMutex(vb->mutex);
|
||||
goto error_2;
|
||||
vb->render_expired_frames = render_expired_frames;
|
||||
if (render_expired_frames) {
|
||||
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
|
||||
SDL_DestroyMutex(vb->mutex);
|
||||
goto error_2;
|
||||
}
|
||||
// interrupted is not used if expired frames are not rendered
|
||||
// since offering a frame will never block
|
||||
vb->interrupted = false;
|
||||
}
|
||||
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
|
||||
SDL_DestroyCond(vb->rendering_frame_consumed_cond);
|
||||
#endif
|
||||
if (vb->render_expired_frames) {
|
||||
SDL_DestroyCond(vb->rendering_frame_consumed_cond);
|
||||
}
|
||||
SDL_DestroyMutex(vb->mutex);
|
||||
av_frame_free(&vb->rendering_frame);
|
||||
av_frame_free(&vb->decoding_frame);
|
||||
|
@ -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
|
||||
while (!vb->rendering_frame_consumed && !vb->interrupted) {
|
||||
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex);
|
||||
if (vb->render_expired_frames) {
|
||||
// wait for the current (expired) frame to be consumed
|
||||
while (!vb->rendering_frame_consumed && !vb->interrupted) {
|
||||
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex);
|
||||
}
|
||||
} else if (!vb->rendering_frame_consumed) {
|
||||
fps_counter_add_skipped_frame(vb->fps_counter);
|
||||
}
|
||||
#else
|
||||
if (vb->fps_counter.started && !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);
|
||||
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);
|
||||
}
|
||||
#ifndef SKIP_FRAMES
|
||||
// if SKIP_FRAMES is disabled, then notify the decoder the current frame is
|
||||
// consumed, so that it may push a new one
|
||||
cond_signal(vb->rendering_frame_consumed_cond);
|
||||
#endif
|
||||
return vb->rendering_frame;
|
||||
}
|
||||
|
||||
void
|
||||
video_buffer_interrupt(struct video_buffer *vb) {
|
||||
#ifdef SKIP_FRAMES
|
||||
(void) vb; // unused
|
||||
#else
|
||||
mutex_lock(vb->mutex);
|
||||
vb->interrupted = true;
|
||||
mutex_unlock(vb->mutex);
|
||||
// wake up blocking wait
|
||||
cond_signal(vb->rendering_frame_consumed_cond);
|
||||
#endif
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
73
app/tests/test_cbuf.c
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
248
app/tests/test_control_msg_serialize.c
Normal file
248
app/tests/test_control_msg_serialize.c
Normal 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;
|
||||
}
|
28
app/tests/test_device_msg_deserialize.c
Normal file
28
app/tests/test_device_msg_deserialize.c
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
project('scrcpy', 'c',
|
||||
version: '1.8',
|
||||
version: '1.9',
|
||||
meson_version: '>= 0.37',
|
||||
default_options: 'c_std=c11')
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
124
server/src/main/java/com/genymobile/scrcpy/ControlMessage.java
Normal file
124
server/src/main/java/com/genymobile/scrcpy/ControlMessage.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
@ -91,7 +123,7 @@ public class EventController {
|
|||
|
||||
private boolean injectChar(char c) {
|
||||
String decomposed = KeyComposition.decompose(c);
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
|
||||
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c};
|
||||
KeyEvent[] events = charMap.getEvents(chars);
|
||||
if (events == null) {
|
||||
return false;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
try {
|
||||
return localServerSocket.accept();
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
||||
LocalSocket socket;
|
||||
LocalSocket videoSocket;
|
||||
LocalSocket controlSocket;
|
||||
if (tunnelForward) {
|
||||
socket = listenAndAccept(SOCKET_NAME);
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
socket.getOutputStream().write(0);
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||
try {
|
||||
videoSocket = localServerSocket.accept();
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
throwable.printStackTrace();
|
||||
System.out.println(PREFIX + "ERROR: " + message);
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String message) {
|
||||
e(message, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
47
server/src/main/java/com/genymobile/scrcpy/Point.java
Normal file
47
server/src/main/java/com/genymobile/scrcpy/Point.java
Normal 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
|
||||
+ '}';
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Point;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Position {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -19,12 +19,17 @@ public final class Server {
|
|||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate());
|
||||
|
||||
// asynchronous
|
||||
startEventController(device, connection);
|
||||
if (options.getControl()) {
|
||||
Controller controller = new Controller(device, connection);
|
||||
|
||||
// asynchronous
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
23
server/src/main/java/com/genymobile/scrcpy/StringUtils.java
Normal file
23
server/src/main/java/com/genymobile/scrcpy/StringUtils.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue