Compare commits

..

37 Commits

Author SHA1 Message Date
Romain Vimont
72c83b2ac2 Format meson.build for readability 2019-05-28 21:04:03 +02:00
Romain Vimont
7dcac5b8b3 Replace SDL_bool by bool in tests
Commit dfed1b250e replaced SDL types by
standard types in sources, but tests were not updated.
2019-05-28 21:04:03 +02:00
Romain Vimont
f88345de0a Use different sockets for video and control
The socket used the device-to-computer direction to stream the video and
the computer-to-device direction to send control events.

Some features, like copy-paste from device to computer, require to send
non-video data from the device to the computer.

To make them possible, use two sockets:
 - one for streaming the video from the device to the client;
 - one for control/events in both direction.
2019-05-28 21:03:54 +02:00
Romain Vimont
2a1bdd0af3 Use net_recv() to read only one byte
Partial read is impossible for 1 byte, so net_recv_all() is useless.
2019-05-28 21:03:44 +02:00
Romain Vimont
2a7fee6c16 Simplify server_connect_to()
Only use 2 branches, using either forward or remote tunnel.
2019-05-28 21:02:57 +02:00
Romain Vimont
92f0266704 Make server_connect_to() return a bool
The resulting socket is accessible from the server instance, there is no
need to return it.

This paves the way to use several sockets in parallel.
2019-05-28 13:43:34 +02:00
Romain Vimont
6cf134f31f Fix indentation
Previous refactorings broke indentation.
2019-05-28 13:37:27 +02:00
Romain Vimont
b75f0e9427 Merge branch 'master' into dev 2019-05-28 13:31:37 +02:00
Romain Vimont
5d473efeb5 Bind Home key to MOVE_HOME
On pressing Home key on the computer, move the cursor to the beginning
of the line instead of going back to the home screen.

<https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_HOME>
<https://developer.android.com/reference/android/view/KeyEvent.html#KEYCODE_MOVE_HOME>

Fixes (part of) <https://github.com/Genymobile/scrcpy/issues/555>.
2019-05-27 10:24:47 +02:00
Romain Vimont
a41dd6c79f Make owned filename a pointer-to-non-const
The file handler owns the filename string, so it needs to free it.
Therefore, it should not be a pointer-to-const.
2019-05-24 17:25:31 +02:00
Romain Vimont
c3779d8513 Make owned serial a pointer-to-non-const
The file handler owns the serial, so it needs to free it. Therefore, it
should not be a pointer-to-const.
2019-05-24 17:24:17 +02:00
Romain Vimont
b3bd5f1b80 Remove useless casts to (void *) 2019-05-24 17:23:21 +02:00
Romain Vimont
a920ba6471 Explain how to customize path in README 2019-05-24 13:25:12 +02:00
Romain Vimont
3133d5d1c7 Continue on icon loading failure
If loading the icon from xpm fails, launch scrcpy without window icon.

<https://github.com/Genymobile/scrcpy/issues/539>
2019-05-23 20:58:08 +02:00
Romain Vimont
2dc1a59471 Check surface returned for icon
SDL_CreateRGBSurfaceFrom() may return NULL, causing a segfault.

