Compare commits

...

154 Commits

Author SHA1 Message Date
d113b6ef61 buffered_reader
Not merged: it has not visible impact on CPU usage.
2019-07-31 15:56:31 +02:00
6abb4902c6 Log recording failure
If recording fails, log "recording failed" instead of "recording
complete".
2019-07-31 11:04:38 +02:00
d4ed8b6f26 Log scrcpy version and URL on start
Keep --version which also print the version of dependencies.
2019-07-31 01:55:43 +02:00
35d9185f6c Record asynchronously
The record file was written from the stream thread. As a consequence,
any blocking I/O to write the file delayed the decoder.

For maximum performance even when recording is enabled, send
(refcounted) packets to a separate recording thread.
2019-07-31 01:55:40 +02:00
63af7fbafe Reduce latency by 1 frame
To packetize the H.264 raw stream, av_parser_parse2() (called by
av_read_frame()) knows that it has received a full frame only after it
has received some data for the next frame. As a consequence, the client
always waited until the next frame before sending the current frame to
the decoder!

On the device side, we know packets boundaries. To reduce latency,
make the device always transmit the "frame meta" to packetize the stream
manually (it was already implemented to send PTS, but only enabled on
recording).

On the client side, replace av_read_frame() by manual packetizing and
parsing.

<https://stackoverflow.com/questions/50682518/replacing-av-read-frame-to-reduce-delay>
<https://trac.ffmpeg.org/ticket/3354>
2019-07-31 01:55:32 +02:00
a90ccbdf3b Add option to change the push target
A drag & drop always pushed the file to /sdcard/.

Add an option to customize the target directory.

Fixes <https://github.com/Genymobile/scrcpy/issues/659>
2019-07-31 01:53:16 +02:00
ca970e8aa6 Merge branch 'master' into dev 2019-07-31 00:14:17 +02:00
6b3d9e3eab Add unit test for device message serialization
There was a test for the deserialization, but not for the serialization.
2019-07-30 12:17:33 +02:00
02692ffa42 Rename "build_" to "compile_"
Recent versions of meson complain about an option having name starting
with "build_":

> DEPRECATION: Option uses prefix "build_", which is reserved for Meson.
> This will become an error in the future.

Use "compile_" instead.
2019-07-29 15:19:07 +02:00
3b69463e61 Update README.md
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-07-29 14:59:44 +02:00
9dea6d2384 Add me as copyright owner 2019-07-29 14:59:44 +02:00
3c55d0c69b Fix double-free on error
If writing the recording header fails, do not clean the resources
immediately to avoid double-free.
2019-07-12 21:07:06 +02:00
4961256123 Close decoder on stream ended
Add missing call to decoder_close().
2019-06-26 23:50:39 +02:00
e4ac943d86 Document --window-title in README 2019-06-24 21:37:38 +02:00
4b997239f3 Merge pull request #614 from beango1:window-title-simple
add option window-title to set the title
2019-06-24 21:32:45 +02:00
8e65c10720 Add option --window-title
Add an option to set a custom window title.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-06-24 19:58:00 +02:00
056e47e752 Replace "cannot" by "could not" 2019-06-23 20:52:03 +02:00
439b009a79 Fix expected parameters count in error message 2019-06-23 20:47:21 +02:00
91ecb4f218 Close socket on error
Suggested-by: barry-ran

<https://github.com/Genymobile/scrcpy/issues/607>
2019-06-20 12:15:45 +02:00
bfb3f0842f Prevent to turn screen off if no control
If --no-control is set, then the controller is not initialized (both in
the client and the server), so it is not possible to control the device
to turn its screen off.

See <https://github.com/Genymobile/scrcpy/issues/608>.
2019-06-20 10:59:19 +02:00
87d7a157a9 Reference USBaudio from README 2019-06-20 10:47:02 +02:00
b91ecf5225 Fix --serial help
Make explicit that --serial excepts a parameter.
2019-06-18 17:13:53 +02:00
1807de4955 Merge pull request #595 from taaem/fix_build_fedora
The Java JDK is needed to build the server
2019-06-15 15:14:22 +02:00
0a233fd27f Fix required java package for Fedora
The Java JDK is needed to build the server. The relevant Fedora package
is java-devel, not java.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-06-15 15:10:51 +02:00
4940746bcb Remove useless else
The if-block ends with a return.
2019-06-14 10:15:53 +02:00
fe758e6e15 Improve comment
Rephrase to simplify and add a link to the issue.
2019-06-14 10:11:15 +02:00
b29a568f08 Merge pull request #587 from schwabe/fix_586_screen_off_qbeta
Use getPhysicalDisplayToken if getBuiltInDisplay is not found
2019-06-14 10:04:12 +02:00
b769083a5b Use getPhysicalDisplayToken on Anroid Q+ instead of getBuiltInDisplay
This makes the -S (screen off) parameter work on Android Q beta 4

Closes #586
2019-06-13 13:30:54 +02:00
8ca36406b9 Remove compilation flag "skip_frames"
It is unused since ebccb9f6cc.
2019-06-12 11:43:18 +02:00
53310a925a Disable portable build by default
The default value of a boolean meson option is true. We want
non-portable build by default.
2019-06-12 11:26:23 +02:00
0cb902d58b Merge pull request #587 from zzndb/patch-1 2019-06-12 11:23:15 +02:00
bcd0a876f7 Fix a spell mistake
After commented default portable option in `app/meson.build` get some
error and then find this. :)

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-06-12 11:22:50 +02:00
de2016a48e Add link to Snap package in README
<https://github.com/Genymobile/scrcpy/issues/523>
2019-06-11 23:41:56 +02:00
19ca6a0d66 Fix typo in README 2019-06-11 23:01:23 +02:00
e2996e85c0 Update links to v1.9 in README and BUILD 2019-06-11 23:00:09 +02:00
c2df0228a3 Merge branch 'dev' 2019-06-11 22:54:58 +02:00
259d3aee93 Bump version to 1.9 2019-06-11 21:50:29 +02:00
90859f1dcf Upgrade tarketSdkVersion to 29
This fixes a lint warning.
2019-06-11 21:49:14 +02:00
1afe9ce2ee Fix deprecation warning in Java unit test 2019-06-11 21:48:58 +02:00
273cec8a92 Fix typo in test name 2019-06-11 21:47:31 +02:00
02f189b1de Remove obsolete detail in README
Now that scrcpy-server.jar is found in the same directory as the
scrcpy executable, using SCRCPY_SERVER_PATH is not particularly useful
on Windows anymore
2019-06-11 21:37:09 +02:00
4abe163233 Remove obsolete explanation in FAQ
Issue 9 was about stdout/stderr not printed in Windows console. This is
solved since the Windows version is cross-compiled from Linux.
2019-06-11 21:20:06 +02:00
5ffdcbb7be Update DEVELOP.md 2019-06-11 21:18:43 +02:00
ffe0417228 Update platform-tools (29.0.1) for Windows
Include the latest version of adb in Windows releases.
2019-06-11 19:19:47 +02:00
e3afb67e7f Downgrade SDL to 2.0.8 for Windows
Revert "Update SDL (2.0.9) for Windows"

Several users experienced freezes with SDL 2.0.9.

This reverts commit a5787dccd6.

See:
 - <https://github.com/Genymobile/scrcpy/issues/425>
 - <https://discourse.libsdl.org/t/unstable-frame-rate-unexpectedly/25783>
2019-06-11 19:18:45 +02:00
4ee1391361 Upgrade FFmpeg (4.1.3) for Windows
Include the latest version of FFmpeg in Windows releases.
2019-06-11 19:18:45 +02:00
2755bfc255 Improve portable builds
In portable builds, scrcpy-server.jar was supposed to be present in the
current directory, so in practice it worked only if scrcpy was launched
from its own directory.

Instead, find the absolute path of the executable and build a suitable
path to use scrcpy-server.jar from the same directory.
2019-06-11 17:44:07 +02:00
3b17ff7c86 Add functions to convert wide char to UTF-8
There was already utf8_to_wide_char(), used to correctly execute
commands on Windows.

Add the reverse converter: utf8_from_wide_char(). We will need it to
build the scrcpy-server path based on the executable directory.
2019-06-11 17:44:07 +02:00
4eb6b26c93 Extract "scrcpy-server.jar" string
The filename is used at several places.
2019-06-11 17:44:07 +02:00
eb34098add Simplify portable build configuration
To create a portable build (with scrcpy-server.jar accessible from the
scrcpy directory), replace OVERRIDE_SERVER_PATH by a simple compilation
flag: PORTABLE.

This paves the way to use more complex rules to determine the path of
scrcpy-server.jar in portable builds.
2019-06-11 17:44:07 +02:00
b777760bca Simplify scrcpy-server path configuration
The full path of scrcpy-server.jar was partially configured from
meson.build then concatenated by C code.

Instead, directly write the path in C.
2019-06-11 17:44:07 +02:00
72bdfbc7a6 Never return 0 for stream protocol
On socket disconnection, on Linux, recv() returns -1 and errno is set.
But on Windows, errno is 0.

In that case, AVERROR(errno) == 0, leading to the warning:

> Invalid return value 0 for stream protocol

To avoid the problem, if errno is 0, return AVERROR_EOF.

Ref: commit 2876463d39
2019-06-11 17:44:07 +02:00
8604f16b30 Truncate device name at UTF-8 code point boundary
Just in case.
2019-06-07 17:45:03 +02:00
5d11339259 Inline lock_util functions
They are just tiny wrappers.
2019-06-07 17:19:00 +02:00
e2a272bf99 Improve framerate counting
The FPS counter was called only on new frames, so it could not print
values regularly, especially when there are very few FPS (when the
device surface does not change).

To the extreme, it was never able to display 0 fps.

Add a separate thread to print framerate every second.
2019-06-07 17:16:26 +02:00
d104d3bda9 Add cond_wait_timeout()
Add a "timed out" version of cond_wait().
2019-06-07 16:54:31 +02:00
eda44b6068 Fix controller cleanup
After commit bfb86ca2c2, the controller
was not stopped and destroyed on quit.
2019-06-07 00:03:21 +02:00
ebccb9f6cc Add runtime option to render expired frames
Replace the compilation flag SKIP_FRAMES by a runtime flag to force
rendering of expired frames. By default, the expired frames are skipped.
2019-06-05 21:39:42 +02:00
a143b8b07a Indent command-line options
Prepare indentation for --render-expired-frames.
2019-06-05 19:02:42 +02:00
9253996873 Add README section explaining --turn-screen-off 2019-06-05 19:02:42 +02:00
a13524e7f9 Replace android-tools-adb by adb
Here is the description of the adb package in Debian:

> Description: Android Debug Bridge
>
> A versatile command line tool that lets you communicate with an
> emulator instance or connected Android-powered device.
>
> This package recommends "android-sdk-platform-tools-common" which
> contains the udev rules for Android devices. Without this package, adb
> and fastboot need to be running with root permission.

And android-tools-adb:

> Description: transitional package
>
> This is a transitional package. It can safely be removed.
2019-06-05 09:52:25 +02:00
f3f3433163 Merge pull request #574 from crow1170/patch-1
Fix dependencies
2019-06-05 09:51:47 +02:00
232aaa386e Fix dependencies
Some missing or misspelled dependencies. Checked on Ubuntu 19.04.
2019-06-05 01:15:59 -04:00
8e66b33000 Add option to turn device screen off
In addition to the shortcut (Ctrl+o) to turn the device screen off, add
a command-line argument to turn it off on start.
2019-06-05 00:55:46 +02:00
7f07b13446 Indent command-line options
Preparse indentation for --turn-screen-off.
2019-06-05 00:55:39 +02:00
acc4dcd520 Disable server controller if --no-control
If --no-control is disabled, there is no need for a controller.

It also avoids to power on the device on start if control is disabled.
2019-06-05 00:25:57 +02:00
ca767ba364 Group server params in a struct
Starting the server requires more and more parameters. For clarity,
group them in a struct.
2019-06-05 00:25:57 +02:00
c8a6783494 Use positive options names internally
For clarity, store the flag resulting of the command-line options
--no-control and --no-display into "control" and "display".
2019-06-05 00:25:15 +02:00
5b56900e2b Rename unused field
The flag is used only in the server_start() implementation, there is no
need to store it in the structure.
2019-06-04 21:29:14 +02:00
8c8649cfcd Remove "turn device screen on" feature
Only keep "turn device screen off" and POWER button.

