Compare commits

...

88 Commits
hidpi ... api19

Author SHA1 Message Date
Romain Vimont
1de8506a0d Support Android API 19
Since "adb forward" fallback has been implemented, it is easy to support
API 19.

Replace the incompatible calls related to MediaCodec to use
minSdkVersion 19 instead of 21.
2018-03-28 22:13:58 +02:00
Romain Vimont
71f50fb697 Merge branch 'master' into dev 2018-03-28 10:45:23 +02:00
Romain Vimont
82efff34e8 Factorize texture creation
SDL_CreateTexture() is called both during initialization and on frame
size change.

To avoid inconsistent changes to arguments value, factorize them to a
single function create_texture().
2018-03-27 11:01:40 +02:00
Romain Vimont
860006e082 Forward double-click events
Double-clicks were not sent to the device anymore since the
"double-click on black borders" feature.

When a double click occurs inside the device screen, send the event to
the device normally.

Fixes <https://github.com/Genymobile/scrcpy/issues/97>.
2018-03-26 14:49:10 +02:00
Romain Vimont
57eaf05289 Improve startup time when show_touches is enabled
Enabling "show touches" involves the execution of an adb command, which
takes some time.

In order to parallelize, execute the command as soon as possible, but
reap the process only once everything is initialized.
2018-03-25 16:39:36 +02:00
Romain Vimont
dd2a5c1ecf Disable "show touches" once window is closed
If --show-touches is set, then the option must be disabled on quit.

Since it executes an adb command, it takes some time, so close the
window beforehand so that the close window button does not seem
unresponsive.
2018-03-25 16:03:02 +02:00
Romain Vimont
66ec252893 Add an option to enable "show touches"
Add -t/--show-touches option to show physical touches while scrcpy is
running.

See <https://github.com/Genymobile/scrcpy/issues/96>.
2018-03-25 15:43:27 +02:00
Romain Vimont
b13d25b9f4 Group scrcpy options into a struct
The scrcpy() function accepts as many parameters as there are options.