<https://github.com/Genymobile/scrcpy/issues/539>
2019-05-20 09:44:45 +02:00
Romain Vimont
3068457b90 Log characters failed to be injected
Some characters may not be injected (e.g. '\r`). Log them instead of
ignoring them silently.
2019-05-20 08:40:10 +02:00
Romain Vimont
56f8e78f58 Merge pull request #542 from npes87184/dev
Return success count in injectText
2019-05-20 08:39:02 +02:00
Yu-Chen Lin
1630f923ef Return success count in injectText
It will insert as many text as possible now.
Fix #509, tested on Windows 10 and Arch Linux.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-05-20 08:36:32 +02:00
Romain Vimont
e443518ed9 Print adb command on error
When the execution of an adb command fails, print the command. This will
help to understand what went wrong.

See <https://github.com/Genymobile/scrcpy/issues/530>.
2019-05-12 15:16:13 +02:00
Romain Vimont
eeb8e8420f Use size_t for command length
The size of an array should have type size_t.
2019-05-12 14:31:18 +02:00
Romain Vimont
39b5893c42 Merge pull request #522 from dos1/compositor
Disable X11 compositor bypass
2019-05-05 17:35:10 +02:00
Sebastian Krzyszkowiak
b941854c73 Disable X11 compositor bypass
Compositor bypass is meant for fullscreen games consuming lots of GPU
resources. For a light app that will usually be windowed, this only
causes unnecessary compositor suspends, especially visible (and
annoying) with complying window manager like KWin.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-05-05 17:35:00 +02:00
Sebastian Krzyszkowiak
068253a3a2 Fix mouse focus clickthrough
Mouse focus clickthrough didn't work due to compat.h header not being
included in scrcpy.c.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-05-05 17:28:25 +02:00
Romain Vimont
c8338b2918 Recover if expand/collapse panels is not available
Some devices don't have the required method. Recover gracefully without
crashing the server.

Fixes <https://github.com/Genymobile/scrcpy/issues/506>.
2019-05-04 14:49:48 +02:00
Romain Vimont
2837c6eaab Add method to log error without throwable
Add Ln.e(message) in addition to Ln.e(message, error).
2019-05-04 14:49:04 +02:00
Romain Vimont
668e54fd4b Upgrade gradle 2019-05-04 14:49:04 +02:00
Romain Vimont
01664777c8 Merge branch 'master' into dev 2019-05-04 14:48:54 +02:00
Gerdal
ffa8c66979 Fix link error on Windows Subsystem for Linux
Build failed on WSL because of lack of reference to WinMain@16 during
linking.

Fixes <https://github.com/Genymobile/scrcpy/issues/316>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-03-31 20:07:07 +02:00
Romain Vimont
5254e585c6 Run server tests on release 2019-03-27 21:51:42 +01:00
Romain Vimont
66baf0f95b Run tests with ASAN enabled
This may capture more errors (like
e2ef39fae5).
2019-03-27 21:50:25 +01:00
Romain Vimont
f11b0ec204 Fix server checkstyle errors
Fix errors reported by:

    gradle -p server check
2019-03-27 21:47:54 +01:00
Romain Vimont
e2ef39fae5 Fix overflow in test
The serialized text is not nul-terminated (its size is explicitely
provided), but the input text in the event is a nul-terminated string.

The test was failing with ASAN enabled.
2019-03-25 11:33:32 +01:00
Romain Vimont
3eda38e5fc Do not call codec.stop() on exception
On exception, the codec is not in a state were .stop() can be called.
2019-03-21 18:46:22 +01:00
Andrew Rabert
a16cf95b8e Remove deprecated Arch Linux package
The `scrcpy-prebuiltserver` has been deprecated in favor of the `scrcpy`
package.

<https://aur.archlinux.org/cgit/aur.git/commit/?h=scrcpy-prebuiltserver&id=2ef4359b2e45fc278a191fae014d381b486ffcfe>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-03-10 23:19:09 +01:00
Romain Vimont
71fd238b0a Update developer documentation for v1.8 2019-03-07 20:48:43 +01:00
Romain Vimont
d795144a36 Add note about Ctrl+C on Windows while recording
Ctrl+C kills the app on Windows, so the recorded file is broken.
2019-03-07 20:42:08 +01:00
Romain Vimont
c287826f8e Update links to v1.8 in README and BUILD 2019-03-07 20:42:02 +01:00
28 changed files with 343 additions and 160 deletions

View File

@@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.7.jar`][direct-scrcpy-server] - [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
_(SHA-256: ee86ec8424f7dc50cacdf927312bdb46e0aa0d68611da584dc4b16d8057bc25e)_ _(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-server-v1.7.jar [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String...
args)`][main] method), compiled against the Android framework, and executed as args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device. `shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61 [main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100
To run such a Java application, the classes must be [_dexed_][dex] (typically, To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@@ -65,8 +65,8 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl]. components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178 [hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers [wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view [aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view
### Threading ### Threading
@@ -89,9 +89,9 @@ The video is encoded using the [`MediaCodec`] API. The codec takes its input
from a [surface] associated to the display, and writes the resulting H.264 from a [surface] associated to the display, and writes the resulting H.264
stream to the provided output stream (the socket connected to the client). stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64 [surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70
On device [rotation], the codec, surface and display are reinitialized, and a On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced. new video stream is produced.
@@ -105,8 +105,9 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92 [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.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126 [repeat]:
https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
@@ -124,11 +125,11 @@ All of them may need to inject input events to the system. To do so, they use
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]). [`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70 [`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 [`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@@ -153,7 +154,7 @@ Note that the client-server roles are expressed at the application level:
- the server _serves_ video stream and handle requests from the client, - the server _serves_ video stream and handle requests from the client,
- the client _controls_ the device through the server. - the client _controls_ the device through the server.
However, the roles are inverted at the network level: However, the roles are reversed at the network level:
- the client opens a server socket and listen on a port before starting the - the client opens a server socket and listen on a port before starting the
server, server,
@@ -162,6 +163,9 @@ However, the roles are inverted at the network level:
This role inversion guarantees that the connection will not fail due to race This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling. conditions, and avoids polling.
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
reverse`. See commit [1038bad] and [issue #5].)_
Once the server is connected, it sends the device information (name and initial Once the server is connected, it sends the device information (name and initial
screen dimensions). Thus, the client may init the window and renderer, before screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available. the first frame is available.
@@ -169,6 +173,8 @@ the first frame is available.
To minimize startup time, SDL initialization is performed while listening for To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]). the connection from the server (see commit [90a46b4]).
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e [90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
@@ -177,17 +183,25 @@ the connection from the server (see commit [90a46b4]).
The client uses 3 threads: The client uses 3 threads:
- the **main** thread, executing the SDL event loop, - the **main** thread, executing the SDL event loop,
- the **decoder** thread, decoding video frames, - 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 events_ to the server.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window).
### Decoder
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264
stream from the socket, and notifies the main thread when a new frame is
available.
There are two [frames] simultaneously in memory: ### Stream
The video [stream] is received from the socket (connected to the server on the
device) in a separate thread.
If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_
to decode the H.264 stream from the socket, and notifies the main thread when a
new frame is available.
There are two [frames][video_buffer] simultaneously in memory:
- the **decoding** frame, written by the decoder from the decoder thread, - the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread. - the **rendering** frame, rendered in a texture from the main thread.
@@ -195,9 +209,23 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one. to decode a new frame while the main thread renders the last one.
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h 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
```
+----------+ +----------+
---> | decoder | ---> | screen |
+---------+ / +----------+ +----------+
socket ---> | stream | ----
+---------+ \ +----------+
---> | recorder |
+----------+
```
### Controller ### Controller
@@ -211,10 +239,10 @@ events_ to a blocking queue hold by the controller. On its own thread, the
controller takes events from the queue, that it serializes and sends to the controller takes events from the queue, that it serializes and sends to the
client. client.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h [controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h [controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h [inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h [convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h
### UI and event loop ### UI and event loop
@@ -225,9 +253,10 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager]. delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c [scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38 [event loop]:
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c#L187
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/screen.h
## Hack ## Hack

View File

@@ -1,4 +1,4 @@
# scrcpy (v1.7) # scrcpy (v1.8)
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
@@ -29,12 +29,10 @@ control it using keyboard and mouse.
On Linux, you typically need to [build the app manually][BUILD]. Don't worry, On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard. it's not that hard.
For Arch Linux, two [AUR] packages have been created by users: For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
@@ -47,13 +45,13 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available: (including `adb`) are available:
- [`scrcpy-win32-v1.7.zip`][direct-win32] - [`scrcpy-win32-v1.8.zip`][direct-win32]
_(SHA-256: 98ae36f2da0b8212c07066fd93139650554274f863d4cee0781501a0c84f7c23)_ _(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
- [`scrcpy-win64-v1.7.zip`][direct-win64] - [`scrcpy-win64-v1.8.zip`][direct-win64]
_(SHA-256: b41416547521062f19e3f3f539e89a70e713bd086e69ef1b29c128993f7aa462)_ _(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-win32-v1.7.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-win64-v1.7.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip
You can also [build the app manually][BUILD]. You can also [build the app manually][BUILD].
@@ -171,6 +169,7 @@ To disable mirroring while recording:
scrcpy --no-display --record file.mp4 scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C # interrupt recording with Ctrl+C
# Ctrl+C does not terminate properly on Windows, so disconnect the device
``` ```
"Skipped frames" are recorded, even if they are not displayed in real time (for "Skipped frames" are recorded, even if they are not displayed in real time (for
@@ -295,6 +294,19 @@ _¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._ _²Right-click turns the screen on if it was off, presses BACK otherwise._
## Custom paths
To use a specific _adb_ binary, configure its path in the environment variable
`ADB`:
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file (it can be [useful] on
Windows), configure its path in `SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## Why _scrcpy_? ## Why _scrcpy_?
A colleague challenged me to find a name as unpronounceable as [gnirehtet]. A colleague challenged me to find a name as unpronounceable as [gnirehtet].

View File

@@ -143,18 +143,34 @@ else
link_args = [] link_args = []
endif 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
tests = [ tests = [
['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/control_event.c']], ['test_control_event_queue', [
['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/control_event.c']], 'tests/test_control_event_queue.c',
['test_strutil', ['tests/test_strutil.c', 'src/str_util.c']], 'src/control_event.c'
]],
['test_control_event_serialize', [
'tests/test_control_event_serialize.c',
'src/control_event.c'
]],
['test_strutil', [
'tests/test_strutil.c',
'src/str_util.c'
]],
] ]
foreach t : tests 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) test(t[0], exe)
endforeach endforeach