After we turn the device screen off (with Ctrl+o), turning it back on
does not always work, and leaves the device in a weird state, where even
the power button may not be sufficient:
<https://github.com/Genymobile/scrcpy/issues/175#issuecomment-497946596>

This is not an acceptable behavior, so disable the shortcut to turn the
physical device screen on. We can use the POWER button (or Ctrl+p)
instead.
2019-06-03 11:44:39 +02:00
e572d81fa2 Rename function to "power on"
This will reduce confusion between "power on" when the device is off and
"turn device screen off" while mirroring.
2019-06-02 20:34:52 +02:00
41225c3e41 Improve key processing readability
The condition "event->type == SDL_KEYDOWN" and the variable
input_manager->controller are used many times. Replace them by local
variables to reduce verbosity.
2019-06-01 07:18:05 +02:00
296047d82a Use net_close() to close sockets
So that it also works on Windows.
2019-05-31 23:33:44 +02:00
0792998cc2 Remove unused import
Introduced by the previous commit.
2019-05-31 23:31:53 +02:00
12a3bb25d3 Implement device screen off while mirroring
Add two shortcuts:
 - Ctrl+o to turn the device screen off while mirroring
 - Ctrl+Shift+o to turn it back on

On power on (either via the POWER key or BACK while screen is off), both
the device screen and the mirror are turned on.

<https://github.com/Genymobile/scrcpy/issues/175>
2019-05-31 23:07:23 +02:00
3ee9560ece Fix comment style
For consistency.
2019-05-31 22:57:12 +02:00
a56045dd80 Prevent socket leak on error
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-05-31 22:24:31 +02:00
fcf225049d Use consistent variable names
Use the same variable name in functions declaration and definition.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-05-31 22:24:17 +02:00
6537c2ef01 Add clipboard logs
Synchronizing local and device clipboards in invisible. Add INFO logs
on success.
2019-05-31 16:18:00 +02:00
9712cb8123 Do not minimize on focus loss
The default behavior seems annoying.

Fixes <https://github.com/Genymobile/scrcpy/issues/554>
2019-05-31 16:18:00 +02:00
ad55a9addc Prefix server logs
Sometimes, it is not obvious whether a log is generated by the server or
by the client. Prefix server logs for clarity.
2019-05-31 16:18:00 +02:00
28980bbc90 Rename "event" to "message"
After the recent refactorings, a "control event" is not necessarily an
"event" (it may be a "command"). Similarly, the unique "device event"
used to send the device clipboard content is more a "reponse" to the
request from the client than an "event".

Rename both to "message", and rename the message types to better
describe their intent.
2019-05-31 16:18:00 +02:00
f710d76c9e Merge pull request #568 from npes87184/dev
Correct return value type in handle_event
2019-05-31 15:27:35 +02:00
2a8a3e6ed5 Correct return value type in handle_event
handle_event return the type enum event_result not bool

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-05-31 20:57:06 +08:00
c2cef8d501 server/meson.build: Prevent using input field for directory
This will fix build warning in newer meson.
Fix #540.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-05-30 23:06:52 +02:00
0125af1e46 Update DEVELOP after recent refactorings 2019-05-30 22:46:52 +02:00
c13a24389c Implement computer-to-device clipboard copy
It was already possible to _paste_ (with Ctrl+v) the content of the
computer clipboard on the device. Technically, it injects a sequence of
events to generate the text.

Add a new feature (Ctrl+Shift+v) to copy to the device clipboard
instead, without injecting the content. Contrary to events injection,
this preserves the UTF-8 content exactly, so the text is not broken by
special characters.

<https://github.com/Genymobile/scrcpy/issues/413>
2019-05-30 22:46:52 +02:00
2322069656 Extract control event String parsing
Parsing a String from a serialized control event, encoded as length (2
bytes) + data, will be necessary in several events.

Extract it to a separate method.
2019-05-30 22:46:52 +02:00
61f5f96b42 Fix control event String parsing
At least 2 bytes must be available to read the length of the String.
2019-05-30 22:46:52 +02:00
63c078ee6c Implement device-to-computer clipboard copy
On Ctrl+C:
 - the client sends a GET_CLIPBOARD command to the device;
 - the device retrieve its current clipboard text and sends it in a
   GET_CLIPBOARD device event;
 - the client sets this text as the system clipboard text, so that it
   can be pasted in another application.

Fixes <https://github.com/Genymobile/scrcpy/issues/145>
2019-05-30 22:46:52 +02:00
3149e2cf4a Add device event sender
Create a separate component to send device events, managed by the
controller.
2019-05-30 22:46:52 +02:00
6112095e75 Add device event receiver
Create a separate component to handle device events, managed by the
controller.
2019-05-30 22:46:52 +02:00
f9d2d99166 Add GET_CLIPBOARD device event
Add the first device event, used to forward the device clipboard to the
computer.
2019-05-30 22:36:22 +02:00
ec71a3f66a Use two sockets for video and control
The socket used the device-to-computer direction to stream the video and
the computer-to-device direction to send control events.

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

To make them possible, use two sockets:
 - one for streaming the video from the device to the client;
 - one for control/events in both directions.
2019-05-30 22:35:41 +02:00
69360c7407 Extract control event string serialization
A string is serialized as a length (2 bytes) followed by the string data
(non nul-terminated).

For now, it is used only once, but we will need to serialize strings in
other events.
2019-05-30 22:35:04 +02:00
6ec2ddd2d1 Truncate UTF-8 properly
This will avoid to produce invalid UTF-8 results (although unlikely).
2019-05-30 22:34:59 +02:00
0a7fe7ad57 Add helpers to truncate UTF-8 at code points
This will help to avoid truncating a UTF-8 string in the middle of a
code point, producing an invalid UTF-8 result.
2019-05-30 22:30:18 +02:00
3aa5426cad Add unit tests for control events serialization
Add missing tests for serialization and deserialization of control
events.
2019-05-30 22:30:18 +02:00
63207d9cd5 Fix wrong comment in unit test 2019-05-30 22:30:18 +02:00
ad4c061cd2 Use custom class Point
The framework class android.graphics.Point cannot be used in unit tests.
Implement our own Point.
2019-05-30 22:30:18 +02:00
63909fd10d Merge commands with other control events
Several commands were grouped under the same event type "command", with
a separate field to indicate the actual command.