To simplify, group all options in a separate struct.
2018-03-25 15:16:29 +02:00
Romain Vimont
b449c09442 Merge branch 'master' into dev 2018-03-25 15:16:19 +02:00
Romain Vimont
8b84492830 Merge branch 'stek29/macos' (#56)
macOS specific README changes
2018-03-25 14:31:02 +02:00
Viktor Oreshkin
4d50832f8e Add instructions to install Java 8 on macOS
And remove gcc from the packages list, clang is available by default.
2018-03-25 14:29:22 +02:00
Romain Vimont
e0e8dfeb3b Merge pull request #94 from pierlon/pierlon-patch-1
Add instructions to run via Docker
2018-03-25 14:12:29 +02:00
Romain Vimont
f4d6449af7 Merge pull request #95 from Sea-n/patch-1
Update README.md
2018-03-25 14:00:20 +02:00
Sean
64963fff62 Update README.md
Fix Typo
2018-03-25 17:51:29 +08:00
Pierre Gordon
b7d9b8739c Add instructions to run via Docker 2018-03-24 23:15:14 -05:00
Romain Vimont
88f6b43c8f Merge pull request #93 from CampbellOwen/patch-1
Add links to FFmpeg and LibSDL2 dependencies
2018-03-24 09:26:12 +01:00
Owen Campbell
324a264233 Change links to wikipedia 2018-03-24 01:09:41 -07:00
Owen Campbell
3bb2cda955 Add links to FFmpeg and LibSDL2 dependencies 2018-03-24 00:55:05 -07:00
Romain Vimont
35298bb0c6 Process the last video frame
On H.264 stream EOF, the eof_reached flag is set, but av_read_frame()
still provides a frame, so check the flag only afterwards.

As a side-effect, it also fixes a memory leak (the very last packet was
not unref).
2018-03-23 14:01:58 +01:00
Romain Vimont
73c332e3e4 Unref last packet on exit 2018-03-23 13:57:32 +01:00
Romain Vimont
15014f2689 Clarify adb requirements
Since _scrcpy_ also supports `adb forward`, remove the part about `adb
reverse`.

Make explicit that _adb_ is included in the prebuilt application for
Windows (many users manually download the platform-tools for no reason).
2018-03-23 10:55:52 +01:00
Romain Vimont
29b5c5b8f4 Merge branch 'arich/addShake' into dev (#85)
Add support for CTRL+S to send hardware "shake" to device
2018-03-23 10:13:19 +01:00
Romain Vimont
88ee6bc928 Swap MENU and APP_SWITCH shortcuts
Ctrl+s was mapped to MENU, while Ctrl+m was mapped to APP_SWITCH.

To avoid confusion, swap the shortcuts:
 - Ctrl+m like _M_enu
 - Ctrl+s like _S_witch
2018-03-23 10:10:24 +01:00
Romain Vimont
35e9a64c34 Rename "shake" to "menu"
The action sends AKEYCODE_MENU, so just name it "menu".

See <https://github.com/Genymobile/scrcpy/pull/85>.
2018-03-23 10:07:48 +01:00
Romain Vimont
9cac38fe58 Describe workaround to get output on Windows
Since nothing is printed to the console, we need a way to get the output
in case of errors.

Describe how in the README.
2018-03-23 09:57:45 +01:00
Andy Rich
301c52b603 Add support for CTRL+S to send hardware "shake" to device w/readme 2018-03-22 16:15:24 -07:00
Romain Vimont
f00c6c5b13 Disable custom SDL signal handlers
Request SDL not to replace the SIGINT and SIGTERM handlers, so that the
process is immediately terminated on Ctrl+C.

This avoids process hanging on Ctrl+C during network calls on
initialization.

Some of them accepted a timeout, but it was not used since
commit 9b056f5091 anymore.
2018-03-21 21:43:12 +01:00
Romain Vimont
3b3803da0d Remove useless blocks in switch/case
Remove unnecessary additional blocks.
2018-03-21 11:14:15 +01:00
Romain Vimont
f5cf6c1b2c Include source root directory
All headers and sources are in src/. To avoid using relative includes
from subdirectories ("../../"), include the source root directory.
2018-03-20 21:32:41 +01:00
Romain Vimont
2573df9727 Document the step to clone the project
This is not obvious to everyone, especially non-developers.
2018-03-18 12:10:06 +01:00
Romain Vimont
c65cb36d3b Increase the number of connection attempts
In "adb forward" mode, it may take a while before the server socket is
listening, so increase the number of connection attempts.

See <https://github.com/Genymobile/scrcpy/issues/5#issuecomment-373718551>.
2018-03-16 14:59:08 +01:00
Romain Vimont
821ec9843c Fix win32 build
The types size_t and ssize_t are defined on Windows (in MSYS2), so there
is no need to typedef SIZE_T and SSIZE_T.

Exit code is "unsigned long" both on Windows 32 and 64 bits.

See <https://github.com/Genymobile/scrcpy/issues/46#issuecomment-373603596>.
2018-03-16 08:58:59 +01:00
Romain Vimont
f16bd88802 Remove useless cast
For consistency with mouse button events handling, directly assign from
Sint32 to Uint16.
2018-03-15 16:30:51 +01:00
Romain Vimont
f3e8834a3c Fix warning message
Mouse "wheel button" is meaningless :)
2018-03-15 16:14:40 +01:00
Romain Vimont
080df5eb5d Fix switch/case code style
For readability and consistency, indent case statatements, and remove
unnecessary additional blocks.
2018-03-15 16:00:40 +01:00
Romain Vimont
047179f232 Add FAQ section about mouse clicks
On some devices, mouse clicks do not work by default. Enabling an option
is required.
2018-03-15 09:29:14 +01:00
Romain Vimont
2992dda497 Add link to the article for v1.1 in README 2018-03-14 18:03:34 +01:00
Romain Vimont
6406f64edc Update FAQ after v1.1 release
Two issues described in the FAQ have been fixed by V1.1. Remove them
from the FAQ.
2018-03-14 09:56:36 +01:00
Romain Vimont
f3f19d4e1d Update links to v1.1 in README 2018-03-14 09:53:55 +01:00
Romain Vimont
d744837f13 Bump version to 1.1 2018-03-14 09:34:00 +01:00
Romain Vimont
f7bc0bd5b5 Merge branch 'dev' into release 2018-03-14 09:33:53 +01:00
Romain Vimont
8a3c6a3ae7 Remove useless argument
Do not pass any data to the event watcher, it is unused.
2018-03-14 09:32:05 +01:00
Romain Vimont
c530d95881 Immediately close the server socket on the device
In "adb forward" mode, close the server socket as soon as the client is
connected.

Even if unlikely to be useful, it allows to run several instances of
scrcpy also in "adb forward" mode.
2018-03-14 09:28:25 +01:00
Romain Vimont
0b1e59186f Workaround continuous resizing on Windows/MacOS
On Windows and MacOS, resizing blocks the event loop, so resizing events
are not triggered:
 - <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
 - <https://stackoverflow.com/a/40693139/1987178>

As a workaround, register an event watcher to render the screen from
another thread.

Since the whole event loop is blocked during resizing, the screen
content is not refreshed (on Windows and MacOS) until resizing ends.
2018-03-13 22:48:04 +01:00
Romain Vimont
e69f6f710d Disable stdout/stderr buffering on Windows
In MSYS2 on Windows, the output is buffered by default. Disable
buffering to print output immediately.

Note that in cmd.exe, it still prints nothing.
2018-03-13 10:20:09 +01:00
Romain Vimont
b858204786 Remove black borders on double-click
Resize the window to fit the device screen on click on black borders
(same as Ctrl+x).

Suggested-by: Guillaume Roche <groche@genymobile.com>
2018-03-13 08:37:46 +01:00
Romain Vimont
e8510a8cc3 Add links to AUR packages in README 2018-03-12 16:00:12 +01:00
Romain Vimont
1038bad385 Make it work over tcpip
"adb reverse" currently does not work over tcpip (i.e. on a device
connected by "adb connect"):
<https://issuetracker.google.com/issues/37066218>

To work around the problem, if the call to "adb reverse" fails, then
fallback to "adb forward", and reverse the client/server roles.

Keep the "adb reverse" mode as the default because it does not involve
connection retries: when using "adb forward", the client must try to
connect successively until the server listens.

Due to the tunnel, every connect() will succeed, so the client must
attempt to read() to detect a connection failure. For this purpose, when
using the "adb forward" mode, the server initially writes a dummy byte,
read by the client.

Fixes <https://github.com/Genymobile/scrcpy/issues/5>.
2018-03-12 14:10:32 +01:00
Romain Vimont
2b3ed5bcdb Store serial in server instance
The serial is needed for many server actions, but this is an
implementation detail, so the caller should not have to provide it on
every call.

Instead, store the serial in the server instance on server_start().

This paves the way to implement the "adb forward" fallback properly.
2018-03-12 14:10:32 +01:00
Romain Vimont
9e328ef98b Always use the best render scale quality available
Because why not.

See <https://wiki.libsdl.org/SDL_HINT_RENDER_SCALE_QUALITY>.
2018-03-12 14:10:12 +01:00
Romain Vimont
f9a63ec272 Reverse horizontal scrolling behavior
The SDL mouse wheel event seems inconsistent between horizontal and
vertical scrolling.

> Movements to the left generate negative x values and to the right
> generate positive x values. Movements down (scroll backward) generate
> negative y values and up (scroll forward) generate positive y values.

<https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>

Reverse the horizontal.

Fixes <https://github.com/Genymobile/scrcpy/issues/49>.
2018-03-12 09:37:46 +01:00
Romain Vimont
f6d0893316 Merge branch 'fedora_install' (pull request #29)
Document how to install on Fedora
2018-03-11 22:07:12 +01:00
Romain Vimont
ed65cd72fd Use one subsection by distribution in README
Now that instructions are given for both Debian/Ubuntu and Fedora, use
subsections.
2018-03-11 22:06:15 +01:00
Michael Gangolf
70599998eb Document how to install on Fedora 2018-03-11 22:06:15 +01:00
Romain Vimont
e87cd175cc Improve dependencies in README
Document server and client dependencies separately, to avoid unneeded
packages installation when building using the prebuilt server.

Also remove "zip", since it's only used for building a portable version
(which is not documented in README).
2018-03-11 22:05:41 +01:00
Romain Vimont
c075ad0a1e Fix mouse clicks on LG devices
Use default values (0) for some fields of PointerCoords so that mouse
clicks work correctly on LG devices.

Fixes <https://github.com/Genymobile/scrcpy/issues/18>.
2018-03-11 14:11:43 +01:00
Romain Vimont
dac7196bd6 Support screens with dimensions not divisible by 8
The codec only supports dimensions which are multiple of 8.

Thus, when --max-size is specified, the value is always rounded down to
the nearest multiple of 8.

However, it was wrongly assumed that the physical size is always a
multiple of 8. To support such devices, also round down the physical
screen dimensions.

Fixes <https://github.com/Genymobile/scrcpy/issues/39>.
2018-03-11 14:09:25 +01:00
Romain Vimont
14b15ceb06 Add a FAQ for common issues 2018-03-11 10:40:46 +01:00
Romain Vimont
b9bb4ff740 Merge branch 'sdushantha' (pull request #28)
Improve README syntax highlighting.
2018-03-10 16:31:17 +01:00
Romain Vimont
cc4a015256 Add empty lines around code blocks
And fix spaces (do not randomly use non-breaking spaces for
indentation).
2018-03-10 16:24:03 +01:00
Siddharth Dushantha
8476b4aab8 removed "$" and changed Mac OS ---> MacOS 2018-03-10 15:34:59 +01:00
Siddharth Dushantha
a1491862e4 added "$" in front of terminal commands 2018-03-10 09:12:47 +01:00
Romain Vimont
c87d94ee27 Map middle-click to HOME
Middle-click is useless in practice. Use it for HOME.
2018-03-10 00:44:19 +01:00
Romain Vimont
675704c71c Map right-click to BACK if screen is on
Right-click was used to turn the screen on. It did nothing when the
screen was already on.

Instead, in that case, press BACK (like Vysor).

Suggested by: <https://www.reddit.com/r/Android/comments/834zmr/introducing_scrcpy_an_app_to_display_and_control/dvfueft/>
2018-03-10 00:16:29 +01:00
Romain Vimont
9396ea6d42 Fix text input event segfault
The text input control_event was initially designed for mapping
SDL_TextInputEvent, limited to 32 characters.

For simplicity, the copy/paste feature was implemented using the same
control_event: it just sends the text to paste.

However, the pasted text might have a length breaking some assumptions:
 - on the client, the event max-size was smaller than the text
   max-length,
 - on the server, the raw buffer storing the events was smaller than the
   max event size.

Fix these inconsistencies, and encode the length on 2 bytes, to accept
more than 256 characters.

Fixes <https://github.com/Genymobile/scrcpy/issues/10>.
2018-03-09 22:30:10 +01:00
Romain Vimont
f9562f537a Unref the packet on error
Do not leak the packet data on error.
2018-03-08 21:36:04 +01:00
Romain Vimont
a34fbd23e9 Do not leak the packet data
Oops! The content of the packets were never freed.
2018-03-08 20:46:02 +01:00
Romain Vimont
e8b8a570e7 Document ./run script usage
Indicate how to run the app from the build directory in README. It's
convenient during development.
2018-03-08 14:53:18 +01:00
Romain Vimont
0e9a76c0c4 Add link to blog article in README 2018-03-08 12:31:26 +01:00
Romain Vimont
f9f305d19d Update release checksums in README 2018-03-08 11:34:54 +01:00
Romain Vimont
727d1ef1e2 Add developer documentation
And update README.
2018-03-08 10:27:40 +01:00
Romain Vimont
c2ac6fe7bd Upgrade version to 1.0 2018-03-08 09:21:06 +01:00
Romain Vimont
e2a7abcd53 Implement clipboard paste
Paste computer clipboard to the device on Ctrl+v.

The other direction (pasting the device clipboard to the computer) is
not implemented. It would require a communication channel from the
device to the computer, other than the socket used by the video stream.
2018-03-07 18:07:02 +01:00
Romain Vimont
e4d64e8752 Initialize struct field by field
Initializing with braces initializes the other fields to 0, which is not
necessary.
2018-03-07 18:07:02 +01:00
Romain Vimont
fffeedffda Expose High DPI support configuration flag
The High DPI support is enabled by default, so that the renderer use the
full definition of High DPI screens.

However, there are still mouse coordinates problems on some MacOS having
High DPI support (but not all), so expose a way to disable it.
2018-03-07 18:07:02 +01:00
Romain Vimont
82b4acee73 Do not fail on EAGAIN
A call to avcodec_receive_frame() may return AVERROR(EAGAIN) if more
input is required. This is not an error, do not fail.
2018-03-07 18:07:02 +01:00
Romain Vimont
633b18d786 Provide a better URL to document key decomposition
Directly link to the relevant subsection.
2018-03-07 18:07:02 +01:00
Romain Vimont
ab780ce26d Avoid useless variables initialization
Initialize variables only when necessary.
2018-03-07 18:07:02 +01:00
Romain Vimont
84ad6633a6 Move the new avcodec implementation before the old
The API to decode the video frames is different depending on the
libavcodec version.

Move the new API usage to the #if-block.
2018-03-07 18:07:01 +01:00
Romain Vimont
1b0cea61a5 Do not use return code for thread run function
The decoder sometimes returned a non-zero value on error, but not on
every path.

Since we never use the value, always return 0 at the end (like in the
controller).
2018-03-07 18:07:01 +01:00
Romain Vimont
42f6341a14 Revert "Enable high dpi support"
Just enabling this flag breaks mouse location values.

This reverts commit 38b56f552e.
2018-03-07 18:06:43 +01:00
Romain Vimont
acd2dc3183 Shutdown sockets before closing
The server socket does not release the port it was listening for if we
just close it: we must also shutdown it.
2018-03-07 18:04:39 +01:00
Romain Vimont
db396f2138 Fix scroll wheel mouse position
SDL_MouseWheelEvent does not provide the mouse location, so we used
SDL_GetMouseState() to retrieve it.

Unfortunately, SDL_GetMouseState() returns a position expressed in the
window coordinate system while the position filled in SDL events are
expressed in the renderer coordinate system. As a consequence, the
scroll was not applied at the right position on the device.

Therefore, convert the coordinate system.

See <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>.
2018-03-07 18:04:39 +01:00
Romain Vimont
e6feb991db Fix comment typo
Replace "at network level" by "at the network level".
2018-03-07 18:04:38 +01:00
Romain Vimont
e3f5d3b49b Double the default bitrate
Set the default video bitrate to 8Mbps. This greatly increase quality on
fast motion, without negative side effects.
2018-03-07 18:04:38 +01:00
Romain Vimont
a7979e4e74 Rename rotation detection method name
The old name checkRotationChanged() did not suggest that the flag was
reset.
2018-03-07 18:04:38 +01:00
Romain Vimont
38b56f552e Enable high dpi support
Use high DPI if available.

Note that on Mac OS X, setting this flag is not sufficient:

> On Apple's OS X you must set the NSHighResolutionCapable Info.plist
> property to YES, otherwise you will not receive a High DPI OpenGL
> display.

<https://wiki.libsdl.org/SDL_CreateWindow#flags>
2018-03-07 18:04:38 +01:00
Romain Vimont
8ace3d1781 Update README
Explain how to build, install and run the application.
2018-03-07 18:04:38 +01:00
39 changed files with 1194 additions and 304 deletions

238
DEVELOP.md Normal file
View File

@@ -0,0 +1,238 @@
# scrcpy for developers
## Overview
This application is composed of two parts:
- the server (`scrcpy-server.jar`), to be executed on the device,
- the client (the `scrcpy` binary), executed on the host computer.
The client is responsible to push the server to the device and start its
execution.
Once the client and the server are connected to each other, the server initially
sends device information (name and initial screen dimensions), then starts to
send a raw H.264 video stream of the device screen. The client decodes the video
frames, and display them as soon as possible, without buffering, to minimize
latency. The client is not aware of the device rotation (which is handled by the
server), it just knows the dimensions of the video frames.
The client captures relevant keyboard and mouse events, that it transmits to the
server, which injects them to the device.
## Server
### Privileges
Capturing the screen requires some privileges, which are granted to `shell`.
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.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run
with:
adb shell CLASSPATH=/data/local/tmp/classes.dex \
app_process / my.package.MainClass
_The path `/data/local/tmp` is a good candidate to push the server, since it's
readable and writable by `shell`, but not world-writable, so a malicious
application may not replace the server just before the client executes it._
Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing
`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle
build system, the server is built to an (unsigned) APK (renamed to
`scrcpy-server.jar`).
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
[apk]: https://en.wikipedia.org/wiki/Android_application_package
### Hidden methods
Although compiled against the Android framework, [hidden] methods and classes are
not directly accessible (and they may differ from one Android version to
another).
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.0/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view
### Threading
The server uses 2 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.
Since the video encoding is typically hardware, there would be no benefit in
encoding and streaming in two different threads.
### Screen video encoding
The encoding is managed by [`ScreenEncoder`].
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.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced.
New frames are produced only when changes occur on the surface. This is good
because it avoids to send unnecessary frames, but there are drawbacks:
- it does not send any frame on start if the device screen does not change,
- after fast motion changes, the last frame may have poor quality.
Both problems are [solved][repeat] by the 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
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection
_Control events_ are received from the client by the [`EventController`] (run in
a separate thread). There are 5 types of input events:
- keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly),
- mouse motion/click,
- mouse scroll,
- custom command (e.g. to switch the screen on).
All of them may need to inject input events to the system. To do so, they use
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
## Client
The client relies on [SDL], which provides cross-platform API for UI, input
events, threading, etc.
The video stream is decoded by [libav] (FFmpeg).
[SDL]: https://www.libsdl.org
[libav]: https://www.libav.org/
### 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.
Note that the client-server roles are expressed at the application level:
- the server _serves_ video stream and handle requests from the client,
- the client _controls_ the device through the server.
However, the roles are inverted at the network level:
- the client opens a server socket and listen on a port before starting the
server,
- the server connects to the client.
This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling.
Once the server is connected, it sends the device information (name and initial
screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available.
To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]).
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
### Threading
The client uses 3 threads:
- the **main** thread, executing the SDL event loop,
- the **decoder** thread, decoding video frames,
- the **controller** thread, sending _control events_ to the server.
### 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:
- the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread.
When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one.
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h
### 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.
On SDL event, received on the main thread, the [input manager][inputmanager]
creates appropriate [_control events_][controlevent]. 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.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
### UI and event loop
Initialization, input events and rendering are all [managed][scrcpy] in the main
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.0/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
## Hack
For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and
contribute ;-)

81
FAQ.md Normal file
View File

@@ -0,0 +1,81 @@
# Frequently Asked Questions
## Common issues
The application is very young, it is not unlikely that you encounter problems
with it.
Here are the common reported problems and their status.
### On Windows, I have no output in the console
When run in `cmd.exe`, the application does not print anything. Even `scrcpy
--help` have no output. We don't know why yet.
However, if you run the very same `scrcpy.exe` from
[MSYS2](https://www.msys2.org/) (`mingw64`), then it correctly prints output.
As a workaround, redirect outputs to files, so that you can read the files
afterwards:
```bash
scrcpy >stdout 2>stderr
type stdout
type stderr
```
_Note that all SDL logs are printed to stderr._
### On Windows, when I start the application, nothing happens
The previous problem does not help to get a clue about the cause.
The most common is your device not being detected by `adb`, or is unauthorized.
Check everything is ok by calling:
adb devices
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
### I get a black screen for some applications like Silence
This is expected, they requested to protect the screen.
In [Silence], you can disable it in settings → Privacy → Screen security.
[silence]: https://f-droid.org/en/packages/org.smssecure.smssecure/
See [issue 36].
[issue 36]: https://github.com/Genymobile/scrcpy/issues/36
### Mouse clicks do not work
On some devices, you may need to enable an option to allow [simulating input].
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
### Mouse clicks at wrong location
On MacOS, with HiDPI support and multiple screens, input location are wrongly
scaled. See [issue 15].
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
A workaround is to build with HiDPI support disabled:
```bash
meson x --buildtype release -Dhidpi_support=false
```
However, the video will be displayed at lower resolution.

365
README.md
View File

@@ -1,141 +1,328 @@
# ScrCpy
# scrcpy
This project displays screens of Android devices plugged on USB in live.
This application provides display and control of Android devices connected on
USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
and _MacOS_.
![screenshot](assets/screenshot-debian-600.jpg)
## Run
## Requirements
### Runtime requirements
The Android part requires at least API 19 (Android 4.4).
This projects requires _FFmpeg_, _LibSDL2_ and _LibSDL2-net_.
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
On Windows, if you use the [prebuilt application](#windows), it is already
included. Otherwise, just download the [platform-tools][platform-tools-windows]
and extract the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
Make sure you [enabled adb debugging][enable-adb] on your device(s).
The client requires [FFmpeg] and [LibSDL2].
[adb]: https://developer.android.com/studio/command-line/adb.html
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
## Build and install
### System-specific steps
#### Linux
Install the packages from your package manager. For example, on Debian:
Install the required packages from your package manager.
sudo apt install ffmpeg libsdl2-2.0.0 libsdl2-net-2.0.0
##### Debian/Ubuntu
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0.0
# client build dependencies
sudo apt install make gcc pkg-config meson \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
# server build dependencies
sudo apt install openjdk-8-jdk
```
##### Fedora
```bash
# enable RPM fusion free
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies
sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies
sudo dnf install java
```
##### Arch Linux
Two [AUR] packages have been created by users:
- [`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
#### Windows
From [MSYS2]:
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
pacman -S mingw-w64-x86_64-SDL2
pacman -S mingw-w64-x86_64-SDL2_net
pacman -S mingw-w64-x86_64-ffmpeg
- [`scrcpy-windows-with-deps-v1.1.zip`][direct-windows-with-deps].
_(SHA-256: 27eb36c15937601d1062c1dc0b45faae0e06fefea2019aadeb4fa7f76a07bb4c)_
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-windows-with-deps-v1.1.zip
_(It's just a portable version including _dll_ copied from MSYS2.)_
Instead, you may want to build it manually. You need [MSYS2] to build the
project. From an MSYS2 terminal, install the required packages:
[MSYS2]: http://www.msys2.org/
```bash
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
#### MacOS
# client build dependencies
pacman -S mingw-w64-x86_64-make \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-meson
```
TODO
Java (>= 7) is not available in MSYS2, so if you plan to build the server,
install it manually and make it available from the `PATH`:
```bash
export PATH="$JAVA_HOME/bin:$PATH"
```
## Build
#### Mac OS
The project is divided into two parts:
- the server, running on the device (in `server/`);
- the client, running on the computer (in `app/`).
Use [Homebrew] to install the packages:
The server is a raw Java project requiring Android SDK. It not an Android
project: the target file is a `.jar`, and a `main()` method is executed with
_shell_ rights.
[Homebrew]: https://brew.sh/
The client is a C project using [SDL] and [FFmpeg], built with [Meson]/[Ninja].
```bash
# runtime dependencies
brew install sdl2 ffmpeg
The root directory contains a `Makefile` to build both parts.
# client build dependencies
brew install pkg-config meson
```
[sdl]: https://www.libsdl.org/
[ffmpeg]: https://www.ffmpeg.org/
[meson]: http://mesonbuild.com/
[ninja]: https://ninja-build.org/
Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it avaliable from the `PATH`:
```bash
brew tap caskroom/versions
brew cask install java8
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
export PATH="$JAVA_HOME/bin:$PATH"
```
### Build requirements
#### Docker
Install the [Android SDK], the JDK 8 (`openjdk-8-jdk`), and the packages
described below.
See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
### Common steps
Install the [Android SDK] (_Android Studio_), and set `ANDROID_HOME` to
its directory. For example:
[Android SDK]: https://developer.android.com/studio/index.html
```bash
export ANDROID_HOME=~/android/sdk
```
#### Linux
Clone the project:
sudo apt install make gcc openjdk-8-jdk pkg-config meson zip \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev libsdl2-net-dev
```bash
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
```
Then, build:
```bash
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
```
You can test it from here:
```bash
ninja run
```
Or you can install it on the system:
```bash
sudo ninja install # without sudo on Windows
```
This installs two files:
- `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server.jar`
Just remove them to "uninstall" the application.
#### Windows
#### Prebuilt server
Install these packages:
Since the server binary, that will be pushed to the Android device, does not
depend on your system and architecture, you may want to use the prebuilt binary
instead:
pacman -S mingw-w64-x86_64-make
pacman -S mingw-w64-x86_64-gcc
pacman -S mingw-w64-x86_64-pkg-config
pacman -S mingw-w64-x86_64-meson
pacman -S zip
- [`scrcpy-server-v1.1.jar`][direct-scrcpy-server].
_(SHA-256: 14826512bf38447ec94adf3b531676ce038d19e7e06757fb4e537882b17e77b3)_
Java 8 is not available in MSYS2, so install it manually and make it available
from the `PATH`:
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-server-v1.1.jar
export PATH="$JAVA_HOME/bin:$PATH"
In that case, the build does not require Java or the Android SDK.
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
### Build
Make sure your `ANDROID_HOME` variable is set to your Android SDK directory:
export ANDROID_HOME=~/android/sdk
From the project root directory, execute:
make build
To run the build:
make run
It is also pass arguments to `scrcpy` via `make`:
make run ARGS="-p 1234"
The purpose of this command is to execute `scrcpy` during the development.
### Test
To execute unit tests:
make test
The server-side tests require JUnit 4:
sudo apt install junit4
### Generate a release
From the project root directory, execute:
make release
This will generate the application in `dist/scrcpy/`.
```bash
meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar
cd x
ninja
sudo ninja install
```
## Run
Plug a device, and from `dist/scrcpy/`, execute:
_At runtime, `adb` must be accessible from your `PATH`._
./scrcpy
If everything is ok, just plug an Android device, and execute:
```bash
scrcpy
```
It accepts command-line arguments, listed by:
```bash
scrcpy --help
```
For example, to decrease video bitrate to 2Mbps (default is 8Mbps):
```bash
scrcpy -b 2M
```
To limit the video dimensions (e.g. if the device is 2540×1440, but the host
screen is smaller, or cannot decode such a high definition):
```bash
scrcpy -m 1024
```
If several devices are listed in `adb devices`, you must specify the _serial_:
./scrcpy 0123456789abcdef
```bash
scrcpy -s 0123456789abcdef
```
To change the default port (useful to launch several `scrcpy` simultaneously):
To show physical touches while scrcpy is running:
./scrcpy -p 1234
```bash
scrcpy -t
```
Other options are available, check `scrcpy --help`.
To run without installing:
```bash
./run x [options]
```
(where `x` is your build directory).
## Shortcuts
| Action | Shortcut |
| -------------------------------------- |:---------------------------- |
| switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| click on `APP_SWITCH` | `Ctrl`+`s` |
| click on `MENU` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_DOWN` | `Ctrl`+`-` |
| click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click²_ |
| paste computer clipboard to device | `Ctrl`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
## Why _scrcpy_?
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
[`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een.
[gnirehtet]: https://github.com/Genymobile/gnirehtet
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
## Common issues
See the [FAQ](FAQ.md).
## Developers
Read the [developers page].
[developers page]: DEVELOP.md
## Licence
Copyright (C) 2018 Genymobile
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## Articles
- [Introducing scrcpy](https://blog.rom1v.com/2018/03/introducing-scrcpy/)
- [Scrcpy now works wirelessly](https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/)

View File

@@ -42,7 +42,7 @@ conf = configuration_data()
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
# the version, updated on release
conf.set_quoted('SCRCPY_VERSION', '0.1')
conf.set_quoted('SCRCPY_VERSION', '1.1')
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
@@ -73,16 +73,20 @@ conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# the default video bitrate, in bits/second
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '4000000') # 4Mbps
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'))
configure_file(configuration: conf, output: 'config.h')
executable('scrcpy', src, dependencies: dependencies, install: true)
src_dir = include_directories('src')
executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true)
### TESTS
@@ -93,8 +97,6 @@ tests = [
['test_strutil', ['tests/test_strutil.c', 'src/strutil.c']],
]
src_dir = include_directories('src')
foreach t : tests
exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies)
test(t[0], exe)

View File

@@ -4,10 +4,9 @@
#include <stdlib.h>
#include <string.h>
#include "common.h"
#include "log.h"
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
static const char *adb_command;
static inline const char *get_adb_command() {
@@ -45,6 +44,13 @@ process_t adb_forward(const char *serial, uint16_t local_port, const char *devic
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_forward_remove(const char *serial, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port);
const char *const adb_cmd[] = {"forward", "--remove", local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME

View File

@@ -7,12 +7,11 @@
// <https://stackoverflow.com/a/44383330/1987178>
#ifdef _WIN32
# define PRIexitcode "lu"
# ifdef _WIN64
# define PRIsizet PRIu64
# define PRIexitcode "lu"
# else
# define PRIsizet PRIu32
# define PRIexitcode "u"
# endif
#else
# define PRIsizet "zu"
@@ -39,6 +38,7 @@ SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
process_t adb_forward_remove(const char *serial, uint16_t local_port);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t adb_push(const char *serial, const char *local, const char *remote);

View File

@@ -3,6 +3,7 @@
#include <SDL2/SDL_stdinc.h>
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)

View File

@@ -36,11 +36,12 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
// write length (1 byte) + date (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;
}
buf[1] = (Uint8) len;
memcpy(&buf[2], &event->text_event.text, len);
return 2 + len;
write16(&buf[1], (Uint16) len);
memcpy(&buf[3], event->text_event.text, len);
return 3 + len;
}
case CONTROL_EVENT_TYPE_MOUSE:
buf[1] = event->mouse_event.action;
@@ -61,6 +62,12 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
}
}
void control_event_destroy(struct control_event *event) {
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
SDL_free(event->text_event.text);
}
}
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
return queue->head == queue->tail;
}
@@ -77,7 +84,11 @@ SDL_bool control_event_queue_init(struct control_event_queue *queue) {
}
void control_event_queue_destroy(struct control_event_queue *queue) {
// nothing to do in the current implementation
int i = queue->tail;
while (i != queue->head) {
control_event_destroy(&queue->data[i]);
i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE;
}
}
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {

View File

@@ -9,8 +9,8 @@
#include "common.h"
#define CONTROL_EVENT_QUEUE_SIZE 64
#define SERIALIZED_EVENT_MAX_SIZE 33
#define TEXT_MAX_LENGTH 31
#define TEXT_MAX_LENGTH 300
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE,
@@ -20,7 +20,7 @@ enum control_event_type {
CONTROL_EVENT_TYPE_COMMAND,
};
#define CONTROL_EVENT_COMMAND_SCREEN_ON 0
#define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0
struct control_event {
enum control_event_type type;
@@ -31,7 +31,7 @@ struct control_event {
enum android_metastate metastate;
} keycode_event;
struct {
char text[TEXT_MAX_LENGTH + 1]; // nul-terminated string
char *text; // owned, to be freed by SDL_free()
} text_event;
struct {
enum android_motionevent_action action;
@@ -68,4 +68,6 @@ SDL_bool control_event_queue_is_full(const struct control_event_queue *queue);
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event);
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event);
void control_event_destroy(struct control_event *event);
#endif

View File

@@ -65,7 +65,9 @@ static int run_controller(void *data) {
}
struct control_event event;
while (control_event_queue_take(&controller->queue, &event)) {
if (!process_event(controller, &event)) {
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
goto end;
}

View File

@@ -145,8 +145,8 @@ SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
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 = (Uint16) from->x;
to->mouse_event.position.point.y = (Uint16) from->y;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
return SDL_TRUE;
}
@@ -172,7 +172,10 @@ SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
to->scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
to->scroll_event.hscroll = mul * from->x;
// 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;
return SDL_TRUE;

View File

@@ -40,37 +40,33 @@ static void notify_stopped(void) {
static int run_decoder(void *data) {
struct decoder *decoder = data;
int ret = 0;
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
return -1;
goto run_end;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOGC("Could not allocate decoder context");
return -1;
goto run_end;
}
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Could not open H.264 codec");
ret = -1;
goto run_finally_free_codec_ctx;
}
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
ret = -1;
goto run_finally_close_codec;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
ret = -1;
goto run_finally_free_format_ctx;
}
@@ -80,7 +76,6 @@ static int run_decoder(void *data) {
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
ret = -1;
goto run_finally_free_format_ctx;
}
@@ -88,7 +83,6 @@ static int run_decoder(void *data) {
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
ret = -1;
goto run_finally_free_avio_ctx;
}
@@ -97,10 +91,25 @@ static int run_decoder(void *data) {
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet) && !avio_ctx->eof_reached) {
while (!av_read_frame(format_ctx, &packet)) {
// the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 37, 0)
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0)
int ret;
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
goto run_quit;
}
ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame);
if (!ret) {
// a frame was received
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
av_packet_unref(&packet);
goto run_quit;
}
#else
while (packet.size > 0) {
int got_picture;
int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet);
@@ -114,19 +123,12 @@ static int run_decoder(void *data) {
packet.size -= len;
packet.data += len;
}
#else
int ret;
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
goto run_quit;
}
if ((ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame)) < 0) {
LOGE("Could not receive video frame: %d", ret);
goto run_quit;
}
push_frame(decoder);
#endif
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
break;
}
}
LOGD("End of frames");
@@ -142,7 +144,8 @@ run_finally_close_codec:
run_finally_free_codec_ctx:
avcodec_free_context(&codec_ctx);
notify_stopped();
return ret;
run_end:
return 0;
}
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) {

View File

@@ -4,10 +4,24 @@
#include "lockutil.h"
#include "log.h"
static struct point get_mouse_point(void) {
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer coordinates (as provided in SDL mouse events)
//
// See my question:
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
static void convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
SDL_Rect viewport;
float scale_x, scale_y;
SDL_RenderGetViewport(renderer, &viewport);
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
*x = (int) (*x / scale_x) - viewport.x;
*y = (int) (*y / scale_y) - viewport.y;
}
static struct point get_mouse_point(struct screen *screen) {
int x;
int y;
SDL_GetMouseState(&x, &y);
convert_to_renderer_coordinates(screen->renderer, &x, &y);
SDL_assert_release(x >= 0 && x < 0x10000 && y >= 0 && y < 0x10000);
return (struct point) {
.x = (Uint16) x,
@@ -22,14 +36,11 @@ static SDL_bool is_ctrl_down(void) {
static void send_keycode(struct controller *controller, enum android_keycode keycode, const char *name) {
// send DOWN event
struct control_event control_event = {
.type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = {
.action = AKEY_EVENT_ACTION_DOWN,
.keycode = keycode,
.metastate = 0,
},
};
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
@@ -67,13 +78,16 @@ static inline void action_volume_down(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN");
}
static void turn_screen_on(struct controller *controller) {
struct control_event control_event = {
.type = CONTROL_EVENT_TYPE_COMMAND,
.command_event = {
.action = CONTROL_EVENT_COMMAND_SCREEN_ON,
},
};
static inline void action_menu(struct controller *controller) {
send_keycode(controller, AKEYCODE_MENU, "MENU");
}
// turn the screen on if it was off, press BACK otherwise
static void press_back_or_turn_screen_on(struct controller *controller) {
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action = CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on");
}
@@ -91,6 +105,27 @@ static void switch_fps_counter_state(struct frames *frames) {
mutex_unlock(frames->mutex);
}
static void clipboard_paste(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_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
control_event.text_event.text = text;
if (!controller_push_event(controller, &control_event)) {
SDL_free(text);
LOGW("Cannot send clipboard paste event");
}
}
void input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event) {
if (is_ctrl_down()) {
@@ -107,8 +142,11 @@ void input_manager_process_text_input(struct input_manager *input_manager,
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
strncpy(control_event.text_event.text, event->text, TEXT_MAX_LENGTH);
control_event.text_event.text[TEXT_MAX_LENGTH] = '\0';
control_event.text_event.text = SDL_strdup(event->text);
if (!control_event.text_event.text) {
LOGW("Cannot strdup input text");
return;
}
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send text event");
}
@@ -116,23 +154,24 @@ void input_manager_process_text_input(struct input_manager *input_manager,
void input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event) {
SDL_Keycode keycode = event->keysym.sym;
SDL_bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
SDL_bool repeat = event->repeat;
// capture all Ctrl events
if (ctrl) {
SDL_bool repeat = event->repeat;
// only consider keydown events, and ignore repeated events
if (repeat || event->type != SDL_KEYDOWN) {
return;
}
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (shift) {
// currently, there is no shortcut implying SHIFT
return;
}
SDL_Keycode keycode = event->keysym.sym;
switch (keycode) {
case SDLK_h:
action_home(input_manager->controller);
@@ -141,12 +180,18 @@ void input_manager_process_key(struct input_manager *input_manager,
case SDLK_BACKSPACE:
action_back(input_manager->controller);
return;
case SDLK_m:
case SDLK_s:
action_app_switch(input_manager->controller);
return;
case SDLK_m:
action_menu(input_manager->controller);
return;
case SDLK_p:
action_power(input_manager->controller);
return;
case SDLK_v:
clipboard_paste(input_manager->controller);
return;
case SDLK_f:
screen_switch_fullscreen(input_manager->screen);
return;
@@ -188,9 +233,26 @@ void input_manager_process_mouse_motion(struct input_manager *input_manager,
void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event) {
if (event->button == SDL_BUTTON_RIGHT && event->type == SDL_MOUSEBUTTONDOWN) {
turn_screen_on(input_manager->controller);
return;
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
return;
}
if (event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
SDL_bool outside_device_screen =
event->x < 0 || event->x >= input_manager->screen->frame_size.width ||
event->y < 0 || event->y >= input_manager->screen->frame_size.height;
if (outside_device_screen) {
screen_resize_to_fit(input_manager->screen);
return;
}
// otherwise, send the click event to the device
}
};
struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
@@ -204,12 +266,12 @@ void input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event) {
struct position position = {
.screen_size = input_manager->screen->frame_size,
.point = get_mouse_point(),
.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 wheel button event");
LOGW("Cannot send mouse wheel event");
}
}
}

View File

@@ -12,6 +12,7 @@ struct args {
const char *serial;
SDL_bool help;
SDL_bool version;
SDL_bool show_touches;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
@@ -45,6 +46,10 @@ static void usage(const char *arg0) {
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\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"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
@@ -57,19 +62,25 @@ static void usage(const char *arg0) {
" resize window to 1:1 (pixel-perfect)\n"
"\n"
" Ctrl+x\n"
" Double-click on black borders\n"
" resize window to remove black borders\n"
"\n"
" Ctrl+h\n"
" Home\n"
" Middle-click\n"
" click on HOME\n"
"\n"
" Ctrl+b\n"
" Ctrl+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n"
"\n"
" Ctrl+m\n"
" Ctrl+s\n"
" click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" click on MENU\n"
"\n"
" Ctrl+'+'\n"
" click on VOLUME_UP\n"
"\n"
@@ -79,9 +90,12 @@ static void usage(const char *arg0) {
" Ctrl+p\n"
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click\n"
" Right-click (when screen is off)\n"
" turn screen on\n"
"\n"
" Ctrl+v\n"
" paste computer clipboard to device\n"
"\n"
" Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n",
@@ -174,47 +188,45 @@ static SDL_bool parse_port(char *optarg, Uint16 *port) {
static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"bit-rate", required_argument, NULL, 'b'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'},
{"serial", required_argument, NULL, 's'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
{"bit-rate", required_argument, NULL, 'b'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "b:hm:p:s:v", long_options, NULL)) != -1) {
while ((c = getopt_long(argc, argv, "b:hm:p:s:tv", long_options, NULL)) != -1) {
switch (c) {
case 'b': {
case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) {
return SDL_FALSE;
}
break;
}
case 'h': {
case 'h':
args->help = SDL_TRUE;
break;
}
case 'm': {
case 'm':
if (!parse_max_size(optarg, &args->max_size)) {
return SDL_FALSE;
}
break;
}
case 'p': {
case 'p':
if (!parse_port(optarg, &args->port)) {
return SDL_FALSE;
}
break;
}
case 's': {
case 's':
args->serial = optarg;
break;
}
case 'v': {
case 't':
args->show_touches = SDL_TRUE;
break;
case 'v':
args->version = SDL_TRUE;
break;
}
default:
// getopt prints the error message on stderr
return SDL_FALSE;
@@ -230,10 +242,17 @@ static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
}
int main(int argc, char *argv[]) {
#ifdef __WINDOWS__
// disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL);
setbuf(stderr, NULL);
#endif
struct args args = {
.serial = NULL,
.help = SDL_FALSE,
.version = SDL_FALSE,
.show_touches = SDL_FALSE,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
@@ -262,7 +281,14 @@ int main(int argc, char *argv[]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
int res = scrcpy(args.serial, args.port, args.max_size, args.bit_rate) ? 0 : 1;
struct scrcpy_options options = {
.serial = args.serial,
.port = args.port,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
};
int res = scrcpy(&options) ? 0 : 1;
avformat_network_deinit(); // ignore failure

View File

@@ -18,6 +18,26 @@
typedef struct in_addr IN_ADDR;
#endif
socket_t net_connect(Uint32 addr, Uint16 port) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
return INVALID_SOCKET;
}
SOCKADDR_IN sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(addr);
sin.sin_port = htons(port);
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("connect");
return INVALID_SOCKET;
}
return sock;
}
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {

View File

@@ -9,8 +9,6 @@
#define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH
typedef SIZE_T size_t;
typedef SSIZE_T ssize_t;
typedef SOCKET socket_t;
#else
# include <sys/socket.h>
@@ -21,6 +19,7 @@
SDL_bool net_init(void);
void net_cleanup(void);
socket_t net_connect(Uint32 addr, Uint16 port);
socket_t net_listen(Uint32 addr, Uint16 port, int backlog);
socket_t net_accept(socket_t server_socket);

View File

@@ -35,7 +35,29 @@ static struct input_manager input_manager = {
.screen = &screen,
};
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
#ifdef CONTINUOUS_RESIZING_WORKAROUND
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
// not triggered. As a workaround, handle them in an event handler.
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static int event_watcher(void *data, SDL_Event *event) {
if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) {
// called from another thread, not very safe, but it's a workaround!
screen_render(&screen);
}
return 0;
}
#endif
static void event_loop(void) {
#ifdef CONTINUOUS_RESIZING_WORKAROUND
SDL_AddEventWatch(event_watcher, NULL);
#endif
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
@@ -57,16 +79,15 @@ static void event_loop(void) {
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
break;
case SDL_TEXTINPUT: {
case SDL_TEXTINPUT:
input_manager_process_text_input(&input_manager, &event.text);
break;
}
case SDL_KEYDOWN:
case SDL_KEYUP:
input_manager_process_key(&input_manager, &event.key);
@@ -74,38 +95,58 @@ static void event_loop(void) {
case SDL_MOUSEMOTION:
input_manager_process_mouse_motion(&input_manager, &event.motion);
break;
case SDL_MOUSEWHEEL: {
case SDL_MOUSEWHEEL:
input_manager_process_mouse_wheel(&input_manager, &event.wheel);
break;
}
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP: {
case SDL_MOUSEBUTTONUP:
input_manager_process_mouse_button(&input_manager, &event.button);
break;
}
}
}
}
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate) {
if (!server_start(&server, serial, local_port, max_size, bit_rate)) {
static process_t set_show_touches_enabled(const char *serial, SDL_bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
SDL_bool scrcpy(const struct scrcpy_options *options) {
if (!server_start(&server, options->serial, options->port,
options->max_size, options->bit_rate)) {
return SDL_FALSE;
}
process_t proc_show_touches;
SDL_bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
show_touches_waited = SDL_FALSE;
}
SDL_bool ret = SDL_TRUE;
if (!SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1")) {
LOGW("Cannot request to keep default signal handlers");
}
if (!sdl_init_and_configure()) {
ret = SDL_FALSE;
goto finally_destroy_server;
}
// SDL initialization replace the signal handler for SIGTERM, so Ctrl+C is
// managed by the event loop. This blocking call blocks the event loop, so
// timeout the connection not to block indefinitely in case of SIGTERM.
#define SERVER_CONNECT_TIMEOUT_MS 2000
socket_t device_socket = server_connect_to(&server, serial, SERVER_CONNECT_TIMEOUT_MS);
socket_t device_socket = server_connect_to(&server);
if (device_socket == INVALID_SOCKET) {
server_stop(&server, serial);
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
@@ -117,13 +158,13 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// 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, serial);
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
if (!frames_init(&frames)) {
server_stop(&server, serial);
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
}
@@ -134,7 +175,7 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// start the decoder
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server, serial);
server_stop(&server);
goto finally_destroy_frames;
}
@@ -153,10 +194,16 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
goto finally_stop_and_join_controller;
}
event_loop();
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = SDL_TRUE;
}
event_loop();
LOGD("quit...");
screen_destroy(&screen);
finally_stop_and_join_controller:
controller_stop(&controller);
controller_join(&controller);
@@ -165,11 +212,21 @@ finally_destroy_controller:
finally_stop_decoder:
decoder_stop(&decoder);
// stop the server before decoder_join() to wake up the decoder
server_stop(&server, serial);
server_stop(&server);
decoder_join(&decoder);
finally_destroy_frames:
frames_destroy(&frames);
finally_destroy_server:
if (options->show_touches) {
if (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_FALSE);
wait_show_touches(proc_show_touches);
}
server_destroy(&server);
return ret;

View File

@@ -3,6 +3,14 @@
#include <SDL2/SDL_stdinc.h>
SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 bit_rate);
struct scrcpy_options {
const char *serial;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
SDL_bool show_touches;
};
SDL_bool scrcpy(const struct scrcpy_options *options);
#endif

View File

@@ -18,8 +18,8 @@ SDL_bool sdl_init_and_configure(void) {
atexit(SDL_Quit);
// Bilinear resizing
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
// Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
LOGW("Could not enable bilinear filtering");
}
@@ -136,12 +136,21 @@ void screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER;
}
static inline SDL_Texture *create_texture(SDL_Renderer *renderer, struct size frame_size) {
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
}
SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, struct size frame_size) {
screen->frame_size = frame_size;
struct size window_size = get_initial_optimal_size(frame_size);
Uint32 window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height, SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE);
window_size.width, window_size.height, window_flags);
if (!screen->window) {
LOGC("Could not create window: %s", SDL_GetError());
return SDL_FALSE;
@@ -170,8 +179,7 @@ SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, s
SDL_FreeSurface(icon);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height);
screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
screen->texture = create_texture(screen->renderer, frame_size);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen);
@@ -220,8 +228,7 @@ static SDL_bool prepare_for_frame(struct screen *screen, struct size new_frame_s
LOGD("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height);
screen->texture = SDL_CreateTexture(screen->renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
new_frame_size.width, new_frame_size.height);
screen->texture = create_texture(screen->renderer, new_frame_size);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
return SDL_FALSE;

View File

@@ -4,6 +4,7 @@
#include <inttypes.h>
#include <stdint.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h"
#include "log.h"
@@ -37,17 +38,45 @@ static SDL_bool remove_server(const char *serial) {
return process_check_success(process, "adb shell rm");
}
static SDL_bool enable_tunnel(const char *serial, Uint16 local_port) {
static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse");
}
static SDL_bool disable_tunnel(const char *serial) {
static SDL_bool disable_tunnel_reverse(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove");
}
static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate) {
static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward");
}
static SDL_bool disable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward_remove(serial, local_port);
return process_check_success(process, "adb forward --remove");
}
static SDL_bool enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return SDL_TRUE;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = SDL_TRUE;
return enable_tunnel_forward(server->serial, server->local_port);
}
static SDL_bool disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
return disable_tunnel_forward(server->serial, server->local_port);
}
return disable_tunnel_reverse(server->serial);
}
static process_t execute_server(const char *serial,
Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) {
char max_size_string[6];
char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size);
@@ -60,17 +89,51 @@ static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_
"com.genymobile.scrcpy.Server",
max_size_string,
bit_rate_string,
tunnel_forward ? "true" : "false",
};
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
static socket_t listen_on_port(Uint16 port) {
#define IPV4_LOCALHOST 0x7F000001
static socket_t listen_on_port(Uint16 port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t connect_and_read_byte(Uint16 port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
if (socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
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) {
// the server is not listening yet behind the adb tunnel
return INVALID_SOCKET;
}
return socket;
}
static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) {
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
socket_t socket = connect_and_read_byte(port);
if (socket != INVALID_SOCKET) {
// it worked!
return socket;
}
if (attempts) {
SDL_Delay(delay);
}
} while (--attempts > 0);
return INVALID_SOCKET;
}
static void close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
LOGW("Cannot close socket");
return;
@@ -84,63 +147,84 @@ void server_init(struct server *server) {
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate) {
server->local_port = local_port;
if (serial) {
server->serial = SDL_strdup(serial);
}
if (!push_server(serial)) {
return SDL_FALSE;
}
server->server_copied_to_device = SDL_TRUE;
if (!enable_tunnel(serial, local_port)) {
if (!enable_tunnel(server)) {
return SDL_FALSE;
}
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at network level, the client
// listens and the server connects to the client. That way, the client can
// listen before starting the server app, so there is no need to try to
// connect until the server socket is listening on the device.
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!server->tunnel_forward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(serial);
return SDL_FALSE;
server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
return SDL_FALSE;
}
}
// server will connect to our server socket
server->process = execute_server(serial, max_size, bit_rate);
server->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward);
if (server->process == PROCESS_NONE) {
close_socket(&server->server_socket);
disable_tunnel(serial);
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
return SDL_FALSE;
}
server->adb_reverse_enabled = SDL_TRUE;
server->tunnel_enabled = SDL_TRUE;
return SDL_TRUE;
}
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms) {
server->device_socket = net_accept(server->server_socket);
socket_t server_connect_to(struct server *server) {
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
Uint32 attempts = 50;
Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay);
}
if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
// we don't need the server socket anymore
close_socket(&server->server_socket);
if (!server->tunnel_forward) {
// we don't need the server socket anymore
close_socket(&server->server_socket);
}
// the server is started, we can clean up the jar from the temporary folder
remove_server(serial); // ignore failure
remove_server(server->serial); // ignore failure
server->server_copied_to_device = SDL_FALSE;
// we don't need the adb tunnel anymore
disable_tunnel(serial); // ignore failure
server->adb_reverse_enabled = SDL_FALSE;
disable_tunnel(server); // ignore failure
server->tunnel_enabled = SDL_FALSE;
return server->device_socket;
}
void server_stop(struct server *server, const char *serial) {
void server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
@@ -150,13 +234,13 @@ void server_stop(struct server *server, const char *serial) {
cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated");
if (server->adb_reverse_enabled) {
if (server->tunnel_enabled) {
// ignore failure
disable_tunnel(serial);
disable_tunnel(server);
}
if (server->server_copied_to_device) {
remove_server(serial); // ignore failure
remove_server(server->serial); // ignore failure
}
}
@@ -167,4 +251,5 @@ void server_destroy(struct server *server) {
if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket);
}
SDL_free((void *) server->serial);
}

View File

@@ -5,18 +5,24 @@
#include "net.h"
struct server {
const char *serial;
process_t process;
socket_t server_socket;
socket_t server_socket; // only used if !tunnel_forward
socket_t device_socket;
SDL_bool adb_reverse_enabled;
Uint16 local_port;
SDL_bool tunnel_enabled;
SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse"
SDL_bool server_copied_to_device;
};
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \
.device_socket = INVALID_SOCKET, \
.adb_reverse_enabled = SDL_FALSE, \
.local_port = 0, \
.tunnel_enabled = SDL_FALSE, \
.tunnel_forward = SDL_FALSE, \
.server_copied_to_device = SDL_FALSE, \
}
@@ -28,10 +34,10 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po
Uint16 max_size, Uint32 bit_rate);
// block until the communication with the server is established
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms);
socket_t server_connect_to(struct server *server);
// disconnect and kill the server process
void server_stop(struct server *server, const char *serial);
void server_stop(struct server *server);
// close and release sockets
void server_destroy(struct server *server);

View File

@@ -1,4 +1,4 @@
#include "../../command.h"
#include "command.h"
#include <signal.h>
#include <sys/types.h>

View File

@@ -1,4 +1,4 @@
#include "../../net.h"
#include "net.h"
# include <unistd.h>

View File

@@ -1,7 +1,7 @@
#include "../../command.h"
#include "command.h"
#include "../../log.h"
#include "../../strutil.h"
#include "log.h"
#include "strutil.h"
HANDLE cmd_execute(const char *path, const char *const argv[]) {
STARTUPINFO si;

View File

@@ -1,6 +1,6 @@
#include "../../net.h"
#include "net.h"
#include "../../log.h"
#include "log.h"
SDL_bool net_init(void) {
WSADATA wsa;

View File

@@ -35,16 +35,36 @@ static void test_serialize_text_event() {
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 15);
assert(size == 16);
const unsigned char expected[] = {
0x01, // CONTROL_EVENT_TYPE_KEYCODE
0x0d, // text length
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() {
struct control_event event;
event.type = CONTROL_EVENT_TYPE_TEXT;
char text[TEXT_MAX_LENGTH];
memset(text, 'a', sizeof(text));
event.text_event.text = text;
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 3 + sizeof(text));
unsigned char expected[3 + TEXT_MAX_LENGTH];
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE
expected[1] = 0x01;
expected[2] = 0x2c; // text length (16 bits)
memset(&expected[3], 'a', TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_mouse_event() {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_MOUSE,
@@ -114,6 +134,7 @@ static void test_serialize_scroll_event() {
int main() {
test_serialize_keycode_event();
test_serialize_text_event();
test_serialize_long_text_event();
test_serialize_mouse_event();
test_serialize_scroll_event();
return 0;

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -3,3 +3,4 @@ option('build_server', type: 'boolean', value: true, description: 'Build the ser
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('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')

View File

@@ -4,10 +4,10 @@ android {
compileSdkVersion 27
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
versionCode 2
versionName "1.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@@ -11,7 +11,7 @@ public final class ControlEvent {
public static final int TYPE_SCROLL = 3;
public static final int TYPE_COMMAND = 4;
public static final int COMMAND_SCREEN_ON = 0;
public static final int COMMAND_BACK_OR_SCREEN_ON = 0;
private int type;
private String text;

View File

@@ -13,12 +13,12 @@ public class ControlEventReader {
private static final int SCROLL_PAYLOAD_LENGTH = 16;
private static final int COMMAND_PAYLOAD_LENGTH = 1;
private static final int MAX_TEXT_LENGTH = 32;
private static final int RAW_BUFFER_SIZE = 128;
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[MAX_TEXT_LENGTH];
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH];
public ControlEventReader() {
// invariant: the buffer is always in "get" mode
@@ -94,7 +94,7 @@ public class ControlEventReader {
if (buffer.remaining() < 1) {
return null;
}
int len = toUnsigned(buffer.get());
int len = toUnsigned(buffer.getShort());
if (buffer.remaining() < len) {
return null;
}

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
@@ -33,8 +34,24 @@ public final class DesktopConnection implements Closeable {
return localSocket;
}
public static DesktopConnection open(Device device) throws IOException {
LocalSocket socket = connect(SOCKET_NAME);
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;
if (tunnelForward) {
socket = listenAndAccept(SOCKET_NAME);
// send one byte so the client may read() to detect a connection error
socket.getOutputStream().write(0);
} else {
socket = connect(SOCKET_NAME);
}
DesktopConnection connection = new DesktopConnection(socket);
Size videoSize = device.getScreenInfo().getVideoSize();

View File

@@ -50,8 +50,8 @@ public final class Device {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
int w = deviceSize.getWidth();
int h = deviceSize.getHeight();
int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8
int h = deviceSize.getHeight() & ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");

View File

@@ -39,10 +39,6 @@ public class EventController {
coords.orientation = 0;
coords.pressure = 1;
coords.size = 1;
coords.toolMajor = 1;
coords.toolMinor = 1;
coords.touchMajor = 1;
coords.touchMinor = 1;
}
private void setPointerCoords(Point point) {
@@ -93,14 +89,12 @@ public class EventController {
return injectKeyEvent(action, keycode, 0, metaState);
}
private boolean injectText(String text) {
return injectText(text, true);
}
private boolean injectText(String text, boolean decomposeOnFailure) {
KeyEvent[] events = charMap.getEvents(text.toCharArray());
private boolean injectChar(char c) {
String decomposed = KeyComposition.decompose(c);
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c};
KeyEvent[] events = charMap.getEvents(chars);
if (events == null) {
return decomposeOnFailure ? injectDecomposition(text) : false;
return false;
}
for (KeyEvent event : events) {
if (!injectEvent(event)) {
@@ -110,10 +104,9 @@ public class EventController {
return true;
}
private boolean injectDecomposition(String text) {
private boolean injectText(String text) {
for (char c : text.toCharArray()) {
String composedText = KeyComposition.decompose(c);
if (composedText == null || !injectText(composedText, false)) {
if (!injectChar(c)) {
return false;
}
}
@@ -170,10 +163,15 @@ public class EventController {
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_SCREEN_ON:
return turnScreenOn();
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
return pressBackOrTurnScreenOn();
default:
Ln.w("Unsupported command: " + action);
}

View File

@@ -11,7 +11,7 @@ import java.util.Map;
* This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])}
* KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
* <p>
* See <a href="https://source.android.com/devices/input/key-character-map-files#key-declarations">diacritical dead key characters</a>.
* See <a href="https://source.android.com/devices/input/key-character-map-files#behaviors">diacritical dead key characters</a>.
*/
public final class KeyComposition {

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
public class Options {
private int maxSize;
private int bitRate;
private boolean tunnelForward;
public int getMaxSize() {
return maxSize;
@@ -19,4 +20,12 @@ public class Options {
public void setBitRate(int bitRate) {
this.bitRate = bitRate;
}
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
}

View File

@@ -6,6 +6,7 @@ import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
@@ -16,7 +17,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_BIT_RATE = 4_000_000; // bits per second
private static final int DEFAULT_FRAME_RATE = 60; // fps
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
@@ -40,16 +40,12 @@ public class ScreenEncoder implements Device.RotationListener {
this(bitRate, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
}
public ScreenEncoder() {
this(DEFAULT_BIT_RATE, DEFAULT_FRAME_RATE, DEFAULT_I_FRAME_INTERVAL);
}
@Override
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
}
public boolean checkRotationChanged() {
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
@@ -82,21 +78,32 @@ public class ScreenEncoder implements Device.RotationListener {
}
}
@SuppressWarnings("deprecation") // Android API 19 requires to call deprecated methods
private boolean encode(MediaCodec codec, OutputStream outputStream) throws IOException {
@SuppressWarnings("checkstyle:MagicNumber")
byte[] buf = new byte[bitRate / 8]; // may contain up to 1 second of video
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!checkRotationChanged() && !eof) {
ByteBuffer[] outputBuffers = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
outputBuffers = codec.getOutputBuffers();
}
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
if (checkRotationChanged()) {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = codec.getOutputBuffer(outputBufferId);
} else {
outputBuffer = outputBuffers[outputBufferId];
}
while (outputBuffer.hasRemaining()) {
int remaining = outputBuffer.remaining();
int len = Math.min(buf.length, remaining);
@@ -105,6 +112,8 @@ public class ScreenEncoder implements Device.RotationListener {
outputBuffer.get(buf, 0, len);
outputStream.write(buf, 0, len);
}
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
}
} finally {
if (outputBufferId >= 0) {

View File

@@ -10,7 +10,8 @@ public final class Server {
private static void scrcpy(Options options) throws IOException {
final Device device = new Device(options);
try (DesktopConnection connection = DesktopConnection.open(device)) {
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
// asynchronous
@@ -55,6 +56,13 @@ public final class Server {
int bitRate = Integer.parseInt(args[1]);
options.setBitRate(bitRate);
if (args.length < 3) {
return options;
}
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward);
return options;
}

View File

@@ -3,14 +3,15 @@ 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;
import org.junit.Assert;
import org.junit.Test;
public class ControlEventReaderTest {
@@ -43,8 +44,8 @@ public class ControlEventReaderTest {
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeByte(text.length);
dos.write("testé".getBytes(StandardCharsets.UTF_8));
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
@@ -54,6 +55,26 @@ public class ControlEventReaderTest {
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();