View File

@@ -1,5 +1,6 @@
#include "command.h" #include "command.h"
#include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -20,15 +21,52 @@ get_adb_command(void) {
return adb_command; return adb_command;
} }
// serialize argv to string "[arg1], [arg2], [arg3]"
static size_t
argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
size_t idx = 0;
bool first = true;
while (*argv) {
const char *arg = *argv;
size_t len = strlen(arg);
// count space for "[], ...\0"
if (idx + len + 8 >= bufsize) {
// not enough space, truncate
assert(idx < bufsize - 4);
memcpy(&buf[idx], "...", 3);
idx += 3;
break;
}
if (first) {
first = false;
} else {
buf[idx++] = ',';
buf[idx++] = ' ';
}
buf[idx++] = '[';
memcpy(&buf[idx], arg, len);
idx += len;
buf[idx++] = ']';
argv++;
}
assert(idx < bufsize);
buf[idx] = '\0';
return idx;
}
static void static void
show_adb_err_msg(enum process_result err) { show_adb_err_msg(enum process_result err, const char *const argv[]) {
char buf[512];
switch (err) { switch (err) {
case PROCESS_ERROR_GENERIC: case PROCESS_ERROR_GENERIC:
LOGE("Failed to execute adb"); argv_to_string(argv, buf, sizeof(buf));
LOGE("Failed to execute: %s", buf);
break; break;
case PROCESS_ERROR_MISSING_BINARY: case PROCESS_ERROR_MISSING_BINARY:
LOGE("'adb' command not found (make it accessible from your PATH " argv_to_string(argv, buf, sizeof(buf));
"or define its full path in the ADB environment variable)"); LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)");
break; break;
case PROCESS_SUCCESS: case PROCESS_SUCCESS:
/* do nothing */ /* do nothing */
@@ -37,7 +75,7 @@ show_adb_err_msg(enum process_result err) {
} }
process_t process_t
adb_execute(const char *serial, const char *const adb_cmd[], int len) { adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
const char *cmd[len + 4]; const char *cmd[len + 4];
int i; int i;
process_t process; process_t process;
@@ -54,7 +92,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], int len) {
cmd[len + i] = NULL; cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd[0], cmd, &process); enum process_result r = cmd_execute(cmd[0], cmd, &process);
if (r != PROCESS_SUCCESS) { if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r); show_adb_err_msg(r, cmd);
return PROCESS_NONE; return PROCESS_NONE;
} }
return process; return process;