Move these commands at the same level as other control events. It will
allow to implement commands with arguments.
2019-05-30 22:30:18 +02:00
3b4366e5bf Stop stream immediately on quit
If the stream is stopped, av_read_frame() will be woken up and yield a
corrupted packet. Do not try to decode or record it.
2019-05-30 22:30:18 +02:00
47f1003200 Close server socket before killing process
The sockets may be closed and shutdown on server_stop(). This will
interrupt the stream and controller threads more quickly and gracefully.
2019-05-30 22:30:18 +02:00
bfb86ca2c2 Simplify cleanup
The cleanup is not linear: for example, the server must be stopped and
its sockets must be shutdown after the stream and controller are stopped
(so that they don't continue processing garbage), but before they are
joined, to avoid a deadlock if they are blocked on a socket read.

Simplify the spaghetti-cleanup by keeping trace of initialization at
runtime.
2019-05-30 22:30:18 +02:00
0dee9b04b2 Use net_recv() to read only one byte
Partial read is impossible for 1 byte, so net_recv_all() is useless.
2019-05-30 22:30:18 +02:00
8fc58bde75 Simplify server_connect_to()
Only use 2 branches, using either forward or remote tunnel.
2019-05-30 22:30:18 +02:00
5a431cdf9b Make server_connect_to() return a bool
The resulting socket is accessible from the server instance, there is no
need to return it.

This paves the way to use several sockets in parallel.
2019-05-30 22:30:18 +02:00
6edb1294f0 Add missing return 0 in unit test 2019-05-30 22:30:18 +02:00
073181b294 Use cbuf for file handler request queue
Replace the file_handler_request_queue implementation by cbuf.
2019-05-30 22:30:18 +02:00
241a3dcba5 Use cbuf for control event queue
Replace the control_event_queue implementation by cbuf.
2019-05-30 22:30:18 +02:00
b38292cd69 Add generic circular buffer
Add a circular buffer implementation, to factorize multiple specific
queues implementation.
2019-05-30 22:30:18 +02:00
7475550ae8 Add buffer_read16be()
Add a function to read 16 bits in big-endian to a uint16_t.
2019-05-30 22:30:18 +02:00
7fc8793d5b Make buffer util functions accept const buffers
So that they can be used both on const and non-const input buffers.
2019-05-30 22:30:18 +02:00
bf5e54b2e9 Make control_event_serialize() return size_t
control_event_serialize() returns the number of bytes written, so the
type should be size_t.
2019-05-30 22:30:18 +02:00
507b0bcccf Fix memory leak on error
The variable condition was not destroyed on strdup() failure.
2019-05-30 22:30:18 +02:00
e1afd9f8b0 Fix event ownership comment 2019-05-30 22:30:18 +02:00
b08dada6c1 Prefix control event constants by namespace
This will avoid conflicts with future device events.
2019-05-30 22:30:18 +02:00
999c964689 Make macro expansion-safe
Use parentheses to avoid unexpected results.

For example, make:

    2 * SERIALIZED_EVENT_MAX_SIZE

expand to:

    2 * (3 + TEXT_MAX_LENGTH)

instead of:

    2 * 3 + TEXT_MAX_LENGTH
2019-05-30 22:30:18 +02:00
befe455e44 Remove unused includes
The struct control_event does not use mutexes, and net.h does not need
SDL_platform.h.
2019-05-30 22:30:18 +02:00
d2504f974c Fix indentation
Previous refactorings broke indentation.
2019-05-30 22:30:18 +02:00
0fbab42f8c Format meson.build for readability 2019-05-30 22:30:18 +02:00
08f506b24f Replace SDL_bool by bool in tests
Commit dfed1b250e replaced SDL types by
standard types in sources, but tests were not updated.
2019-05-30 22:30:18 +02:00
3bc1c51b91 Always use SDL_malloc() and SDL_free()
To avoid mixing SDL_malloc()/SDL_strdup() with free(), or malloc() with
SDL_free(), always use the SDL version.
2019-05-30 22:30:08 +02:00
7ed976967f Fix checkstyle warning
Checkstyle wants a specific order of imports.
2019-05-30 00:22:05 +02:00
b75f0e9427 Merge branch 'master' into dev 2019-05-28 13:31:37 +02:00
5d473efeb5 Bind Home key to MOVE_HOME
On pressing Home key on the computer, move the cursor to the beginning
of the line instead of going back to the home screen.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
## Requirements ## Requirements
You need [adb]. It is available in the [Android SDK platform You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`android-adb-tools`). tools][platform-tools], or packaged in your distribution (`adb`).
On Windows, download the [platform-tools][platform-tools-windows] and extract On Windows, download the [platform-tools][platform-tools-windows] and extract
the following files to a directory accessible from your `PATH`: the following files to a directory accessible from your `PATH`:
@ -40,10 +40,10 @@ Install the required packages from your package manager.
```bash ```bash
# runtime dependencies # runtime dependencies
sudo apt install ffmpeg libsdl2-2.0.0 sudo apt install ffmpeg libsdl2-2.0-0
# client build dependencies # client build dependencies
sudo apt install make gcc pkg-config meson ninja-build \ sudo apt install make gcc git pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \ libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev libsdl2-dev
@ -70,7 +70,7 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
sudo dnf install SDL2-devel ffms2-devel meson gcc make sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies # server build dependencies
sudo dnf install java sudo dnf install java-devel
``` ```
@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.7.jar`][direct-scrcpy-server] - [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
_(SHA-256: ee86ec8424f7dc50cacdf927312bdb46e0aa0d68611da584dc4b16d8057bc25e)_ _(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-server-v1.7.jar [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String...
args)`][main] method), compiled against the Android framework, and executed as args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device. `shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61 [main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123
To run such a Java application, the classes must be [_dexed_][dex] (typically, To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@ -65,17 +65,20 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl]. components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178 [hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers [wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view [aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view
### Threading ### Threading
The server uses 2 threads: The server uses 3 threads:
- the **main** thread, encoding and streaming the video to the client; - the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control events_ (typically, - the **controller** thread, listening for _control messages_ (typically,
keyboard and mouse events) from the client. keyboard and mouse events) from the client;
- the **receiver** thread (managed by the controller), sending _device messges_
to the clients (currently, it is only used to send the device clipboard
content).
Since the video encoding is typically hardware, there would be no benefit in Since the video encoding is typically hardware, there would be no benefit in
encoding and streaming in two different threads. encoding and streaming in two different threads.
@ -89,9 +92,9 @@ The video is encoded using the [`MediaCodec`] API. The codec takes its input
from a [surface] associated to the display, and writes the resulting H.264 from a [surface] associated to the display, and writes the resulting H.264
stream to the provided output stream (the socket connected to the client). stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64 [surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69
On device [rotation], the codec, surface and display are reinitialized, and a On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced. new video stream is produced.
@ -105,30 +108,30 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92 [rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126 [repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection ### Input events injection
_Control events_ are received from the client by the [`EventController`] (run in _Control messages_ are received from the client by the [`Controller`] (run in a
a separate thread). There are 5 types of input events: separate thread). There are several types of input events:
- keycode (cf [`KeyEvent`]), - keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly), - text (special characters may not be handled by keycodes directly),
- mouse motion/click, - mouse motion/click,
- mouse scroll, - mouse scroll,
- custom command (e.g. to switch the screen on). - other commands (e.g. to switch the screen on or to copy the clipboard).
All of them may need to inject input events to the system. To do so, they use Some of them need to inject input events to the system. To do so, they use the
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]). [`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70 [`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 [`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@ -145,15 +148,15 @@ The video stream is decoded by [libav] (FFmpeg).
### Initialization ### Initialization
On startup, in addition to _libav_ and _SDL_ initialization, the client must On startup, in addition to _libav_ and _SDL_ initialization, the client must
push and start the server on the device, and open a socket so that they may push and start the server on the device, and open two sockets (one for the video
communicate. stream, one for control) so that they may communicate.
Note that the client-server roles are expressed at the application level: Note that the client-server roles are expressed at the application level:
- the server _serves_ video stream and handle requests from the client, - the server _serves_ video stream and handle requests from the client,
- the client _controls_ the device through the server. - the client _controls_ the device through the server.
However, the roles are inverted at the network level: However, the roles are reversed at the network level:
- the client opens a server socket and listen on a port before starting the - the client opens a server socket and listen on a port before starting the
server, server,
@ -162,6 +165,9 @@ However, the roles are inverted at the network level:
This role inversion guarantees that the connection will not fail due to race This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling. conditions, and avoids polling.
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
reverse`. See commit [1038bad] and [issue #5].)_
Once the server is connected, it sends the device information (name and initial Once the server is connected, it sends the device information (name and initial
screen dimensions). Thus, the client may init the window and renderer, before screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available. the first frame is available.
@ -169,25 +175,38 @@ the first frame is available.
To minimize startup time, SDL initialization is performed while listening for To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]). the connection from the server (see commit [90a46b4]).
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e [90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
### Threading ### Threading
The client uses 3 threads: The client uses 4 threads:
- the **main** thread, executing the SDL event loop, - the **main** thread, executing the SDL event loop,
- the **decoder** thread, decoding video frames, - the **stream** thread, receiving the video and used for decoding and
- the **controller** thread, sending _control events_ to the server. recording,
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the client.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to
print the framerate regularly in the console.
### Decoder
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264 ### Stream
stream from the socket, and notifies the main thread when a new frame is
available.
There are two [frames] simultaneously in memory: The video [stream] is received from the socket (connected to the server on the
device) in a separate thread.
If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_
to decode the H.264 stream from the socket, and notifies the main thread when a
new frame is available.
There are two [frames][video_buffer] simultaneously in memory:
- the **decoding** frame, written by the decoder from the decoder thread, - the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread. - the **rendering** frame, rendered in a texture from the main thread.
@ -195,26 +214,39 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one. to decode a new frame while the main thread renders the last one.
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h
```
+----------+ +----------+
---> | decoder | ---> | screen |
+---------+ / +----------+ +----------+
socket ---> | stream | ----
+---------+ \ +----------+
---> | recorder |
+----------+
```
### Controller ### Controller
The [controller] is responsible to send _control events_ to the device. It runs The [controller] is responsible to send _control messages_ to the device. It
in a separate thread, to avoid I/O on the main thread. 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] On SDL event, received on the main thread, the [input manager][inputmanager]
creates appropriate [_control events_][controlevent]. It is responsible to creates appropriate [_control messages_][controlmsg]. It is responsible to
convert SDL events to Android events (using [convert]). It pushes the _control 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 messages_ to a queue hold by the controller. On its own thread, the controller
controller takes events from the queue, that it serializes and sends to the takes messages from the queue, that it serializes and sends to the client.
client.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h [controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h [controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h [inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h [convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h
### UI and event loop ### UI and event loop
@ -225,9 +257,9 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager]. delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c [scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38 [event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h [screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h
## Hack ## Hack

4
FAQ.md
View File

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

View File

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

View File

@ -44,7 +44,7 @@ clean:
build-server: build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \ meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dbuild_app=false ) --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)" ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32: prepare-deps-win32:
@ -56,8 +56,8 @@ build-win32: prepare-deps-win32
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dcompile_server=false \
-Doverride_server_path=scrcpy-server.jar ) -Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)" ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32 build-win32-noconsole: prepare-deps-win32
@ -66,9 +66,9 @@ build-win32-noconsole: prepare-deps-win32
--cross-file cross_win32.txt \ --cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dcompile_server=false \
-Dwindows_noconsole=true \ -Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar ) -Dportable=true )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64: prepare-deps-win64:
@ -80,8 +80,8 @@ build-win64: prepare-deps-win64
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dcompile_server=false \
-Doverride_server_path=scrcpy-server.jar ) -Dportable=true )
ninja -C "$(WIN64_BUILD_DIR)" ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64 build-win64-noconsole: prepare-deps-win64
@ -90,9 +90,9 @@ build-win64-noconsole: prepare-deps-win64
--cross-file cross_win64.txt \ --cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \ --buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \ -Dcrossbuild_windows=true \
-Dbuild_server=false \ -Dcompile_server=false \
-Dwindows_noconsole=true \ -Dwindows_noconsole=true \
-Doverride_server_path=scrcpy-server.jar ) -Dportable=true )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole dist-win32: build-server build-win32 build-win32-noconsole
@ -100,28 +100,29 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32 zip-win32: dist-win32
cd "$(DIST)"; \ cd "$(DIST)"; \

139
README.md
View File

@ -1,15 +1,15 @@
# scrcpy (v1.7) # scrcpy (v1.9)
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
It works on _GNU/Linux_, _Windows_ and _MacOS_. It works on _GNU/Linux_, _Windows_ and _macOS_.
![screenshot](assets/screenshot-debian-600.jpg) ![screenshot](assets/screenshot-debian-600.jpg)
## Requirements ## Requirements
The Android part requires at least API 21 (Android 5.0). The Android device requires at least API 21 (Android 5.0).
Make sure you [enabled adb debugging][enable-adb] on your device(s). Make sure you [enabled adb debugging][enable-adb] on your device(s).
@ -29,12 +29,16 @@ control it using keyboard and mouse.
On Linux, you typically need to [build the app manually][BUILD]. Don't worry, On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard. it's not that hard.
For Arch Linux, two [AUR] packages have been created by users: A [Snap] package is available: [`scrcpy`][snap-link].
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/) [snap-link]: https://snapstats.org/snaps/scrcpy
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
@ -47,18 +51,18 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available: (including `adb`) are available:
- [`scrcpy-win32-v1.7.zip`][direct-win32] - [`scrcpy-win32-v1.9.zip`][direct-win32]
_(SHA-256: 98ae36f2da0b8212c07066fd93139650554274f863d4cee0781501a0c84f7c23)_ _(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
- [`scrcpy-win64-v1.7.zip`][direct-win64] - [`scrcpy-win64-v1.9.zip`][direct-win64]
_(SHA-256: b41416547521062f19e3f3f539e89a70e713bd086e69ef1b29c128993f7aa462)_ _(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-win32-v1.7.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.7/scrcpy-win64-v1.7.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip
You can also [build the app manually][BUILD]. You can also [build the app manually][BUILD].
### Mac OS ### macOS
The application is available in [Homebrew]. Just install it: The application is available in [Homebrew]. Just install it:
@ -97,22 +101,22 @@ scrcpy --help
### Reduce size ### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to Sometimes, it is useful to mirror an Android device at a lower definition to
increase performances. increase performance.
To limit both width and height to some value (e.g. 1024): To limit both the width and height to some value (e.g. 1024):
```bash ```bash
scrcpy --max-size 1024 scrcpy --max-size 1024
scrcpy -m 1024 # short version scrcpy -m 1024 # short version
``` ```
The other dimension is computed to that the device aspect-ratio is preserved. The other dimension is computed to that the device aspect ratio is preserved.
That way, a device in 1920×1080 will be mirrored at 1024×576. That way, a device in 1920×1080 will be mirrored at 1024×576.
### Change bit-rate ### Change bit-rate
The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps): The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
```bash ```bash
scrcpy --bit-rate 2M scrcpy --bit-rate 2M
@ -124,7 +128,7 @@ scrcpy -b 2M # short version
The device screen may be cropped to mirror only part of the screen. The device screen may be cropped to mirror only part of the screen.
This is useful for example to mirror only 1 eye of the Oculus Go: This is useful for example to mirror only one eye of the Oculus Go:
```bash ```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
@ -171,6 +175,7 @@ To disable mirroring while recording:
scrcpy --no-display --record file.mp4 scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C # interrupt recording with Ctrl+C
# Ctrl+C does not terminate properly on Windows, so disconnect the device
``` ```
"Skipped frames" are recorded, even if they are not displayed in real time (for "Skipped frames" are recorded, even if they are not displayed in real time (for
@ -246,6 +251,11 @@ _scrcpy_ window.
There is no visual feedback, a log is printed to the console. There is no visual feedback, a log is printed to the console.
The target directory can be changed on start:
```bash
scrcpy --push-target /sdcard/foo/bar/
```
### Read-only ### Read-only
@ -257,44 +267,92 @@ scrcpy --no-control
scrcpy -n scrcpy -n
``` ```
### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
command-line option:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
### Custom window title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
```
### Forward audio ### Forward audio
Audio is not forwarded by _scrcpy_. Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
There is a limited solution using [AOA], implemented in the [`audio`] branch. If Also see [issue #14].
you are interested, see [issue 14].
[USBaudio]: https://github.com/rom1v/usbaudio
[AOA]: https://source.android.com/devices/accessories/aoa2 [issue #14]: https://github.com/Genymobile/scrcpy/issues/14
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts ## Shortcuts
| Action | Shortcut | | Action | Shortcut |
| -------------------------------------- |:---------------------------- | | -------------------------------------- |:---------------------------- |
| switch fullscreen mode | `Ctrl`+`f` | | Switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| click on `APP_SWITCH` | `Ctrl`+`s` | | Click on `APP_SWITCH` | `Ctrl`+`s` |
| click on `MENU` | `Ctrl`+`m` | | Click on `MENU` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on MacOS) | | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ (`Cmd`+`↑` on macOS) |
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) | | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on macOS) |
| click on `POWER` | `Ctrl`+`p` | | Click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click²_ | | Power on | _Right-click²_ |
| expand notification panel | `Ctrl`+`n` | | Turn device screen off (keep mirroring)| `Ctrl`+`o` |
| collapse notification panel | `Ctrl`+`Shift`+`n` | | Expand notification panel | `Ctrl`+`n` |
| paste computer clipboard to device | `Ctrl`+`v` | | Collapse notification panel | `Ctrl`+`Shift`+`n` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` | | Copy device clipboard to computer | `Ctrl`+`c` |
| Paste computer clipboard to device | `Ctrl`+`v` |
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` |
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._ _²Right-click turns the screen on if it was off, presses BACK otherwise._
## Custom paths
To use a specific _adb_ binary, configure its path in the environment variable
`ADB`:
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file, configure its path in
`SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## Why _scrcpy_? ## Why _scrcpy_?
A colleague challenged me to find a name as unpronounceable as [gnirehtet]. A colleague challenged me to find a name as unpronounceable as [gnirehtet].
@ -327,6 +385,7 @@ Read the [developers page].
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

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

View File

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

72
app/src/buffered_reader.c Normal file
View File

@ -0,0 +1,72 @@
#include "buffered_reader.h"
#include <SDL2/SDL_assert.h>
#include "log.h"
bool
buffered_reader_init(struct buffered_reader *reader, socket_t socket,
size_t bufsize) {
reader->buf = SDL_malloc(bufsize);
if (!reader->buf) {
LOGC("Could not allocate buffer");
return false;
}
reader->socket = socket;
reader->bufsize = bufsize;
reader->offset = 0;
reader->len = 0;
return true;
}
void
buffered_reader_destroy(struct buffered_reader *reader) {
SDL_free(reader->buf);
}
static ssize_t
buffered_reader_fill(struct buffered_reader *reader) {
SDL_assert(!reader->len);
ssize_t r = net_recv(reader->socket, reader->buf, reader->bufsize);
if (r > 0) {
reader->offset = 0;
reader->len = r;
}
return r;
}
ssize_t
buffered_reader_recv(struct buffered_reader *reader, void *buf, size_t count) {
if (!reader->len) {
// read from the socket
ssize_t r = buffered_reader_fill(reader);
if (r <= 0) {
return r;
}
}
size_t r = count < reader->len ? count : reader->len;
memcpy(buf, reader->buf + reader->offset, r);
reader->offset += r;
reader->len -= r;
return r;
}
ssize_t
buffered_reader_recv_all(struct buffered_reader *reader, void *buf,
size_t count) {
size_t done = 0;
while (done < count) {
ssize_t r = buffered_reader_recv(reader, buf, count - done);
if (r <= 0) {
// if there was some data, return them immediately
return done ? done : r;
}
done += r;
buf += r;
}
return done;
}

29
app/src/buffered_reader.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef BUFFERED_READER_H
#define BUFFERED_READER_H
#include "common.h"
#include "net.h"
struct buffered_reader {
socket_t socket;
void *buf;
size_t bufsize;
size_t offset;
size_t len;
};
bool
buffered_reader_init(struct buffered_reader *reader, socket_t socket,
size_t bufsize);
void
buffered_reader_destroy(struct buffered_reader *reader);
ssize_t
buffered_reader_recv(struct buffered_reader *reader, void *buf, size_t count);
ssize_t
buffered_reader_recv_all(struct buffered_reader *reader, void *buf,
size_t count);
#endif

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,91 +0,0 @@
#ifndef CONTROLEVENT_H
#define CONTROLEVENT_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include "android/input.h"
#include "android/keycodes.h"
#include "common.h"
#define CONTROL_EVENT_QUEUE_SIZE 64
#define TEXT_MAX_LENGTH 300
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE,
CONTROL_EVENT_TYPE_TEXT,
CONTROL_EVENT_TYPE_MOUSE,
CONTROL_EVENT_TYPE_SCROLL,
CONTROL_EVENT_TYPE_COMMAND,
};
enum control_event_command {
CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON,
CONTROL_EVENT_COMMAND_EXPAND_NOTIFICATION_PANEL,
CONTROL_EVENT_COMMAND_COLLAPSE_NOTIFICATION_PANEL,
};
struct control_event {
enum control_event_type type;
union {
struct {
enum android_keyevent_action action;
enum android_keycode keycode;
enum android_metastate metastate;
} keycode_event;
struct {
char *text; // owned, to be freed by SDL_free()
} text_event;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
struct position position;
} mouse_event;
struct {
struct position position;
int32_t hscroll;
int32_t vscroll;
} scroll_event;
struct {
enum control_event_command action;
} command_event;
};
};
struct control_event_queue {
struct control_event data[CONTROL_EVENT_QUEUE_SIZE];
int head;
int tail;
};
// buf size must be at least SERIALIZED_EVENT_MAX_SIZE
int
control_event_serialize(const struct control_event *event, unsigned char *buf);
bool
control_event_queue_init(struct control_event_queue *queue);
void
control_event_queue_destroy(struct control_event_queue *queue);
bool
control_event_queue_is_empty(const struct control_event_queue *queue);
bool
control_event_queue_is_full(const struct control_event_queue *queue);
// event is copied, the queue does not use the event after the function returns
bool
control_event_queue_push(struct control_event_queue *queue,
const struct control_event *event);
bool
control_event_queue_take(struct control_event_queue *queue,
struct control_event *event);
void
control_event_destroy(struct control_event *event);
#endif

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

@ -0,0 +1,86 @@
#include "control_msg.h"
#include <string.h>
#include "buffer_util.h"
#include "log.h"
#include "str_util.h"
static void
write_position(uint8_t *buf, const struct position *position) {
buffer_write32be(&buf[0], position->point.x);
buffer_write32be(&buf[4], position->point.y);
buffer_write16be(&buf[8], position->screen_size.width);
buffer_write16be(&buf[10], position->screen_size.height);
}
// write length (2 bytes) + string (non nul-terminated)
static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
size_t len = utf8_truncation_index(utf8, max_len);
buffer_write16be(buf, (uint16_t) len);
memcpy(&buf[2], utf8, len);
return 2 + len;
}
size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[0] = msg->type;
switch (msg->type) {
case CONTROL_MSG_TYPE_INJECT_KEYCODE:
buf[1] = msg->inject_keycode.action;
buffer_write32be(&buf[2], msg->inject_keycode.keycode);
buffer_write32be(&buf[6], msg->inject_keycode.metastate);
return 10;
case CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len = write_string(msg->inject_text.text,
CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]);
return 1 + len;
}
case CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT:
buf[1] = msg->inject_mouse_event.action;
buffer_write32be(&buf[2], msg->inject_mouse_event.buttons);
write_position(&buf[6], &msg->inject_mouse_event.position);
return 18;
case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
buffer_write32be(&buf[13],
(uint32_t) msg->inject_scroll_event.hscroll);
buffer_write32be(&buf[17],
(uint32_t) msg->inject_scroll_event.vscroll);
return 21;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
size_t len = write_string(msg->inject_text.text,
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[1]);
return 1 + len;
}
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode;
return 2;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
// no additional data
return 1;
default:
LOGW("Unknown message type: %u", (unsigned) msg->type);
return 0;
}
}
void
control_msg_destroy(struct control_msg *msg) {
switch (msg->type) {
case CONTROL_MSG_TYPE_INJECT_TEXT:
SDL_free(msg->inject_text.text);
break;
case CONTROL_MSG_TYPE_SET_CLIPBOARD:
SDL_free(msg->set_clipboard.text);
break;
default:
// do nothing
break;
}
}

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

@ -0,0 +1,74 @@
#ifndef CONTROLMSG_H
#define CONTROLMSG_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "android/input.h"
#include "android/keycodes.h"
#include "common.h"
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
CONTROL_MSG_TYPE_INJECT_TEXT,
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
};
enum screen_power_mode {
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SCREEN_POWER_MODE_OFF = 0,
SCREEN_POWER_MODE_NORMAL = 2,
};
struct control_msg {
enum control_msg_type type;
union {
struct {
enum android_keyevent_action action;
enum android_keycode keycode;
enum android_metastate metastate;
} inject_keycode;
struct {
char *text; // owned, to be freed by SDL_free()
} inject_text;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
struct position position;
} inject_mouse_event;
struct {
struct position position;
int32_t hscroll;
int32_t vscroll;
} inject_scroll_event;
struct {
char *text; // owned, to be freed by SDL_free()
} set_clipboard;
struct {
enum screen_power_mode mode;
} set_screen_power_mode;
};
};
// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE
// return the number of bytes written
size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf);
void
control_msg_destroy(struct control_msg *msg);
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,48 @@
#include "device_msg.h"
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "buffer_util.h"
#include "log.h"
ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg) {
if (len < 3) {
// at least type + empty string length
return 0; // not available
}
msg->type = buf[0];
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: {
uint16_t clipboard_len = buffer_read16be(&buf[1]);
if (clipboard_len > len - 3) {
return 0; // not available
}
char *text = SDL_malloc(clipboard_len + 1);
if (!text) {
LOGW("Could not allocate text for clipboard");
return -1;
}
if (clipboard_len) {
memcpy(text, &buf[3], clipboard_len);
}
text[clipboard_len] = '\0';
msg->clipboard.text = text;
return 3 + clipboard_len;
}
default:
LOGW("Unknown device message type: %d", (int) msg->type);
return -1; // error, we cannot recover
}
}
void
device_msg_destroy(struct device_msg *msg) {
if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) {
SDL_free(msg->clipboard.text);
}
}

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

@ -0,0 +1,32 @@
#ifndef DEVICEMSG_H
#define DEVICEMSG_H
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)
enum device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD,
};
struct device_msg {
enum device_msg_type type;
union {
struct {
char *text; // owned, to be freed by SDL_free()
} clipboard;
};
};
// return the number of bytes consumed (0 for no msg available, -1 on error)
ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg);
void
device_msg_destroy(struct device_msg *msg);
#endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,38 +0,0 @@
#include <lock_util.h>
#include <stdlib.h>
#include <SDL2/SDL_mutex.h>
#include "log.h"
void
mutex_lock(SDL_mutex *mutex) {
if (SDL_LockMutex(mutex)) {
LOGC("Could not lock mutex");
abort();
}
}
void
mutex_unlock(SDL_mutex *mutex) {
if (SDL_UnlockMutex(mutex)) {
LOGC("Could not unlock mutex");
abort();
}
}
void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
if (SDL_CondWait(cond, mutex)) {
LOGC("Could not wait on condition");
abort();
}
}
void
cond_signal(SDL_cond *cond) {
if (SDL_CondSignal(cond)) {
LOGC("Could not signal a condition");
abort();
}
}

View File

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

View File

@ -5,6 +5,7 @@
#include <stdint.h> #include <stdint.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "compat.h" #include "compat.h"
@ -16,6 +17,8 @@ struct args {
const char *serial; const char *serial;
const char *crop; const char *crop;
const char *record_filename; const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format; enum recorder_format record_format;
bool fullscreen; bool fullscreen;
bool no_control; bool no_control;
@ -27,6 +30,8 @@ struct args {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
bool always_on_top; bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
}; };
static void usage(const char *arg0) { static void usage(const char *arg0) {
@ -72,15 +77,29 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n" " Set the TCP port the client listens on.\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" --push-target path\n"
" Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n"
" Default is \"/sdcard/\".\n"
"\n"
" -r, --record file.mp4\n" " -r, --record file.mp4\n"
" Record screen to file.\n" " Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n" " The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n" " set, or by the file extension (.mp4 or .mkv).\n"
"\n" "\n"
" -s, --serial\n" " --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" -s, --serial serial\n"
" The device serial number. Mandatory only if several devices\n" " The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n" " are connected to adb.\n"
"\n" "\n"
" -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n"
"\n"
" -t, --show-touches\n" " -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n" " Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n" " It only shows physical touches (not clicks from scrcpy).\n"
@ -91,6 +110,9 @@ static void usage(const char *arg0) {
" -v, --version\n" " -v, --version\n"
" Print the version of scrcpy.\n" " Print the version of scrcpy.\n"
"\n" "\n"
" --window-title text\n"
" Set a custom window title.\n"
"\n"
"Shortcuts:\n" "Shortcuts:\n"
"\n" "\n"
" Ctrl+f\n" " Ctrl+f\n"
@ -104,7 +126,6 @@ static void usage(const char *arg0) {
" resize window to remove black borders\n" " resize window to remove black borders\n"
"\n" "\n"
" Ctrl+h\n" " Ctrl+h\n"
" Home\n"
" Middle-click\n" " Middle-click\n"
" click on HOME\n" " click on HOME\n"
"\n" "\n"
@ -129,7 +150,10 @@ static void usage(const char *arg0) {
" click on POWER (turn screen on/off)\n" " click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click (when screen is off)\n" " Right-click (when screen is off)\n"
" turn screen on\n" " power on\n"
"\n"
" Ctrl+o\n"
" turn device screen off (keep mirroring)\n"
"\n" "\n"
" Ctrl+n\n" " Ctrl+n\n"
" expand notification panel\n" " expand notification panel\n"
@ -137,9 +161,15 @@ static void usage(const char *arg0) {
" Ctrl+Shift+n\n" " Ctrl+Shift+n\n"
" collapse notification panel\n" " collapse notification panel\n"
"\n" "\n"
" Ctrl+c\n"
" copy device clipboard to computer\n"
"\n"
" Ctrl+v\n" " Ctrl+v\n"
" paste computer clipboard to device\n" " paste computer clipboard to device\n"
"\n" "\n"
" Ctrl+Shift+v\n"
" copy computer clipboard to device\n"
"\n"
" Ctrl+i\n" " Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n" " enable/disable FPS counter (print frames/second in logs)\n"
"\n" "\n"
@ -274,27 +304,38 @@ guess_record_format(const char *filename) {
return 0; return 0;
} }
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
static bool static bool
parse_args(struct args *args, int argc, char *argv[]) { parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, 'T'}, {"always-on-top", no_argument, NULL, 'T'},
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'}, {"crop", required_argument, NULL, 'c'},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"record", required_argument, NULL, 'r'}, {"push-target", required_argument, NULL,
{"record-format", required_argument, NULL, 'f'}, OPT_PUSH_TARGET},
{"serial", required_argument, NULL, 's'}, {"record", required_argument, NULL, 'r'},
{"show-touches", no_argument, NULL, 't'}, {"record-format", required_argument, NULL, 'f'},
{"version", no_argument, NULL, 'v'}, {"render-expired-frames", no_argument, NULL,
{NULL, 0, NULL, 0 }, OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL,
OPT_WINDOW_TITLE},
{NULL, 0, NULL, 0 },
}; };
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:tTv", long_options, while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) { NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b':
@ -338,6 +379,9 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 's': case 's':
args->serial = optarg; args->serial = optarg;
break; break;
case 'S':
args->turn_screen_off = true;
break;
case 't': case 't':
args->show_touches = true; args->show_touches = true;
break; break;
@ -347,6 +391,15 @@ parse_args(struct args *args, int argc, char *argv[]) {
case 'v': case 'v':
args->version = true; args->version = true;
break; break;
case OPT_RENDER_EXPIRED_FRAMES:
args->render_expired_frames = true;
break;
case OPT_WINDOW_TITLE:
args->window_title = optarg;
break;
case OPT_PUSH_TARGET:
args->push_target = optarg;
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@ -383,6 +436,11 @@ parse_args(struct args *args, int argc, char *argv[]) {
} }
} }
if (args->no_control && args->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
return true; return true;
} }
@ -398,6 +456,8 @@ main(int argc, char *argv[]) {
.serial = NULL, .serial = NULL,
.crop = NULL, .crop = NULL,
.record_filename = NULL, .record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0, .record_format = 0,
.help = false, .help = false,
.version = false, .version = false,
@ -408,6 +468,8 @@ main(int argc, char *argv[]) {
.always_on_top = false, .always_on_top = false,
.no_control = false, .no_control = false,
.no_display = false, .no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
}; };
if (!parse_args(&args, argc, argv)) { if (!parse_args(&args, argc, argv)) {
return 1; return 1;
@ -423,6 +485,8 @@ main(int argc, char *argv[]) {
return 0; return 0;
} }
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all(); av_register_all();
#endif #endif
@ -440,14 +504,18 @@ main(int argc, char *argv[]) {
.crop = args.crop, .crop = args.crop,
.port = args.port, .port = args.port,
.record_filename = args.record_filename, .record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format, .record_format = args.record_format,
.max_size = args.max_size, .max_size = args.max_size,
.bit_rate = args.bit_rate, .bit_rate = args.bit_rate,
.show_touches = args.show_touches, .show_touches = args.show_touches,
.fullscreen = args.fullscreen, .fullscreen = args.fullscreen,
.always_on_top = args.always_on_top, .always_on_top = args.always_on_top,
.no_control = args.no_control, .control = !args.no_control,
.no_display = args.no_display, .display = !args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
}; };
int res = scrcpy(&options) ? 0 : 1; int res = scrcpy(&options) ? 0 : 1;

View File

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

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

@ -0,0 +1,107 @@
#include "receiver.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_clipboard.h>
#include "config.h"
#include "device_msg.h"
#include "lock_util.h"
#include "log.h"
bool
receiver_init(struct receiver *receiver, socket_t control_socket) {
if (!(receiver->mutex = SDL_CreateMutex())) {
return false;
}
receiver->control_socket = control_socket;
return true;
}
void
receiver_destroy(struct receiver *receiver) {
SDL_DestroyMutex(receiver->mutex);
}
static void
process_msg(struct receiver *receiver, struct device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD:
LOGI("Device clipboard copied");
SDL_SetClipboardText(msg->clipboard.text);
break;
}
}
static ssize_t
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
size_t head = 0;
for (;;) {
struct device_msg msg;
ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg);
if (r == -1) {
return -1;
}
if (r == 0) {
return head;
}
process_msg(receiver, &msg);
device_msg_destroy(&msg);
head += r;
SDL_assert(head <= len);
if (head == len) {
return head;
}
}
}
static int
run_receiver(void *data) {
struct receiver *receiver = data;
unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE];
size_t head = 0;
for (;;) {
SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf,
DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
if (r <= 0) {
LOGD("Receiver stopped");
break;
}
ssize_t consumed = process_msgs(receiver, buf, r);
if (consumed == -1) {
// an error occurred
break;
}
if (consumed) {
// shift the remaining data in the buffer
memmove(buf, &buf[consumed], r - consumed);
head = r - consumed;
}
}
return 0;
}
bool
receiver_start(struct receiver *receiver) {
LOGD("Starting receiver thread");
receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver);
if (!receiver->thread) {
LOGC("Could not start receiver thread");
return false;
}
return true;
}
void
receiver_join(struct receiver *receiver) {
SDL_WaitThread(receiver->thread, NULL);
}

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

@ -0,0 +1,32 @@
#ifndef RECEIVER_H
#define RECEIVER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
// receive events from the device
// managed by the controller
struct receiver {
socket_t control_socket;
SDL_Thread *thread;
SDL_mutex *mutex;
};
bool
receiver_init(struct receiver *receiver, socket_t control_socket);
void
receiver_destroy(struct receiver *receiver);
bool
receiver_start(struct receiver *receiver);
// no receiver_stop(), it will automatically stop on control_socket shutdown
void
receiver_join(struct receiver *receiver);
#endif

View File

@ -5,6 +5,7 @@
#include "compat.h" #include "compat.h"
#include "config.h" #include "config.h"
#include "lock_util.h"
#include "log.h" #include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@ -26,6 +27,82 @@ find_muxer(const char *name) {
return oformat; return oformat;
} }
static struct record_packet *
record_packet_new(const AVPacket *packet) {
struct record_packet *rec = SDL_malloc(sizeof(*rec));
if (!rec) {
return NULL;
}
if (av_packet_ref(&rec->packet, packet)) {
SDL_free(rec);
return NULL;
}
rec->next = NULL;
return rec;
}
static void
record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet);
SDL_free(rec);
}
static void
recorder_queue_init(struct recorder_queue *queue) {
queue->first = NULL;
// queue->last is undefined if queue->first == NULL
}
static inline bool
recorder_queue_is_empty(struct recorder_queue *queue) {
return !queue->first;
}
static bool
recorder_queue_push(struct recorder_queue *queue, const AVPacket *packet) {
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
return false;
}
rec->next = NULL;
if (recorder_queue_is_empty(queue)) {
queue->first = queue->last = rec;
} else {
// chain rec after the (current) last packet
queue->last->next = rec;
// the last packet is now rec
queue->last = rec;
}
return true;
}
static inline struct record_packet *
recorder_queue_take(struct recorder_queue *queue) {
SDL_assert(!recorder_queue_is_empty(queue));
struct record_packet *rec = queue->first;
SDL_assert(rec);
queue->first = rec->next;
// no need to update queue->last if the queue is left empty:
// queue->last is undefined if queue->first == NULL
return rec;
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
struct record_packet *rec = queue->first;
while (rec) {
struct record_packet *current = rec;
rec = rec->next;
record_packet_delete(current);
}
queue->first = NULL;
}
bool bool
recorder_init(struct recorder *recorder, recorder_init(struct recorder *recorder,
const char *filename, const char *filename,
@ -33,10 +110,28 @@ recorder_init(struct recorder *recorder,
struct size declared_frame_size) { struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename); recorder->filename = SDL_strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
LOGE("Cannot strdup filename"); LOGE("Could not strdup filename");
return false; return false;
} }
recorder->mutex = SDL_CreateMutex();
if (!recorder->mutex) {
LOGC("Could not create mutex");
SDL_free(recorder->filename);
return false;
}
recorder->queue_cond = SDL_CreateCond();
if (!recorder->queue_cond) {
LOGC("Could not create cond");
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
return false;
}
recorder_queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format; recorder->format = format;
recorder->declared_frame_size = declared_frame_size; recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false; recorder->header_written = false;
@ -46,6 +141,8 @@ recorder_init(struct recorder *recorder,
void void
recorder_destroy(struct recorder *recorder) { recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename); SDL_free(recorder->filename);
} }
@ -119,12 +216,17 @@ recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx); int ret = av_write_trailer(recorder->ctx);
if (ret < 0) { if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename); LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
} }
avio_close(recorder->ctx->pb); avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx); avformat_free_context(recorder->ctx);
const char *format_name = recorder_get_format_name(recorder->format); if (recorder->failed) {
LOGI("Recording complete to %s file: %s", format_name, recorder->filename); LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
} }
static bool static bool
@ -133,7 +235,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) { if (!extradata) {
LOGC("Cannot allocate extradata"); LOGC("Could not allocate extradata");
return false; return false;
} }
@ -151,9 +253,6 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
int ret = avformat_write_header(recorder->ctx, NULL); int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) { if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename); LOGE("Failed to write header to %s", recorder->filename);
SDL_free(extradata);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return false; return false;
} }
@ -169,13 +268,110 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
bool bool
recorder_write(struct recorder *recorder, AVPacket *packet) { recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) { if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = recorder_write_header(recorder, packet); bool ok = recorder_write_header(recorder, packet);
if (!ok) { if (!ok) {
return false; return false;
} }
recorder->header_written = true; recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
} }
recorder_rescale_packet(recorder, packet); recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0; return av_write_frame(recorder->ctx, packet) >= 0;
} }
static int
run_recorder(void *data) {
struct recorder *recorder = data;
for (;;) {
mutex_lock(recorder->mutex);
while (!recorder->stopped &&
recorder_queue_is_empty(&recorder->queue)) {
cond_wait(recorder->queue_cond, recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && recorder_queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex);
break;
}
struct record_packet *rec = recorder_queue_take(&recorder->queue);
mutex_unlock(recorder->mutex);
bool ok = recorder_write(recorder, &rec->packet);
record_packet_delete(rec);
if (!ok) {
LOGE("Could not record packet");
mutex_lock(recorder->mutex);
recorder->failed = true;
// discard pending packets
recorder_queue_clear(&recorder->queue);
mutex_unlock(recorder->mutex);
break;
}
}
LOGD("Recorder thread ended");
return 0;
}
bool
recorder_start(struct recorder *recorder) {
LOGD("Starting recorder thread");
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder);
if (!recorder->thread) {
LOGC("Could not start recorder thread");
return false;
}
return true;
}
void
recorder_stop(struct recorder *recorder) {
mutex_lock(recorder->mutex);
recorder->stopped = true;
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
}
void
recorder_join(struct recorder *recorder) {
SDL_WaitThread(recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex);
SDL_assert(!recorder->stopped);
if (recorder->failed) {
// reject any new packet (this will stop the stream)
return false;
}
bool ok = recorder_queue_push(&recorder->queue, packet);
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
return ok;
}

View File

@ -3,6 +3,8 @@
#include <stdbool.h> #include <stdbool.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "common.h" #include "common.h"
@ -11,16 +13,33 @@ enum recorder_format {
RECORDER_FORMAT_MKV, RECORDER_FORMAT_MKV,
}; };
struct record_packet {
AVPacket packet;
struct record_packet *next;
};
struct recorder_queue {
struct record_packet *first;
struct record_packet *last; // undefined if first is NULL
};
struct recorder { struct recorder {
char *filename; char *filename;
enum recorder_format format; enum recorder_format format;
AVFormatContext *ctx; AVFormatContext *ctx;
struct size declared_frame_size; struct size declared_frame_size;
bool header_written; bool header_written;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *queue_cond;
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
}; };
bool bool
recorder_init(struct recorder *recoder, const char *filename, recorder_init(struct recorder *recorder, const char *filename,
enum recorder_format format, struct size declared_frame_size); enum recorder_format format, struct size declared_frame_size);
void void
@ -33,6 +52,15 @@ void
recorder_close(struct recorder *recorder); recorder_close(struct recorder *recorder);
bool bool
recorder_write(struct recorder *recorder, AVPacket *packet); recorder_start(struct recorder *recorder);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
#endif #endif

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,59 +17,13 @@
#include "log.h" #include "log.h"
#include "recorder.h" #include "recorder.h"
#define BUFSIZE 0x10000 #define STREAM_BUFSIZE 0x10000
#define HEADER_SIZE 12 #define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1) #define NO_PTS UINT64_C(-1)
static struct frame_meta *
frame_meta_new(uint64_t pts) {
struct frame_meta *meta = malloc(sizeof(*meta));
if (!meta) {
return meta;
}
meta->pts = pts;
meta->next = NULL;
return meta;
}
static void
frame_meta_delete(struct frame_meta *frame_meta) {
free(frame_meta);
}
static bool static bool
receiver_state_push_meta(struct receiver_state *state, uint64_t pts) { stream_recv_packet(struct stream *stream, AVPacket *packet) {
struct frame_meta *frame_meta = frame_meta_new(pts);
if (!frame_meta) {
return false;
}
// append to the list
// (iterate to find the last item, in practice the list should be tiny)
struct frame_meta **p = &state->frame_meta_queue;
while (*p) {
p = &(*p)->next;
}
*p = frame_meta;
return true;
}
static uint64_t
receiver_state_take_meta(struct receiver_state *state) {
struct frame_meta *frame_meta = state->frame_meta_queue; // first item
SDL_assert(frame_meta); // must not be empty
uint64_t pts = frame_meta->pts;
state->frame_meta_queue = frame_meta->next; // remove the item
frame_meta_delete(frame_meta);
return pts;
}
static int
read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
struct receiver_state *state = &stream->receiver_state;
// The video stream contains raw packets, without time information. When we // The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header // record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet. // added by the server before each raw packet.
@ -82,60 +36,31 @@ read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
// //
// It is followed by <packet_size> bytes containing the packet/frame. // It is followed by <packet_size> bytes containing the packet/frame.
if (!state->remaining) { uint8_t header[HEADER_SIZE];
#define HEADER_SIZE 12 ssize_t r =
uint8_t header[HEADER_SIZE]; buffered_reader_recv_all(&stream->buffered_reader, header, HEADER_SIZE);
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); if (r < HEADER_SIZE) {
if (r == -1) { return false;
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
// no partial read (net_recv_all())
SDL_assert_release(r == HEADER_SIZE);
uint64_t pts = buffer_read64be(header);
state->remaining = buffer_read32be(&header[8]);
if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
LOGE("Could not store PTS for recording");
// we cannot save the PTS, the recording would be broken
return AVERROR(ENOMEM);
}
} }
SDL_assert(state->remaining); uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]);
SDL_assert(len);
if (buf_size > state->remaining) { if (av_new_packet(packet, len)) {
buf_size = state->remaining; LOGE("Could not allocate packet");
return false;
} }
ssize_t r = net_recv(stream->socket, buf, buf_size); r = buffered_reader_recv_all(&stream->buffered_reader, packet->data, len);
if (r == -1) { if (r < len) {
return AVERROR(errno); av_packet_unref(packet);
} return false;
if (r == 0) {
return AVERROR_EOF;
} }
SDL_assert(state->remaining >= r); packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
state->remaining -= r;
return r; return true;
}
static int
read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
} }
static void static void
@ -145,120 +70,221 @@ notify_stopped(void) {
SDL_PushEvent(&stop_event); SDL_PushEvent(&stop_event);
} }
static bool
process_config_packet(struct stream *stream, AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
LOGE("Could not send config packet to recorder");
return false;
}
return true;
}
static bool
process_frame(struct stream *stream, AVPacket *packet) {
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
return false;
}
if (stream->recorder) {
packet->dts = packet->pts;
if (!recorder_push(stream->recorder, packet)) {
LOGE("Could not send packet to recorder");
return false;
}
}
return true;
}
static bool
stream_parse(struct stream *stream, AVPacket *packet) {
uint8_t *in_data = packet->data;
int in_len = packet->size;
uint8_t *out_data = NULL;
int out_len = 0;
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
&out_data, &out_len, in_data, in_len,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
SDL_assert(r == in_len);
SDL_assert(out_len == in_len);
if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;
}
bool ok = process_frame(stream, packet);
if (!ok) {
LOGE("Could not process frame");
return false;
}
return true;
}
static bool
stream_push_packet(struct stream *stream, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (stream->has_pending || is_config) {
size_t offset;
if (stream->has_pending) {
offset = stream->pending.size;
if (av_grow_packet(&stream->pending, packet->size)) {
LOGE("Could not grow packet");
return false;
}
} else {
offset = 0;
if (av_new_packet(&stream->pending, packet->size)) {
LOGE("Could not create packet");
return false;
}
stream->has_pending = true;
}
memcpy(stream->pending.data + offset, packet->data, packet->size);
if (!is_config) {
// prepare the concat packet to send to the decoder
stream->pending.pts = packet->pts;
stream->pending.dts = packet->dts;
stream->pending.flags = packet->flags;
packet = &stream->pending;
}
}
if (is_config) {
// config packet
bool ok = process_config_packet(stream, packet);
if (!ok) {
return false;
}
} else {
// data packet
bool ok = stream_parse(stream, packet);
if (stream->has_pending) {
// the pending packet must be discarded (consumed or error)
stream->has_pending = false;
av_packet_unref(&stream->pending);
}
if (!ok) {
return false;
}
}
return true;
}
static int static int
run_stream(void *data) { run_stream(void *data) {
struct stream *stream = data; struct stream *stream = data;
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto end;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto finally_free_format_ctx;
}
// initialize the receiver state
stream->receiver_state.frame_meta_queue = NULL;
stream->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets
int (*read_packet)(void *, uint8_t *, int) =
stream->recorder ? read_packet_with_meta : read_raw_packet;
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream,
read_packet, NULL, NULL);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto finally_free_format_ctx;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
goto finally_free_avio_ctx;
}
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) { if (!codec) {
LOGE("H.264 decoder not found"); LOGE("H.264 decoder not found");
goto end; goto end;
} }
stream->codec_ctx = avcodec_alloc_context3(codec);
if (!stream->codec_ctx) {
LOGC("Could not allocate codec context");
goto end;
}
if (stream->decoder && !decoder_open(stream->decoder, codec)) { if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder"); LOGE("Could not open decoder");
goto finally_close_input; goto finally_free_codec_ctx;
} }
if (stream->recorder && !recorder_open(stream->recorder, codec)) { if (stream->recorder) {
LOGE("Could not open recorder"); if (!recorder_open(stream->recorder, codec)) {
goto finally_close_input; LOGE("Could not open recorder");
goto finally_close_decoder;
}
if (!recorder_start(stream->recorder)) {
LOGE("Could not start recorder");
goto finally_close_recorder;
}
} }
AVPacket packet; stream->parser = av_parser_init(AV_CODEC_ID_H264);
av_init_packet(&packet); if (!stream->parser) {
packet.data = NULL; LOGE("Could not initialize parser");
packet.size = 0; goto finally_stop_and_join_recorder;
}
while (!av_read_frame(format_ctx, &packet)) { // We must only pass complete frames to av_parser_parse2()!
if (stream->decoder && !decoder_push(stream->decoder, &packet)) { // It's more complicated, but this allows to reduce the latency by 1 frame!
av_packet_unref(&packet); stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
goto quit;
} for (;;) {
AVPacket packet;
if (stream->recorder) { bool ok = stream_recv_packet(stream, &packet);
// we retrieve the PTS in order they were received, so they will if (!ok) {
// be assigned to the correct frame // end of stream
uint64_t pts = receiver_state_take_meta(&stream->receiver_state); break;
packet.pts = pts;
packet.dts = pts;
// no need to rescale with av_packet_rescale_ts(), the timestamps
// are in microseconds both in input and output
if (!recorder_write(stream->recorder, &packet)) {
LOGE("Could not write frame to output file");
av_packet_unref(&packet);
goto quit;
}
} }
ok = stream_push_packet(stream, &packet);
av_packet_unref(&packet); av_packet_unref(&packet);
if (!ok) {
if (avio_ctx->eof_reached) { // cannot process packet (error already logged)
break; break;
} }
} }
LOGD("End of frames"); LOGD("End of frames");
quit: if (stream->has_pending) {
av_packet_unref(&stream->pending);
}
av_parser_close(stream->parser);
finally_stop_and_join_recorder:
if (stream->recorder) {
recorder_stop(stream->recorder);
LOGI("Finishing recording...");
recorder_join(stream->recorder);
}
finally_close_recorder:
if (stream->recorder) { if (stream->recorder) {
recorder_close(stream->recorder); recorder_close(stream->recorder);
} }
finally_close_input: finally_close_decoder:
avformat_close_input(&format_ctx); if (stream->decoder) {
finally_free_avio_ctx: decoder_close(stream->decoder);
av_free(avio_ctx->buffer); }
av_free(avio_ctx); finally_free_codec_ctx:
finally_free_format_ctx: avcodec_free_context(&stream->codec_ctx);
avformat_free_context(format_ctx);
end: end:
notify_stopped(); notify_stopped();
return 0; return 0;
} }
void bool
stream_init(struct stream *stream, socket_t socket, stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder) { struct decoder *decoder, struct recorder *recorder) {
if (!buffered_reader_init(&stream->buffered_reader, socket,
STREAM_BUFSIZE)) {
return false;
}
stream->socket = socket; stream->socket = socket;
stream->decoder = decoder, stream->decoder = decoder,
stream->recorder = recorder; stream->recorder = recorder;
stream->has_pending = false;
return true;
}
void
stream_destroy(struct stream *stream) {
buffered_reader_destroy(&stream->buffered_reader);
} }
bool bool

View File

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

View File

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

View File

@ -33,7 +33,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
wchar_t *wide = utf8_to_wide_char(cmd); wchar_t *wide = utf8_to_wide_char(cmd);
if (!wide) { if (!wide) {
LOGC("Cannot allocate wide char string"); LOGC("Could not allocate wide char string");
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
@ -44,7 +44,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
#endif #endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) { &pi)) {
free(wide); SDL_free(wide);
*handle = NULL; *handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) { if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY; return PROCESS_ERROR_MISSING_BINARY;
@ -52,7 +52,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
free(wide); SDL_free(wide);
*handle = pi.hProcess; *handle = pi.hProcess;
return PROCESS_SUCCESS; return PROCESS_SUCCESS;
} }
@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code; DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) { || !GetExitCodeProcess(handle, &code)) {
// cannot wait or retrieve the exit code // could not wait or retrieve the exit code
code = -1; // max value, it's unsigned code = -1; // max value, it's unsigned
} }
if (exit_code) { if (exit_code) {
@ -75,3 +75,18 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
} }
return !code; return !code;
} }
char *
get_executable_path(void) {
HMODULE hModule = GetModuleHandleW(NULL);
if (!hModule) {
return NULL;
}
WCHAR buf[MAX_PATH + 1]; // +1 for the null byte
int len = GetModuleFileNameW(hModule, buf, MAX_PATH);
if (!len) {
return NULL;
}
buf[len] = '\0';
return utf8_from_wide_char(buf);
}

View File

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

View File

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

View File

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

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

@ -0,0 +1,73 @@
#include <assert.h>
#include <string.h>
#include "cbuf.h"
struct int_queue CBUF(int, 32);
static void test_cbuf_empty(void) {
struct int_queue queue;
cbuf_init(&queue);
assert(cbuf_is_empty(&queue));
bool push_ok = cbuf_push(&queue, 42);
assert(push_ok);
assert(!cbuf_is_empty(&queue));
int item;
bool take_ok = cbuf_take(&queue, &item);
assert(take_ok);
assert(cbuf_is_empty(&queue));
bool take_empty_ok = cbuf_take(&queue, &item);
assert(!take_empty_ok); // the queue is empty
}
static void test_cbuf_full(void) {
struct int_queue queue;
cbuf_init(&queue);
assert(!cbuf_is_full(&queue));
// fill the queue
for (int i = 0; i < 32; ++i) {
bool ok = cbuf_push(&queue, i);
assert(ok);
}
bool ok = cbuf_push(&queue, 42);
assert(!ok); // the queue if full
int item;
bool take_ok = cbuf_take(&queue, &item);
assert(take_ok);
assert(!cbuf_is_full(&queue));
}
static void test_cbuf_push_take(void) {
struct int_queue queue;
cbuf_init(&queue);
bool push1_ok = cbuf_push(&queue, 42);
assert(push1_ok);
bool push2_ok = cbuf_push(&queue, 35);
assert(push2_ok);
int item;
bool take1_ok = cbuf_take(&queue, &item);
assert(take1_ok);
assert(item == 42);
bool take2_ok = cbuf_take(&queue, &item);
assert(take2_ok);
assert(item == 35);
}
int main(void) {
test_cbuf_empty();
test_cbuf_full();
test_cbuf_push_take();
return 0;
}

View File

@ -1,95 +0,0 @@
#include <assert.h>
#include <string.h>
#include "control_event.h"
static void test_control_event_queue_empty(void) {
struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
assert(control_event_queue_is_empty(&queue));
struct control_event dummy_event;
SDL_bool push_ok = control_event_queue_push(&queue, &dummy_event);
assert(push_ok);
assert(!control_event_queue_is_empty(&queue));
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event);
assert(take_ok);
assert(control_event_queue_is_empty(&queue));
SDL_bool take_empty_ok = control_event_queue_take(&queue, &dummy_event);
assert(!take_empty_ok); // the queue is empty
control_event_queue_destroy(&queue);
}
static void test_control_event_queue_full(void) {
struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
assert(!control_event_queue_is_full(&queue));
struct control_event dummy_event;
// fill the queue
while (control_event_queue_push(&queue, &dummy_event));
SDL_bool take_ok = control_event_queue_take(&queue, &dummy_event);
assert(take_ok);
assert(!control_event_queue_is_full(&queue));
control_event_queue_destroy(&queue);
}
static void test_control_event_queue_push_take(void) {
struct control_event_queue queue;
SDL_bool init_ok = control_event_queue_init(&queue);
assert(init_ok);
struct control_event event = {
.type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = {
.action = AKEY_EVENT_ACTION_DOWN,
.keycode = AKEYCODE_ENTER,
.metastate = AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON,
},
};
SDL_bool push1_ok = control_event_queue_push(&queue, &event);
assert(push1_ok);
event = (struct control_event) {
.type = CONTROL_EVENT_TYPE_TEXT,
.text_event = {
.text = "abc",
},
};
SDL_bool push2_ok = control_event_queue_push(&queue, &event);
assert(push2_ok);
// overwrite event
SDL_bool take1_ok = control_event_queue_take(&queue, &event);
assert(take1_ok);
assert(event.type == CONTROL_EVENT_TYPE_KEYCODE);
assert(event.keycode_event.action == AKEY_EVENT_ACTION_DOWN);
assert(event.keycode_event.keycode == AKEYCODE_ENTER);
assert(event.keycode_event.metastate == (AMETA_CTRL_LEFT_ON | AMETA_CTRL_ON));
// overwrite event
SDL_bool take2_ok = control_event_queue_take(&queue, &event);
assert(take2_ok);
assert(event.type == CONTROL_EVENT_TYPE_TEXT);
assert(!strcmp(event.text_event.text, "abc"));
control_event_queue_destroy(&queue);
}
int main(void) {
test_control_event_queue_empty();
test_control_event_queue_full();
test_control_event_queue_push_take();
return 0;
}

View File

@ -1,141 +0,0 @@
#include <assert.h>
#include <string.h>
#include "control_event.h"
static void test_serialize_keycode_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_KEYCODE,
.keycode_event = {
.action = AKEY_EVENT_ACTION_UP,
.keycode = AKEYCODE_ENTER,
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 10);
const unsigned char expected[] = {
0x00, // CONTROL_EVENT_TYPE_KEYCODE
0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_text_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_TEXT,
.text_event = {
.text = "hello, world!",
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 16);
const unsigned char expected[] = {
0x01, // CONTROL_EVENT_TYPE_KEYCODE
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_long_text_event(void) {
struct control_event event;
event.type = CONTROL_EVENT_TYPE_TEXT;
char text[TEXT_MAX_LENGTH];
memset(text, 'a', sizeof(text));
event.text_event.text = text;
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 3 + sizeof(text));
unsigned char expected[3 + TEXT_MAX_LENGTH];
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE
expected[1] = 0x01;
expected[2] = 0x2c; // text length (16 bits)
memset(&expected[3], 'a', TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_mouse_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_MOUSE,
.mouse_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 18);
const unsigned char expected[] = {
0x02, // CONTROL_EVENT_TYPE_MOUSE
0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_scroll_event(void) {
struct control_event event = {
.type = CONTROL_EVENT_TYPE_SCROLL,
.scroll_event = {
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
.hscroll = 1,
.vscroll = -1,
},
};
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 21);
const unsigned char expected[] = {
0x03, // CONTROL_EVENT_TYPE_SCROLL
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1
0xFF, 0xFF, 0xFF, 0xFF, // -1
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) {
test_serialize_keycode_event();
test_serialize_text_event();
test_serialize_long_text_event();
test_serialize_mouse_event();
test_serialize_scroll_event();
}

View File

@ -0,0 +1,248 @@
#include <assert.h>
#include <string.h>
#include "control_msg.h"
static void test_serialize_inject_keycode(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_KEYCODE,
.inject_keycode = {
.action = AKEY_EVENT_ACTION_UP,
.keycode = AKEYCODE_ENTER,
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 10);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_text(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_TEXT,
.inject_text = {
.text = "hello, world!",
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 16);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_text_long(void) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1];
memset(text, 'a', sizeof(text));
text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text;
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH);
unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH];
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x01;
expected[2] = 0x2c; // text length (16 bits)
memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_mouse_event(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
.inject_mouse_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 18);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_scroll_event(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
.inject_scroll_event = {
.position = {
.point = {
.x = 260,
.y = 1026,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
.hscroll = 1,
.vscroll = -1,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 21);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1
0xFF, 0xFF, 0xFF, 0xFF, // -1
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_back_or_screen_on(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_expand_notification_panel(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_collapse_notification_panel(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_get_clipboard(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_GET_CLIPBOARD,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_set_clipboard(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
.inject_text = {
.text = "hello, world!",
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 16);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_set_screen_power_mode(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
.set_screen_power_mode = {
.mode = SCREEN_POWER_MODE_NORMAL,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 2);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x02, // SCREEN_POWER_MODE_NORMAL
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(void) {
test_serialize_inject_keycode();
test_serialize_inject_text();
test_serialize_inject_text_long();
test_serialize_inject_mouse_event();
test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel();
test_serialize_collapse_notification_panel();
test_serialize_get_clipboard();
test_serialize_set_clipboard();
test_serialize_set_screen_power_mode();
return 0;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,107 +0,0 @@
package com.genymobile.scrcpy;
/**
* Union of all supported event types, identified by their {@code type}.
*/
public final class ControlEvent {
public static final int TYPE_KEYCODE = 0;
public static final int TYPE_TEXT = 1;
public static final int TYPE_MOUSE = 2;
public static final int TYPE_SCROLL = 3;
public static final int TYPE_COMMAND = 4;
public static final int COMMAND_BACK_OR_SCREEN_ON = 0;
public static final int COMMAND_EXPAND_NOTIFICATION_PANEL = 1;
public static final int COMMAND_COLLAPSE_NOTIFICATION_PANEL = 2;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or COMMAND_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private Position position;
private int hScroll;
private int vScroll;
private ControlEvent() {
}
public static ControlEvent createKeycodeControlEvent(int action, int keycode, int metaState) {
ControlEvent event = new ControlEvent();
event.type = TYPE_KEYCODE;
event.action = action;
event.keycode = keycode;
event.metaState = metaState;
return event;
}
public static ControlEvent createTextControlEvent(String text) {
ControlEvent event = new ControlEvent();
event.type = TYPE_TEXT;
event.text = text;
return event;
}
public static ControlEvent createMotionControlEvent(int action, int buttons, Position position) {
ControlEvent event = new ControlEvent();
event.type = TYPE_MOUSE;
event.action = action;
event.buttons = buttons;
event.position = position;
return event;
}
public static ControlEvent createScrollControlEvent(Position position, int hScroll, int vScroll) {
ControlEvent event = new ControlEvent();
event.type = TYPE_SCROLL;
event.position = position;
event.hScroll = hScroll;
event.vScroll = vScroll;
return event;
}
public static ControlEvent createCommandControlEvent(int action) {
ControlEvent event = new ControlEvent();
event.type = TYPE_COMMAND;
event.action = action;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
public int getMetaState() {
return metaState;
}
public int getAction() {
return action;
}
public int getKeycode() {
return keycode;
}
public int getButtons() {
return buttons;
}
public Position getPosition() {
return position;
}
public int getHScroll() {
return hScroll;
}
public int getVScroll() {
return vScroll;
}
}

View File

@ -1,151 +0,0 @@
package com.genymobile.scrcpy;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ControlEventReader {
private static final int KEYCODE_PAYLOAD_LENGTH = 9;
private static final int MOUSE_PAYLOAD_LENGTH = 17;
private static final int SCROLL_PAYLOAD_LENGTH = 20;
private static final int COMMAND_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300;
private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[TEXT_MAX_LENGTH];
public ControlEventReader() {
// invariant: the buffer is always in "get" mode
buffer.limit(0);
}
public boolean isFull() {
return buffer.remaining() == rawBuffer.length;
}
public void readFrom(InputStream input) throws IOException {
if (isFull()) {
throw new IllegalStateException("Buffer full, call next() to consume");
}
buffer.compact();
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
throw new EOFException("Event controller socket closed");
}
buffer.position(head + r);
buffer.flip();
}
public ControlEvent next() {
if (!buffer.hasRemaining()) {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlEvent controlEvent;
switch (type) {
case ControlEvent.TYPE_KEYCODE:
controlEvent = parseKeycodeControlEvent();
break;
case ControlEvent.TYPE_TEXT:
controlEvent = parseTextControlEvent();
break;
case ControlEvent.TYPE_MOUSE:
controlEvent = parseMouseControlEvent();
break;
case ControlEvent.TYPE_SCROLL:
controlEvent = parseScrollControlEvent();
break;
case ControlEvent.TYPE_COMMAND:
controlEvent = parseCommandControlEvent();
break;
default:
Ln.w("Unknown event type: " + type);
controlEvent = null;
break;
}
if (controlEvent == null) {
// failure, reset savedPosition
buffer.position(savedPosition);
}
return controlEvent;
}
private ControlEvent parseKeycodeControlEvent() {
if (buffer.remaining() < KEYCODE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int keycode = buffer.getInt();
int metaState = buffer.getInt();
return ControlEvent.createKeycodeControlEvent(action, keycode, metaState);
}
private ControlEvent parseTextControlEvent() {
if (buffer.remaining() < 1) {
return null;
}
int len = toUnsigned(buffer.getShort());
if (buffer.remaining() < len) {
return null;
}
buffer.get(textBuffer, 0, len);
String text = new String(textBuffer, 0, len, StandardCharsets.UTF_8);
return ControlEvent.createTextControlEvent(text);
}
private ControlEvent parseMouseControlEvent() {
if (buffer.remaining() < MOUSE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int buttons = buffer.getInt();
Position position = readPosition(buffer);
return ControlEvent.createMotionControlEvent(action, buttons, position);
}
private ControlEvent parseScrollControlEvent() {
if (buffer.remaining() < SCROLL_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
int hScroll = buffer.getInt();
int vScroll = buffer.getInt();
return ControlEvent.createScrollControlEvent(position, hScroll, vScroll);
}
private ControlEvent parseCommandControlEvent() {
if (buffer.remaining() < COMMAND_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
return ControlEvent.createCommandControlEvent(action);
}
private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(short value) {
return value & 0xffff;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(byte value) {
return value & 0xff;
}
}

View File

@ -0,0 +1,124 @@
package com.genymobile.scrcpy;
/**
* Union of all supported event types, identified by their {@code type}.
*/
public final class ControlMessage {
public static final int TYPE_INJECT_KEYCODE = 0;
public static final int TYPE_INJECT_TEXT = 1;
public static final int TYPE_INJECT_MOUSE_EVENT = 2;
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private Position position;
private int hScroll;
private int vScroll;
private ControlMessage() {
}
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_KEYCODE;
event.action = action;
event.keycode = keycode;
event.metaState = metaState;
return event;
}
public static ControlMessage createInjectText(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_TEXT;
event.text = text;
return event;
}
public static ControlMessage createInjectMouseEvent(int action, int buttons, Position position) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_MOUSE_EVENT;
event.action = action;
event.buttons = buttons;
event.position = position;
return event;
}
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_SCROLL_EVENT;
event.position = position;
event.hScroll = hScroll;
event.vScroll = vScroll;
return event;
}
public static ControlMessage createSetClipboard(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_CLIPBOARD;
event.text = text;
return event;
}
/**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_SCREEN_POWER_MODE;
event.action = mode;
return event;
}
public static ControlMessage createEmpty(int type) {
ControlMessage event = new ControlMessage();
event.type = type;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
public int getMetaState() {
return metaState;
}
public int getAction() {
return action;
}
public int getKeycode() {
return keycode;
}
public int getButtons() {
return buttons;
}
public Position getPosition() {
return position;
}
public int getHScroll() {
return hScroll;
}
public int getVScroll() {
return vScroll;
}
}

View File

@ -0,0 +1,176 @@
package com.genymobile.scrcpy;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
public static final int TEXT_MAX_LENGTH = 300;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlMessageReader() {
// invariant: the buffer is always in "get" mode
buffer.limit(0);
}
public boolean isFull() {
return buffer.remaining() == rawBuffer.length;
}
public void readFrom(InputStream input) throws IOException {
if (isFull()) {
throw new IllegalStateException("Buffer full, call next() to consume");
}
buffer.compact();
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
throw new EOFException("Controller socket closed");
}
buffer.position(head + r);
buffer.flip();
}
public ControlMessage next() {
if (!buffer.hasRemaining()) {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlMessage msg;
switch (type) {
case ControlMessage.TYPE_INJECT_KEYCODE:
msg = parseInjectKeycode();
break;
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
msg = parseInjectMouseEvent();
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode();
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
msg = ControlMessage.createEmpty(type);
break;
default:
Ln.w("Unknown event type: " + type);
msg = null;
break;
}
if (msg == null) {
// failure, reset savedPosition
buffer.position(savedPosition);
}
return msg;
}
private ControlMessage parseInjectKeycode() {
if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int keycode = buffer.getInt();
int metaState = buffer.getInt();
return ControlMessage.createInjectKeycode(action, keycode, metaState);
}
private String parseString() {
if (buffer.remaining() < 2) {
return null;
}
int len = toUnsigned(buffer.getShort());
if (buffer.remaining() < len) {
return null;
}
buffer.get(textBuffer, 0, len);
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
}
private ControlMessage parseInjectText() {
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectMouseEvent() {
if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int buttons = buffer.getInt();
Position position = readPosition(buffer);
return ControlMessage.createInjectMouseEvent(action, buttons, position);
}
private ControlMessage parseInjectScrollEvent() {
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
int hScroll = buffer.getInt();
int vScroll = buffer.getInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
}
private ControlMessage parseSetClipboard() {
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(text);
}
private ControlMessage parseSetScreenPowerMode() {
if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) {
return null;
}
int mode = buffer.get();
return ControlMessage.createSetScreenPowerMode(mode);
}
private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(short value) {
return value & 0xffff;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int toUnsigned(byte value) {
return value & 0xff;
}
}

View File

@ -1,9 +1,7 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Point;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.InputEvent; import android.view.InputEvent;
@ -13,11 +11,11 @@ import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
public class Controller {
public class EventController {
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@ -25,10 +23,11 @@ public class EventController {
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()}; private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()}; private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
public EventController(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
initPointer(); initPointer();
sender = new DeviceMessageSender(connection);
} }
private void initPointer() { private void initPointer() {
@ -44,8 +43,8 @@ public class EventController {
private void setPointerCoords(Point point) { private void setPointerCoords(Point point) {
MotionEvent.PointerCoords coords = pointerCoords[0]; MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.x; coords.x = point.getX();
coords.y = point.y; coords.y = point.getY();
} }
private void setScroll(int hScroll, int vScroll) { private void setScroll(int hScroll, int vScroll) {
@ -54,32 +53,64 @@ public class EventController {
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
} }
@SuppressWarnings("checkstyle:MagicNumber")
public void control() throws IOException { public void control() throws IOException {
// on start, turn screen on // on start, power on the device
turnScreenOn(); if (!device.isScreenOn()) {
injectKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on.
// Adding this delay prevents to handle the message before the device is actually
// powered on.
SystemClock.sleep(500);
}
while (true) { while (true) {
handleEvent(); handleEvent();
} }
} }
public DeviceMessageSender getSender() {
return sender;
}
private void handleEvent() throws IOException { private void handleEvent() throws IOException {
ControlEvent controlEvent = connection.receiveControlEvent(); ControlMessage msg = connection.receiveControlMessage();
switch (controlEvent.getType()) { switch (msg.getType()) {
case ControlEvent.TYPE_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
injectKeycode(controlEvent.getAction(), controlEvent.getKeycode(), controlEvent.getMetaState()); injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
break; break;
case ControlEvent.TYPE_TEXT: case ControlMessage.TYPE_INJECT_TEXT:
injectText(controlEvent.getText()); injectText(msg.getText());
break; break;
case ControlEvent.TYPE_MOUSE: case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
injectMouse(controlEvent.getAction(), controlEvent.getButtons(), controlEvent.getPosition()); injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
break; break;
case ControlEvent.TYPE_SCROLL: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(controlEvent.getPosition(), controlEvent.getHScroll(), controlEvent.getVScroll()); injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
break; break;
case ControlEvent.TYPE_COMMAND: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
executeCommand(controlEvent.getAction()); pressBackOrTurnScreenOn();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText();
sender.pushClipboardText(clipboardText);
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText());
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
break; break;
default: default:
// do nothing // do nothing
@ -92,7 +123,7 @@ public class EventController {
private boolean injectChar(char c) { private boolean injectChar(char c) {
String decomposed = KeyComposition.decompose(c); String decomposed = KeyComposition.decompose(c);
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[] {c}; char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c};
KeyEvent[] events = charMap.getEvents(chars); KeyEvent[] events = charMap.getEvents(chars);
if (events == null) { if (events == null) {
return false; return false;
@ -105,13 +136,16 @@ public class EventController {
return true; return true;
} }
private boolean injectText(String text) { private int injectText(String text) {
int successCount = 0;
for (char c : text.toCharArray()) { for (char c : text.toCharArray()) {
if (!injectChar(c)) { if (!injectChar(c)) {
return false; Ln.w("Could not inject char u+" + String.format("%04x", (int) c));
continue;
} }
successCount++;
} }
return true; return successCount;
} }
private boolean injectMouse(int action, int buttons, Position position) { private boolean injectMouse(int action, int buttons, Position position) {
@ -160,28 +194,8 @@ public class EventController {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} }
private boolean turnScreenOn() {
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
}
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode); return injectKeycode(keycode);
} }
private boolean executeCommand(int action) {
switch (action) {
case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
return pressBackOrTurnScreenOn();
case ControlEvent.COMMAND_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
return true;
case ControlEvent.COMMAND_COLLAPSE_NOTIFICATION_PANEL:
device.collapsePanels();
return true;
default:
Ln.w("Unsupported command: " + action);
}
return false;
}
} }

View File

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

View File

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

View File

@ -0,0 +1,27 @@
package com.genymobile.scrcpy;
public final class DeviceMessage {
public static final int TYPE_CLIPBOARD = 0;
private int type;
private String text;
private DeviceMessage() {
}
public static DeviceMessage createClipboard(String text) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_CLIPBOARD;
event.text = text;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
}

View File

@ -0,0 +1,34 @@
package com.genymobile.scrcpy;
import java.io.IOException;
public final class DeviceMessageSender {
private final DesktopConnection connection;
private String clipboardText;
public DeviceMessageSender(DesktopConnection connection) {
this.connection = connection;
}
public synchronized void pushClipboardText(String text) {
clipboardText = text;
notify();
}
public void loop() throws IOException, InterruptedException {
while (true) {
String text;
synchronized (this) {
while (clipboardText == null) {
wait();
}
text = clipboardText;
clipboardText = null;
}
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
}
}

View File

@ -0,0 +1,34 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter {
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3;
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
@SuppressWarnings("checkstyle:MagicNumber")
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
buffer.clear();
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);
switch (msg.getType()) {
case DeviceMessage.TYPE_CLIPBOARD:
String text = msg.getText();
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
buffer.putShort((short) len);
buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position());
break;
default:
Ln.w("Unknown device message: " + msg.getType());
break;
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
package com.genymobile.scrcpy;
import java.util.Objects;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Point point = (Point) o;
return x == point.x
&& y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{"
+ "x=" + x
+ ", y=" + y
+ '}';
}
}

View File

@ -1,7 +1,5 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Point;
import java.util.Objects; import java.util.Objects;
public class Position { public class Position {

View File

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

View File

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

View File

@ -0,0 +1,23 @@
package com.genymobile.scrcpy;
public final class StringUtils {
private StringUtils() {
// not instantiable
}
@SuppressWarnings("checkstyle:MagicNumber")
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
int len = utf8.length;
if (len <= maxLength) {
return len;
}
len = maxLength;
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
// the next byte is not the start of a new UTF-8 codepoint
// so if we would cut there, the character would be truncated
len--;
}
return len;
}
}

View File

@ -0,0 +1,44 @@
package com.genymobile.scrcpy.wrappers;
import android.content.ClipData;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClipboardManager {
private final IInterface manager;
private final Method getPrimaryClipMethod;
private final Method setPrimaryClipMethod;
public ClipboardManager(IInterface manager) {
this.manager = manager;
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} catch (NoSuchMethodException e) {
throw new AssertionError(e);
}
}
public CharSequence getText() {
try {
ClipData clipData = (ClipData) getPrimaryClipMethod.invoke(manager, "com.android.shell");
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
public void setText(CharSequence text) {
ClipData clipData = ClipData.newPlainText(null, text);
try {
setPrimaryClipMethod.invoke(manager, clipData, "com.android.shell");
} catch (InvocationTargetException | IllegalAccessException e) {
throw new AssertionError(e);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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