View File

@@ -49,7 +49,7 @@ bool
cmd_simple_wait(process_t pid, exit_code_t *exit_code); cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t process_t
adb_execute(const char *serial, const char *const adb_cmd[], int len); adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
process_t process_t
adb_forward(const char *serial, uint16_t local_port, adb_forward(const char *serial, uint16_t local_port,

View File

@@ -43,4 +43,9 @@
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP # define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
#endif #endif
#if SDL_VERSION_ATLEAST(2, 0, 8)
// <https://hg.libsdl.org/SDL/rev/dfde5d3f9781>
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
#endif
#endif #endif

View File

@@ -81,9 +81,9 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL); MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB); MAP(SDLK_TAB, AKEYCODE_TAB);
MAP(SDLK_HOME, AKEYCODE_HOME);
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
MAP(SDLK_HOME, AKEYCODE_MOVE_HOME);
MAP(SDLK_END, AKEYCODE_MOVE_END); MAP(SDLK_END, AKEYCODE_MOVE_END);
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);

View File

@@ -10,11 +10,11 @@
struct request { struct request {
file_handler_action_t action; file_handler_action_t action;
const char *file; char *file;
}; };
static struct request * static struct request *
request_new(file_handler_action_t action, const char *file) { request_new(file_handler_action_t action, char *file) {
struct request *req = SDL_malloc(sizeof(*req)); struct request *req = SDL_malloc(sizeof(*req));
if (!req) { if (!req) {
return NULL; return NULL;
@@ -29,8 +29,8 @@ request_free(struct request *req) {
if (!req) { if (!req) {
return; return;
} }
SDL_free((void *) req->file); SDL_free(req->file);
SDL_free((void *) req); SDL_free(req);
} }
static bool static bool
@@ -121,7 +121,7 @@ file_handler_destroy(struct file_handler *file_handler) {
SDL_DestroyCond(file_handler->event_cond); SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex); SDL_DestroyMutex(file_handler->mutex);
request_queue_destroy(&file_handler->queue); request_queue_destroy(&file_handler->queue);
SDL_free((void *) file_handler->serial); SDL_free(file_handler->serial);
} }
static process_t static process_t
@@ -137,7 +137,7 @@ push_file(const char *serial, const char *file) {
bool bool
file_handler_request(struct file_handler *file_handler, file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, file_handler_action_t action,
const char *file) { char *file) {
bool res; bool res;
// start file_handler if it's used for the first time // start file_handler if it's used for the first time

View File

@@ -21,7 +21,7 @@ struct request_queue {
}; };
struct file_handler { struct file_handler {
const char *serial; char *serial;
SDL_Thread *thread; SDL_Thread *thread;
SDL_mutex *mutex; SDL_mutex *mutex;
SDL_cond *event_cond; SDL_cond *event_cond;
@@ -46,9 +46,10 @@ file_handler_stop(struct file_handler *file_handler);
void void
file_handler_join(struct file_handler *file_handler); file_handler_join(struct file_handler *file_handler);
// take ownership of file, and will SDL_free() it
bool bool
file_handler_request(struct file_handler *file_handler, file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, file_handler_action_t action,
const char *file); char *file);
#endif #endif

View File

@@ -5,6 +5,7 @@
#include <stdint.h> #include <stdint.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "compat.h" #include "compat.h"
@@ -104,7 +105,6 @@ static void usage(const char *arg0) {
" resize window to remove black borders\n" " resize window to remove black borders\n"
"\n" "\n"
" Ctrl+h\n" " Ctrl+h\n"
" Home\n"
" Middle-click\n" " Middle-click\n"
" click on HOME\n" " click on HOME\n"
"\n" "\n"

View File

@@ -9,6 +9,7 @@
#include "command.h" #include "command.h"
#include "common.h" #include "common.h"
#include "compat.h"
#include "controller.h" #include "controller.h"
#include "decoder.h" #include "decoder.h"
#include "device.h" #include "device.h"
@@ -68,6 +69,13 @@ sdl_init_and_configure(bool display) {
} }
#endif #endif
#ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
// Disable compositor bypassing on X11
if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) {
LOGW("Could not disable X11 compositor bypass");
}
#endif
// Do not disable the screensaver when scrcpy is running // Do not disable the screensaver when scrcpy is running
SDL_EnableScreenSaver(); SDL_EnableScreenSaver();
@@ -282,8 +290,7 @@ scrcpy(const struct scrcpy_options *options) {
goto finally_destroy_server; goto finally_destroy_server;
} }
socket_t device_socket = server_connect_to(&server); if (!server_connect_to(&server)) {
if (device_socket == INVALID_SOCKET) {
server_stop(&server); server_stop(&server);
ret = false; ret = false;
goto finally_destroy_server; goto finally_destroy_server;
@@ -295,7 +302,7 @@ scrcpy(const struct scrcpy_options *options) {
// screenrecord does not send frames when the screen content does not // screenrecord does not send frames when the screen content does not
// change therefore, we transmit the screen size before the video stream, // change therefore, we transmit the screen size before the video stream,
// to be able to init the window immediately // to be able to init the window immediately
if (!device_read_info(device_socket, device_name, &frame_size)) { if (!device_read_info(server.video_socket, device_name, &frame_size)) {
server_stop(&server); server_stop(&server);
ret = false; ret = false;
goto finally_destroy_server; goto finally_destroy_server;
@@ -334,7 +341,7 @@ scrcpy(const struct scrcpy_options *options) {
av_log_set_callback(av_log_callback); 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 // now we consumed the header values, the socket receives the video stream
// start the stream // start the stream
@@ -346,7 +353,7 @@ scrcpy(const struct scrcpy_options *options) {
if (display) { if (display) {
if (control) { if (control) {
if (!controller_init(&controller, device_socket)) { if (!controller_init(&controller, server.control_socket)) {
ret = false; ret = false;
goto finally_stop_stream; goto finally_stop_stream;
} }

View File

@@ -177,13 +177,12 @@ screen_init_rendering(struct screen *screen, const char *device_name,
} }
SDL_Surface *icon = read_xpm(icon_xpm); SDL_Surface *icon = read_xpm(icon_xpm);
if (!icon) { if (icon) {
LOGE("Could not load icon: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
SDL_SetWindowIcon(screen->window, icon); SDL_SetWindowIcon(screen->window, icon);
SDL_FreeSurface(icon); SDL_FreeSurface(icon);
} else {
LOGW("Could not load icon");
}
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
frame_size.height); frame_size.height);

View File

@@ -119,7 +119,7 @@ connect_and_read_byte(uint16_t port) {
char byte; char byte;
// the connection may succeed even if the server behind the "adb tunnel" // the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection // is not listening, so read one byte to detect a working connection
if (net_recv_all(socket, &byte, 1) != 1) { if (net_recv(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel // the server is not listening yet behind the adb tunnel
return INVALID_SOCKET; return INVALID_SOCKET;
} }
@@ -210,7 +210,7 @@ server_start(struct server *server, const char *serial,
close_socket(&server->server_socket); close_socket(&server->server_socket);
} }
disable_tunnel(server); disable_tunnel(server);
SDL_free((void *) server->serial); SDL_free(server->serial);
return false; return false;
} }
@@ -219,31 +219,44 @@ server_start(struct server *server, const char *serial,
return true; return true;
} }
socket_t bool
server_connect_to(struct server *server) { server_connect_to(struct server *server) {
if (!server->tunnel_forward) { 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 { } else {
uint32_t attempts = 100; uint32_t attempts = 100;
uint32_t delay = 100; // ms uint32_t delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, server->video_socket =
delay); connect_to_server(server->local_port, attempts, delay);
if (server->video_socket == INVALID_SOCKET) {
return false;
} }
if (server->device_socket == INVALID_SOCKET) { // we know that the device is listening, we don't need several attempts
return INVALID_SOCKET; server->control_socket =
net_connect(IPV4_LOCALHOST, server->local_port);
if (server->control_socket == INVALID_SOCKET) {
return false;
} }
if (!server->tunnel_forward) {
// we don't need the server socket anymore
close_socket(&server->server_socket);
} }
// we don't need the adb tunnel anymore // we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure disable_tunnel(server); // ignore failure
server->tunnel_enabled = false; server->tunnel_enabled = false;
return server->device_socket; return true;
} }
void void
@@ -268,8 +281,11 @@ server_destroy(struct server *server) {
if (server->server_socket != INVALID_SOCKET) { if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket); close_socket(&server->server_socket);
} }
if (server->device_socket != INVALID_SOCKET) { if (server->video_socket != INVALID_SOCKET) {
close_socket(&server->device_socket); close_socket(&server->video_socket);
}
if (server->control_socket != INVALID_SOCKET) {
close_socket(&server->control_socket);
} }
SDL_free(server->serial); SDL_free(server->serial);
} }

View File

@@ -11,7 +11,8 @@ struct server {
char *serial; char *serial;
process_t process; process_t process;
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket; // only used if !tunnel_forward
socket_t device_socket; socket_t video_socket;
socket_t control_socket;
uint16_t local_port; uint16_t local_port;
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
@@ -22,7 +23,8 @@ struct server {
.serial = NULL, \ .serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.device_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
.local_port = 0, \ .local_port = 0, \
.tunnel_enabled = false, \ .tunnel_enabled = false, \
.tunnel_forward = false, \ .tunnel_forward = false, \
@@ -40,7 +42,7 @@ server_start(struct server *server, const char *serial,
const char *crop, bool send_frame_meta); const char *crop, bool send_frame_meta);
// block until the communication with the server is established // block until the communication with the server is established
socket_t bool
server_connect_to(struct server *server); server_connect_to(struct server *server);
// disconnect and kill the server process // disconnect and kill the server process

View File

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

View File

@@ -5,21 +5,21 @@
static void test_control_event_queue_empty(void) { static void test_control_event_queue_empty(void) {
struct control_event_queue queue; struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue); bool init_ok = control_event_queue_init(&queue);
assert(init_ok); assert(init_ok);
assert(control_event_queue_is_empty(&queue)); assert(control_event_queue_is_empty(&queue));
struct control_event dummy_event; struct control_event dummy_event;
SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event); bool push_ok = control_event_queue_push(&queue, &dummy_event);
assert(push_ok); assert(push_ok);
assert(!control_event_queue_is_empty(&queue)); assert(!control_event_queue_is_empty(&queue));
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); bool take_ok = control_event_queue_take(&queue, &dummy_event);
assert(take_ok); assert(take_ok);
assert(control_event_queue_is_empty(&queue)); assert(control_event_queue_is_empty(&queue));
SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event); bool take_empty_ok = control_event_queue_take(&queue, &dummy_event);
assert(!take_empty_ok); // the queue is empty assert(!take_empty_ok); // the queue is empty
control_event_queue_destroy(&queue); control_event_queue_destroy(&queue);
@@ -27,7 +27,7 @@ static void test_control_event_queue_empty(void) {
static void test_control_event_queue_full(void) { static void test_control_event_queue_full(void) {
struct control_event_queue queue; struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue); bool init_ok = control_event_queue_init(&queue);
assert(init_ok); assert(init_ok);
assert(!control_event_queue_is_full(&queue)); assert(!control_event_queue_is_full(&queue));
@@ -36,7 +36,7 @@ static void test_control_event_queue_full(void) {
// fill the queue // fill the queue
while (control_event_queue_push(&queue, &dummy_event)); while (control_event_queue_push(&queue, &dummy_event));
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event); bool take_ok = control_event_queue_take(&queue, &dummy_event);
assert(take_ok); assert(take_ok);
assert(!control_event_queue_is_full(&queue)); assert(!control_event_queue_is_full(&queue));
@@ -45,7 +45,7 @@ static void test_control_event_queue_full(void) {
static void test_control_event_queue_push_take(void) { static void test_control_event_queue_push_take(void) {
struct control_event_queue queue; struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue); bool init_ok = control_event_queue_init(&queue);
assert(init_ok); assert(init_ok);
struct control_event event = { struct control_event event = {
@@ -57,7 +57,7 @@ static void test_control_event_queue_push_take(void) {
}, },
}; };
SDL_bool push1_ok = control_event_queue_push(&queue, &event); bool push1_ok = control_event_queue_push(&queue, &event);
assert(push1_ok); assert(push1_ok);
event = (struct control_event) { event = (struct control_event) {
@@ -67,11 +67,11 @@ static void test_control_event_queue_push_take(void) {
}, },
}; };
SDL_bool push2_ok = control_event_queue_push(&queue, &event); bool push2_ok = control_event_queue_push(&queue, &event);
assert(push2_ok); assert(push2_ok);
// overwrite event // overwrite event
SDL_bool take1_ok = control_event_queue_take(&queue, &event); bool take1_ok = control_event_queue_take(&queue, &event);
assert(take1_ok); assert(take1_ok);
assert(event.type == CONTROL_EVENT_TYPE_KEYCODE); assert(event.type == CONTROL_EVENT_TYPE_KEYCODE);
assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN); assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN);
@@ -79,7 +79,7 @@ static void test_control_event_queue_push_take(void) {
assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON)); assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON));
// overwrite event // overwrite event
SDL_bool take2_ok = control_event_queue_take(&queue, &event); bool take2_ok = control_event_queue_take(&queue, &event);
assert(take2_ok); assert(take2_ok);
assert(event.type == CONTROL_EVENT_TYPE_TEXT); assert(event.type == CONTROL_EVENT_TYPE_TEXT);
assert(!strcmp(event.text_event.text, "abc")); assert(!strcmp(event.text_event.text, "abc"));

View File

@@ -49,13 +49,14 @@ static void test_serialize_text_event(void) {
static void test_serialize_long_text_event(void) { static void test_serialize_long_text_event(void) {
struct control_event event; struct control_event event;
event.type = CONTROL_EVENT_TYPE_TEXT; event.type = CONTROL_EVENT_TYPE_TEXT;
char text[TEXT_MAX_LENGTH]; char text[TEXT_MAX_LENGTH + 1];
memset(text, 'a', sizeof(text)); memset(text, 'a', sizeof(text));
text[TEXT_MAX_LENGTH] = '\0';
event.text_event.text = text; event.text_event.text = text;
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf); int size = control_event_serialize(&event, buf);
assert(size == 3 + sizeof(text)); assert(size == 3 + TEXT_MAX_LENGTH);
unsigned char expected[3 + TEXT_MAX_LENGTH]; unsigned char expected[3 + TEXT_MAX_LENGTH];
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE

View File

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

View File

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

View File

@@ -1,13 +1,22 @@
#!/bin/bash #!/bin/bash
set -e set -e
# build and test locally # test locally
TESTDIR=build_test
rm -rf "$TESTDIR"
# run client tests with ASAN enabled
meson "$TESTDIR" -Db_sanitize=address
ninja -C"$TESTDIR" test
# test server
GRADLE=${GRADLE:-./gradlew}
$GRADLE -p server check
BUILDDIR=build_release BUILDDIR=build_release
rm -rf "$BUILDDIR" rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
cd "$BUILDDIR" cd "$BUILDDIR"
ninja ninja
ninja test
cd - cd -
# build Windows releases # build Windows releases

View File

@@ -16,16 +16,20 @@ public final class DesktopConnection implements Closeable {
private static final String SOCKET_NAME = "scrcpy"; private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket socket; private final LocalSocket videoSocket;
private final InputStream inputStream; private final FileDescriptor videoFd;
private final FileDescriptor fd;
private final LocalSocket controlSocket;
private final InputStream controlInputStream;
private final ControlEventReader reader = new ControlEventReader(); private final ControlEventReader reader = new ControlEventReader();
private DesktopConnection(LocalSocket socket) throws IOException { private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.socket = socket; this.videoSocket = videoSocket;
inputStream = socket.getInputStream(); this.controlSocket = controlSocket;
fd = socket.getFileDescriptor(); controlInputStream = controlSocket.getInputStream();
videoFd = videoSocket.getFileDescriptor();
} }
private static LocalSocket connect(String abstractName) throws IOException { private static LocalSocket connect(String abstractName) throws IOException {
@@ -44,25 +48,46 @@ public final class DesktopConnection implements Closeable {
} }
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket socket; LocalSocket videoSocket;
LocalSocket controlSocket;
if (tunnelForward) { if (tunnelForward) {
socket = listenAndAccept(SOCKET_NAME); LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
try {
videoSocket = localServerSocket.accept();
// send one byte so the client may read() to detect a connection error // send one byte so the client may read() to detect a connection error
socket.getOutputStream().write(0); videoSocket.getOutputStream().write(0);
try {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
} finally {
localServerSocket.close();
}
} else { } 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(); Size videoSize = device.getScreenInfo().getVideoSize();
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection; return connection;
} }
public void close() throws IOException { public void close() throws IOException {
socket.shutdownInput(); videoSocket.shutdownInput();
socket.shutdownOutput(); videoSocket.shutdownOutput();
socket.close(); videoSocket.close();
controlSocket.shutdownInput();
controlSocket.shutdownOutput();
controlSocket.close();
} }
@SuppressWarnings("checkstyle:MagicNumber") @SuppressWarnings("checkstyle:MagicNumber")
@@ -78,17 +103,17 @@ public final class DesktopConnection implements Closeable {
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
IO.writeFully(fd, buffer, 0, buffer.length); IO.writeFully(videoFd, buffer, 0, buffer.length);
} }
public FileDescriptor getFd() { public FileDescriptor getVideoFd() {
return fd; return videoFd;
} }
public ControlEvent receiveControlEvent() throws IOException { public ControlEvent receiveControlEvent() throws IOException {
ControlEvent event = reader.next(); ControlEvent event = reader.next();
while (event == null) { while (event == null) {
reader.readFrom(inputStream); reader.readFrom(controlInputStream);
event = reader.next(); event = reader.next();
} }
return event; return event;

View File

@@ -1,7 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Point; import android.graphics.Point;
import android.os.SystemClock; import android.os.SystemClock;
@@ -105,13 +104,16 @@ public class EventController {
return true; return true;
} }
private boolean injectText(String text) { private int injectText(String text) {
int successCount = 0;
for (char c : text.toCharArray()) { for (char c : text.toCharArray()) {
if (!injectChar(c)) { 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) { private boolean injectMouse(int action, int buttons, Position position) {

View File

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

View File

@@ -52,7 +52,13 @@ public final class Ln {
if (isEnabled(Level.ERROR)) { if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable); Log.e(TAG, message, throwable);
System.out.println("ERROR: " + message); System.out.println("ERROR: " + message);
if (throwable != null) {
throwable.printStackTrace(); throwable.printStackTrace();
} }
} }
}
public static void e(String message) {
e(message, null);
}
} }

View File

@@ -3,7 +3,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.MediaMuxer;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
@@ -71,8 +70,9 @@ public class ScreenEncoder implements Device.RotationListener {
codec.start(); codec.start();
try { try {
alive = encode(codec, fd); alive = encode(codec, fd);
} finally { // do not call stop() on exception, it would trigger an IllegalStateException
codec.stop(); codec.stop();
} finally {
destroyDisplay(display); destroyDisplay(display);
codec.release(); codec.release();
surface.release(); surface.release();

View File

@@ -4,7 +4,6 @@ import android.graphics.Rect;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
public final class Server { public final class Server {
@@ -25,7 +24,7 @@ public final class Server {
try { try {
// synchronous // synchronous
screenEncoder.streamScreen(device, connection.getFd()); screenEncoder.streamScreen(device, connection.getVideoFd());
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");
@@ -49,8 +48,9 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber") @SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) { private static Options createOptions(String... args) {
if (args.length != 5) if (args.length != 5) {
throw new IllegalArgumentException("Expecting 5 parameters"); throw new IllegalArgumentException("Expecting 5 parameters");
}
Options options = new Options(); Options options = new Options();
@@ -73,6 +73,7 @@ public final class Server {
return options; return options;
} }
@SuppressWarnings("checkstyle:MagicNumber")
private static Rect parseCrop(String crop) { private static Rect parseCrop(String crop) {
if ("-".equals(crop)) { if ("-".equals(crop)) {
return null; return null;

View File

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