Compare commits

...

247 Commits

Author SHA1 Message Date
ad3632f1ff Add --codec-options
Co-authored-by: Romain Vimont <rom@rom1v.com>
2020-05-04 01:17:02 +02:00
c77024314d Add an option to keep the device awake
Add an option to prevent the device to sleep:

    scrcpy --stay-awake
    scrcpy -w

The initial state is restored on exit.

Fixes #631 <https://github.com/Genymobile/scrcpy/issues/631>
2020-05-02 02:14:25 +02:00
828327365a Reorder options in alphabetical order 2020-05-02 01:55:32 +02:00
4668638ee1 Handle "show touches" on the device-side
Now that the server can access the Android settings and clean up
properly, handle the "show touches" option from the server.

The initial state is now correctly restored, even on device
disconnection.
2020-05-02 01:55:30 +02:00
dbb0df607c Move constants to ServiceManager
PACKAGE_NAME and USER_ID could be use by several "managers", so move
them to the service manager.
2020-05-02 01:22:18 +02:00
2f74ec2518 Add a clean up process on the device
In order to clean up on close, use a separate process which is not
killed when the device is disconnected (even if the main process itself
is killed).
2020-05-02 01:22:18 +02:00
8c6799297b Implement access to settings without Context
Expose methods to access the Android settings using private APIs.

This allows to read and write settings values immediately without
starting a new process to call "settings".
2020-05-02 01:22:10 +02:00
62c0c1321f Apply workarounds only on error
To avoid NullPointerException on some devices, workarounds have been
implemented. But these workaround produce (harmless) internal errors
causing exceptions to be printed in the console.

To avoid this problem, apply the workarounds only if it fails without
them.

Fixes #994 <https://github.com/Genymobile/scrcpy/issues/994>
Refs #365 <https://github.com/Genymobile/scrcpy/issues/365>
Refs #940 <https://github.com/Genymobile/scrcpy/issues/940>
2020-05-01 19:42:31 +02:00
d4eeb1c84d Fix AutoAdb url
PR #1344 <https://github.com/Genymobile/scrcpy/pull/1344>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-05-01 19:34:08 +02:00
4e9e712312 Update links to v1.13 in README and BUILD 2020-04-29 22:56:26 +02:00
9babe26805 Bump version to 1.13 2020-04-29 22:24:08 +02:00
76567e684a Upgrade SDL (2.0.12) for Windows
Include the latest version of SDL in Windows releases.
2020-04-27 21:45:15 +02:00
b55ca127f8 Upgrade FFmpeg (4.2.2) for Windows
Include the latest version of FFmpeg in Windows releases.
2020-04-27 21:45:15 +02:00
a12b938234 Merge branch 'master' into dev 2020-04-27 21:44:59 +02:00
a14840a515 Fix typo in comments 2020-04-24 23:01:58 +02:00
8581d6850b Stabilize auto-resize
The window dimensions are integers, so resizing to fit the content may
not be exact.

When computing the optimal size, it could cause to reduce alternatively
the width and height by few pixels, making the "optimal size" unstable.

To avoid this problem, check if the optimal size is already correct
either by keeping the width or the height.
2020-04-24 22:52:02 +02:00
92cb3a6661 Improve resizing workaround
Call the same method as when the event is received on the event loop, so
that the behavior is the same in both cases.
2020-04-24 21:36:25 +02:00
561ede444e Mention Ubuntu 20.04 package
Ubuntu 20.04 has been released today, and scrcpy is available in their
repositories: <https://packages.ubuntu.com/focal/scrcpy>
2020-04-23 16:36:48 +02:00
3c9ae99dda Move rotation coordinates to screen
Move the window-to-frame coordinates conversion from the input manager
to the screen.

This will allow to apply more screen-related transformations without
impacting the input manager.
2020-04-18 02:15:22 +02:00
44f720e4a4 Log new size on auto-resize request
On "resize to fit" and "resize to pixel-perfect", log the new size.
2020-04-18 02:15:22 +02:00
125c5561e8 Use MediaFormat constant for MIME type
Replace "video/avc" by MIMETYPE_VIDEO_AVC.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-04-18 02:14:42 +02:00
14ead499fd Fix touch coordinates on rotated display
The touch coordinates were not rotated.
2020-04-17 18:17:12 +02:00
94a7f1a0f8 Disable input events when necessary
Disable input events on secondary displays before Android 10, even if
FLAG_PRESENTATION is not set.

Refs #1288 <https://github.com/Genymobile/scrcpy/issues/1288>
2020-04-16 20:54:00 +02:00
cc22f4622a Mention mipmapping in FAQ 2020-04-15 17:39:51 +02:00
11a61b2cb3 Add option --no-mipmaps
Add an option to disable trilinear filtering even if mipmapping is
available.
2020-04-15 17:39:51 +02:00
bea7658807 Enable trilinear filtering for OpenGL
Improve downscaling quality if mipmapping is available.

Suggested-by: Giumo Clanjor (哆啦比猫/兰威举) <cjxgm2@gmail.com>

Fixes #40 <https://github.com/Genymobile/scrcpy/issues/40>
Ref: <https://github.com/Genymobile/scrcpy/issues/40#issuecomment-591917787>
2020-04-15 17:39:51 +02:00
8a9b20b27e Add --render-driver command-line option
Add an option to set a render driver hint (SDL_HINT_RENDER_DRIVER).
2020-04-15 17:39:51 +02:00
d62eb2b11c Fix typo in README 2020-04-15 09:57:59 +02:00
eb8f7a1f28 Require Meson 0.48 to get rid of warnings
Debian buster (stable) provides Meson 0.49, which is also available in
stretch (oldstable) backports. It's time to abandon Meson 0.37.

Ref: 20b3f101a4
2020-04-13 22:47:03 +02:00
270d0bf639 Rename max length constant for text injection
To avoid confusion with the max text size for clipboard, rename the
constant limiting the text injection length.
2020-04-13 19:38:43 +02:00
95fa1a69e4 Workaround compiler warning
Some compilers warns on uninitialized value in impossible case:

    warning: variable 'result' is used uninitialized whenever switch
    default is taken [-Wsometimes-uninitialized]
2020-04-13 16:33:21 +02:00
ea46d3ab68 Add missing include string.h
Include <string.h> for strdup() and strtok_r().
2020-04-13 16:33:21 +02:00
7eb16ce364 Fix log format warning
The expression port + 1 is promoted to int, but printed as uint16_t.
2020-04-13 16:33:19 +02:00
927d655ff6 Simplify ScreenEncoder
Do not handle iFrameInterval field and parameter, it is never used
dynamically.
2020-04-12 02:09:28 +02:00
ee2894779a Remove unused lockedVideoOrientation field
During PR #1151, this field has been moved to ScreenInfo, but has not
been removed from ScreenEncoder.
2020-04-12 02:08:16 +02:00
1c6207f8ce Merge branch 'master' into dev 2020-04-12 00:31:57 +02:00
ab52b36895 Reorder options in alphabetical order 2020-04-11 14:31:14 +02:00
9f4735ede3 Fix double click on rotated display
A double-click outside the device content (in the black borders) resizes
so that black borders are removed. But the display rotation was not
taken into account to detect the content.

Use the content size instead of the frame size to fix the issue.

Ref: <https://github.com/Genymobile/scrcpy/issues/898#issuecomment-610993695>
2020-04-08 16:37:33 +02:00
6295c1a110 Remap event positions on rotated display
If the display is rotated, the position of clicks must be adapted.
2020-04-08 14:27:25 +02:00
f3fba3c4b9 Store rotated content size
This avoids to compute it every time from the frame size.
2020-04-08 14:12:54 +02:00
c1ebea26e6 Register rotation watcher on selected display
PR #1275 <https://github.com/Genymobile/scrcpy/pull/1275>

Signed-off-by: Kostiantyn Luzan <vblack2006@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-04-08 12:09:24 +02:00
f07d21f050 Suppress DiscouragedPrivateApi lint warning 2020-04-08 12:09:24 +02:00
a8fd4aec9a Remove --fullscreen validation
Many options are meaningless if --no-display is set.

We don't want to validate all possible combinations, so don't make an
exception for --fullscreen.
2020-04-08 12:09:24 +02:00
cbde7b964a Improve documentation for consistency
Make --lock-video-orientation documentation consistent with that of
--rotation.
2020-04-08 12:09:24 +02:00
28c71c528f Add --rotation command-line option
In addition to Ctrl+Left and Ctrl+Right shortcuts, add a command-line
parameter to set the initial rotation.
2020-04-08 12:09:22 +02:00
d48b375a1d Add shortcuts to rotate display
Add Ctrl+Left and Ctrl+Right shortcuts to rotate the display (the
content of the scrcpy window).

Contrary to --lock-video-orientation, the rotation has no impact on
recording, and can be changed dynamically (and immediately).

Fixes #218 <https://github.com/Genymobile/scrcpy/issues/218>
2020-04-08 12:02:26 +02:00
fd63e7eb5a Format shortcut documentation
For consistency, start the descriptions with a capital letter.
2020-04-08 12:02:15 +02:00
cdd8edbbb6 Add a note about prebuilt server in BUILD.md
Mention that it works with a matching client version.
2020-04-07 23:06:33 +02:00
9b9e717c41 Explain master and dev branches in BUILD
People may not guess that `master` is not the development branch.
2020-04-07 10:43:20 +02:00
15e4da08a3 Improve "low quality" section in FAQ 2020-04-07 10:37:19 +02:00
2afbfc2c75 Add Android device and version in issue template 2020-04-03 21:29:09 +02:00
2cf022491f Add issue templates
Closes #1157 <https://github.com/Genymobile/scrcpy/issues/1157>
2020-04-03 18:51:18 +02:00
d30593e1d5 gitignore: Add x/ directory
People following default build instructions can be caught off guard by
seeing the build artifacts in the git tree.

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-04-03 18:20:33 +02:00
9e78b765da Update to Gradle 6.3
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Signed-off-by: Romain Vimont <rom@rom1v.com>

-- Note from committer:

The binary gradle/wrapper/gradle-wrapper.jar has the expected SHA-256
checksum:

    $ curl -L https://services.gradle.org/distributions/gradle-6.3-wrapper.jar.sha256
    1cef53de8dc192036e7b0cc47584449b0cf570a00d560bfaa6c9eabe06e1fc06

All the changed files match an upgrade executed independently:
<https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper>
2020-04-03 18:11:35 +02:00
271de0954a Update to AGP 3.6.2
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
2020-04-03 18:10:51 +02:00
54ccccd883 Replace SDL_Atomic by stdatomic from C11
There is no reason to use SDL atomics.
2020-04-02 21:05:26 +02:00
bea1c11f8e Do not log success on failure
If calling the private API does not work, an exception is printed. In
that case, do not log that the action succeeded.
2020-04-02 21:05:26 +02:00
94e1696869 Do not warn on terminating the server
If the server is already dead, terminating it fails. This is expected.
2020-04-02 21:05:26 +02:00
a346bb80f4 Do not block on accept() if server died
The server may die before connecting to the client. In that case, the
client was blocked indefinitely (until Ctrl+C) on accept().

To avoid the problem, close the server socket once the server process is
dead.
2020-04-02 21:05:26 +02:00
d421741a83 Wait server from a separate thread
Create a thread just to wait for the server process exit.

This paves the way to simply wake up a blocking accept() in a portable
way.
2020-04-02 21:05:26 +02:00
64d5edce92 Refactor server_start() error handling
This avoids cleanup duplication.
2020-04-02 21:05:26 +02:00
4150eedcdf Add display id parameter
Add --display command line parameter to specify a display id.

PR #1238 <https://github.com/Genymobile/scrcpy/pull/1238>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-04-02 21:02:52 +02:00
5031b2c8ff Remove MagicNumber checkstyle
There are a lot of "magic numbers" that we really don't want to extract
as a constant.

Until now, many @SuppressWarnings annotations were added, but it makes
no sense to check for magic number if we silent the warnings everywhere.
2020-03-28 22:08:16 +01:00
4adf5fde6d Log device details on server start 2020-03-28 15:52:02 +01:00
e050cfdcd6 Fix static_assert() parameters
In C11, static_assert() expects a message.
2020-03-27 14:01:56 +01:00
dc7c677728 Accept negative window position
It seems to work on some window managers.

Fixes #1242 <https://github.com/Genymobile/scrcpy/issues/1242>
2020-03-26 22:52:41 +01:00
3504c0016b Add tests for control message length
This will avoid regressions for #1245.

<https://github.com/Genymobile/scrcpy/issues/1245>
2020-03-26 22:48:01 +01:00
89d1602185 Fix expected message length for touch events
The expected length for a touch event control message was incorrect. As
a consequence, a BufferUnderflowException could occur.

Fixes #1245 <https://github.com/Genymobile/scrcpy/issues/1245>
2020-03-26 22:45:43 +01:00
566ba766af Remove unused constant
It has not been removed when mouse and touch events have been merged.
2020-03-26 22:43:53 +01:00
a0af402d96 Fix the printed versions (were opposite)
PR #1224 <https://github.com/Genymobile/scrcpy/pull/1224>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-03-19 19:28:57 +01:00
7bb91638ad Improve FAQ 2020-03-19 19:22:58 +01:00
600df37753 Mention AutoAdb in README 2020-03-19 19:16:08 +01:00
902b99174d Fix server debugger for Android >= 9
Add a compilation flag to select the debugger method to use:
 - old: Android < 9
 - new: Android >= 9

See <https://github.com/Genymobile/scrcpy/issues/1187#issuecomment-599075661>
2020-03-19 19:15:43 +01:00
cd69eb4a4f Handle NumPad events when NumLock is disabled
PR #1188 <https://github.com/Genymobile/scrcpy/pull/1188>
Fixes #1048 <https://github.com/Genymobile/scrcpy/issues/1048>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-03-14 17:37:14 +01:00
bc7508427b Add scoop instructions for Windows
PR #1202 <https://github.com/Genymobile/scrcpy/pull/1202>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-03-14 17:11:49 +01:00
24ade6ad77 Simplify Chocolatey documentation 2020-03-14 17:07:20 +01:00
ae2d094362 Handle locked video orientation from ScreenInfo
Centralize video size management in ScreenInfo.

This allows to always send the correct initial video size to the client
if the video orientation is locked.
2020-03-08 10:38:31 +01:00
c5f5d1e456 Rename "rotation" to "device rotation"
This paves the way to reduce confusion in ScreenInfo when it will handle
locked video orientation.
2020-03-04 22:15:10 +01:00
63286424bb Compute all screen info from ScreenInfo
Screen information was partially initialized from Device. Move this
initialization to ScreenInfo.
2020-03-04 22:15:10 +01:00
da18c9cdab Remove useles import 2020-03-04 22:15:10 +01:00
c396758b4e Remove link to Windows 32 bits release
Binaries created with MinGW (even a simple Hello World) are detected as
malware by some anti-virus. For some reason, only the 32 bits version of
scrcpy is impacted.

Since users should use the 64 bits version by default anyway, remove the
link to the 32 bits version from the main page.

The 32 bits release is still available in the "releases" tab.

See <https://github.com/Genymobile/scrcpy/issues/1102>
2020-03-03 21:39:27 +01:00
d3281f4b67 Show a friendly hint for adb installation
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-27 21:28:42 +01:00
1982bc439b Add option to lock video orientation
PR #1151 <https://github.com/Genymobile/scrcpy/pull/1151>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-27 21:24:37 +01:00
ef56cc6ff7 Retrieve screen info once
The method getScreenInfo() is synchronized, and the result may change
between calls.

Call it once and store the result in a local variable.
2020-02-27 21:24:37 +01:00
c0f428eb05 Merge branch 'master' into dev 2020-02-27 21:24:32 +01:00
4794ca8ae7 Use linear filtering
Anisotropic filtering makes no sense for scrcpy use case.

This (semantically) reverts 9e328ef98b.
2020-02-25 12:20:18 +01:00
f903cd376d Documentation rectifications
PR #1151 <https://github.com/Genymobile/scrcpy/pull/1151>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-16 16:04:17 +01:00
e8127375ae Add Chocolatey for Windows install
PR #1144 <https://github.com/Genymobile/scrcpy/pull/1144>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-02-15 22:26:30 +01:00
1144f64214 Indicate that -s can also be used for TCP/IP 2020-02-06 18:42:08 +01:00
39356602ed Mention scrcpy Debian package in README 2020-01-19 16:19:51 +01:00
0fb22c3e98 Happy new year 2020! 2020-01-19 16:04:20 +01:00
96bd2c974d Do not report workarounds errors
Some workarounds are needed on some devices. But applying them may cause
exceptions on other devices, where they are not necessary anyway.

Do not report these errors in release builds.

Closes #994 <https://github.com/Genymobile/scrcpy/issues/994>
2020-01-19 15:52:24 +01:00
dc7fcf3c7a Accept port range
Accept a range of ports to listen to, so that it does not fail if
another instance of scrcpy is currently starting.

The range can be passed via the command line:

    scrcpy -p 27183:27186
    scrcpy -p 27183  # implicitly 27183:27183, as before

The default is 27183:27199.

Closes #951 <https://github.com/Genymobile/scrcpy/issues/951>
2020-01-18 17:21:00 +01:00
2a3a9d4ea9 Add util function to parse a list of integers
This will help parsing arguments like '1234:5678' into a list of
integers.
2020-01-18 17:21:00 +01:00
ca0031cbde Refactor server tunnel initialization
Start the server socket in enable_tunnel() directly.

For the caller point of view, enabling the tunnel opens a port (either
the server socket locally or the "adb forward" process).
2020-01-18 17:21:00 +01:00
d1a9a76cc6 Reorder functions
Move functions so that they can be called from enable_tunnel() (in the
following commit).
2020-01-18 17:21:00 +01:00
a8ceaf5284 Fix include order 2020-01-17 21:00:14 +01:00
83d48267a7 Accept --max-fps before Android 10
KEY_MAX_FPS_TO_ENCODER existed privately before Android 10:
<https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
2019-12-19 11:52:09 +01:00
db6252e52b Simplify net.c
The platform-specific code for net.c was implemented in sys/*/net.c.

But the differences are quite limited, so use ifdef-blocks in the single
net.c instead.
2019-12-15 22:04:09 +01:00
229eeb24a2 Merge pull request #1002 into dev
Fix utf-8 char path in windows

<https://github.com/Genymobile/scrcpy/pull/1002>
2019-12-15 13:24:17 +01:00
f9786e5034 Get env in windows correctly
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-12-14 18:47:54 +01:00
78a320a763 Fix utf-8 char path in windows
The file 'E:\安安\scrcpy-win64-v1.12.1-1-g31bd950\scrcpy-server'
exists, however, it will show msg as follow:

    INFO: scrcpy 1.12.1 <https://github.com/Genymobile/scrcpy>
    stat: No such file or directory
    ERROR: 'E:\安安\scrcpy-win64-v1.12.1-1-g31bd950\scrcpy-server' does
    not exist or is not a regular file
    Press any key to continue...

This patch fixes it.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-12-14 18:47:30 +01:00
7d5845196e Fix memory leak on portable builds
The function get_server_path() sometimes returned an owned string,
sometimes a non-owned string.

Always return an allocated (owned) string, and free it after usage.
2019-12-14 18:23:25 +01:00
31bd95022b Update links to v1.12.1 in README and BUILD 2019-12-10 17:59:44 +01:00
4687a0ebac Bump version to 1.12.1 2019-12-10 10:07:48 +01:00
6965d051ae Limit bitrate range to 31 bits integer
A proper solution could be to use "long long" instead (guaranteed to be
at least 64 bits), but it adds its own problems (e.g. "%lld" is not
supported as a printf format on all platforms).

In practice, we don't need such high values, so keep it simple.

Fixes #995 <https://github.com/Genymobile/scrcpy/issues/995>
2019-12-10 09:28:27 +01:00
71df3175bd Update links to v1.12 in README and BUILD 2019-12-09 23:52:26 +01:00
a0f8e7fd9f Bump version to 1.12 2019-12-09 23:24:43 +01:00
e4cebc8d4c Do not build tests in release mode
Assertions would not be executed.

And as a side effect, it causes "unused variable" warnings.
2019-12-09 23:24:39 +01:00
ba1b36758e Define SDL_MAIN_HANDLED in all tests
Each test defines its own main() function. If this flag is not set, then
SDL redefines it to SDL_main(), causing compilation failures.
2019-12-09 23:24:39 +01:00
ad92a192b5 Fix meson.build codestyle 2019-12-09 23:00:55 +01:00
81a8e503e5 Describe screen rotation shortcut in README 2019-12-09 22:51:27 +01:00
242e57d69b Merge branch 'master' into dev 2019-12-09 22:37:20 +01:00
024c2f7e6b Configure log priority early
The log priority must be configured before parsing command-line
arguments, in order to get logs as expected.
2019-12-09 22:31:14 +01:00
1eae139b6e Add missing consts
String parsing functions should not be able to modify their input.
2019-12-09 22:30:48 +01:00
419c869c9c Use ARRAY_LEN() macro in tests 2019-12-09 20:59:11 +01:00
929bf48c7e Add tests for command-line parsing 2019-12-08 23:19:01 +01:00
d950383b72 Move command-line parsing to a separate file 2019-12-08 23:18:57 +01:00
61274a7cdb Factorize integer argument parsing
Add util functions for integer parsing (with tests), and factorize
integer argument parsing to avoid code duplication.
2019-12-08 21:19:53 +01:00
64bcac9157 Refuse to push a non-regular file server
If SCRCPY_SERVER_PATH points to a directory, then a directory will be
pushed to /data/local/tmp/scrcpy-server.jar.

When executing it, app_process will just abort and leave the directory
on the device, causing scrcpy to always fail.

To avoid the problem, check that the server is a regular file before
pushing it.

Closes #956 <https://github.com/Genymobile/scrcpy/issues/956>
2019-12-05 21:07:11 +01:00
3259c60b22 Fix test compilation on mingw
Including SDL2/SDL.h redefines main to SDL_main by default.
2019-12-05 21:06:49 +01:00
e0b117de13 Fix checkstyle warning 2019-12-05 21:05:45 +01:00
eb0f339271 Add shortcut to rotate screen
On Ctrl+r, disable auto-rotation (if enabled), set the screen rotation
and re-enable auto-rotation (if it was enabled).

Closes #11 <https://github.com/Genymobile/scrcpy/issues/11>
2019-12-04 22:03:25 +01:00
bdd05b4a16 Refactor wrappers for Android SDK classes
Internally, a failure to invoke a method via reflection was partially
managed using exceptions, partially using a null return value.

Handle all errors at the same place, by not catching
NoSuchMethodException too early.
2019-12-04 19:34:20 +01:00
525d6d4a75 Try new methods before legacy ones
Use the legacy methods when the new ones do not exist.
2019-12-04 19:32:38 +01:00
4041043d1c Merge pull request #967 into dev
Add some tests

<https://github.com/Genymobile/scrcpy/pull/967>
2019-12-04 19:25:58 +01:00
fbc86a616c Correct coding style
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-12-04 19:24:59 +01:00
b2bf25c52c Add test_buffer_util
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-12-04 19:24:59 +01:00
5eeaed09ae Add test_strquote
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-12-04 19:24:59 +01:00
3100533e56 Fix "natural scrolling"
> Movements down (scroll backward) generate negative y values and up
> (scroll forward) generate positive y values.

> If direction is SDL_MOUSEWHEEL_FLIPPED the values in x and y will be
> opposite. Multiply by -1 to change them back.

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

The x and y values already take the scrolling configuration into
account. Reversing the values when the direction is flipped cancels the
scrolling configuration.

Therefore, just ignore the direction field.

Fixes <https://github.com/Genymobile/scrcpy/issues/966>
2019-12-03 21:10:43 +01:00
684e0abb74 Update BUILD.md to install adb package
PR <https://github.com/Genymobile/scrcpy/pull/965>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-12-03 16:05:23 +01:00
86fcd89d80 Fix max size default value
Suggested-by: jurkov

Closes <https://github.com/Genymobile/scrcpy/issues/978>
2019-12-03 12:07:36 +01:00
8a694a9785 Suggest workaround for error 0xfffffc0e
When the hardware encoder is not able to encode at the given definition,
it fails with an error 0xfffffc0e.

It is documented in the FAQ:
<https://github.com/Genymobile/scrcpy/blob/master/FAQ.md#i-get-an-error-could-not-open-video-stream>

But it is better to directly suggest the workaround in the console.
2019-11-30 10:46:22 +01:00
26529d377f Use virtual device id to avoid NPE
Inject mouse events using id -1 (virtual device) instead of 0 which
does not exist (and causes the InputDevice to be null).

Fixes <https://github.com/Genymobile/scrcpy/issues/962>
2019-11-29 14:31:19 +01:00
15a206b7fc Assert return value of mutex functions
Mutex functions may only fail due to a programming error.

Use assertions in debug builds, and ignore the value in release builds.
2019-11-27 21:40:58 +01:00
d0f5a7fd9f Remove unused includes
No mutex is used in decoder.c and stream.c.
2019-11-27 21:40:54 +01:00
510caff0cd Replace SDL_assert() by assert()
SDL_assert() open a dialog on assertion failure.

There is no reason not to use assert() directly.
2019-11-27 21:19:46 +01:00
b5ebb234dd Replace BUILD_DEBUG by NDEBUG
Use the "standard" NDEBUG definition, which is used by assert().
2019-11-27 21:11:52 +01:00
73e8ec1b35 Remove path argument from cmd_execute()
It is always equal to argv[0] (or not used on Windows).
2019-11-27 21:11:52 +01:00
8dc11a0286 Fix warnings on Windows
fix warnings reported with -Dwarning_level=2 on Windows.
2019-11-27 21:11:52 +01:00
06104a701b Fix windows build
Utilities have been moved to util/, but includes had not been updated
in Windows-specific files.

Ref: dfd0707a29
2019-11-27 13:25:56 +01:00
8bc056b9c6 Fix manpage format 2019-11-27 10:58:17 +01:00
7637a113e3 Compile with warning_level=2 by default 2019-11-26 09:22:20 +01:00
31d9d56117 Fix warnings
Fix warnings reported with -Dwarning_level=2.
2019-11-26 09:10:41 +01:00
6abb8fd0cd Reformat Java code
Reformated by Android studio to match the 150 characters column defined
in checkstyle.
2019-11-25 17:33:42 +01:00
2b845680b0 Initialize Application object to avoid NPE
When an ApplicationInfo is set (commit
90293240cc), some devices (Nvidia Shield
TV) attempt to access the Application object, causing a
NullPointerException.

As a workaround, initialize an Application object.

Fixes <https://github.com/Genymobile/scrcpy/issues/940>
2019-11-24 11:58:40 +01:00
ebdc2ee8b5 Rename variable for consistency
Use suffix "Field" for fields variables.
2019-11-24 11:55:42 +01:00
dfd0707a29 Move utilities to util/ 2019-11-24 11:53:23 +01:00
8ec077ce1b Add Korean translation for README and FAQ
PR <https://github.com/Genymobile/scrcpy/pull/934>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-24 11:09:30 +01:00
83ace84280 Restore the .jar extension on the device side
Commit 3da95b52bd renamed
'scrcpy-server.jar' to 'scrcpy-server' to avoid issues on the client
side.

However, removing the extension may cause issues with app_process, so
restore the extension only on the device side.

Fixes <https://github.com/Genymobile/scrcpy/issues/944>
2019-11-22 15:23:57 +01:00
c9d886f38b Use the existing constants for device server path 2019-11-22 15:16:00 +01:00
7d7f3daff2 Fix aidl option in build_without_gradle.sh
Debian's aidl complains about the missing path for -o option.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-20 17:16:46 +01:00
40c3c57613 Update links to v1.11 in README and BUILD 2019-11-19 23:39:30 +01:00
2aa65015bc Bump version to 1.11 2019-11-19 23:05:39 +01:00
4906aff454 Upgrade platform-tools (29.0.5) for Windows
Include the latest version of adb in Windows releases.
2019-11-19 23:05:39 +01:00
cb6b300483 Upgrade FFmpeg (4.2.1) for Windows
Include the latest version of FFmpeg in Windows releases.
2019-11-19 23:05:39 +01:00
c2116082ab Remove deprecated options from help and manpage
Ref: ff061b4f30
2019-11-19 23:05:39 +01:00
3599fcaae5 Fix help for --window-width and --window-height
The default value is 0 (automatic), not -1.
2019-11-19 22:58:18 +01:00
c610a6b3c7 Document scrcpy via SSH tunnel in README 2019-11-19 22:23:08 +01:00
704c0ff4dd Document copy-paste and --prefer-text in README 2019-11-19 22:23:08 +01:00
b145b8d5f4 Reorganize features in README 2019-11-19 22:23:08 +01:00
90293240cc Fix meizu 16th NPE
Fill AppInfo to avoid NullPointerException on some devices.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-19 22:23:08 +01:00
213c468c20 Move workarounds to a separate class
Extract workarounds (currently only one) to a separate class to avoid
polluting the main code.
2019-11-19 22:23:08 +01:00
18f2e33a8b Fix noconsole.exe
The linker flag "-mwindows" has no effect on my current MinGW.

Instead, passing "-Wl,--subsystem,windows" works.

Fixes <https://github.com/Genymobile/scrcpy/issues/691>
2019-11-19 12:22:11 +01:00
7aed5d5b60 Fix typos
PR <https://github.com/Genymobile/scrcpy/pull/927>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-18 17:45:52 +01:00
601b0fecdd Extract DEBUG flag in build_without_gradle.sh 2019-11-18 14:33:14 +01:00
7fd800d583 Generate VERSION_NAME in build_without_gradle.sh
Since commit b963a3b9d5, the server uses
BuildConfig.VERSION_NAME.

Generate this field manually for building without gradle.
2019-11-18 14:32:07 +01:00
1d97d7213d Add option --max-fps
Add an option to limit the capture frame rate. It only works for devices
with Android >= 10.

Fixes <https://github.com/Genymobile/scrcpy/issues/488>
2019-11-17 22:10:39 +01:00
fb976816f9 Do not expose frame rate in ScreenEncoder
The KEY_FRAME_RATE parameter value is necessary for the configuration of
the encoder, but its actual value does not impact the frame rate (only
resources used by the encoder).

Therefore, it's an internal detail and should not be exposed by the
ScreenEncoder class.
2019-11-17 22:10:33 +01:00
1b78a77962 Fix error message 2019-11-17 22:08:34 +01:00
59073223aa Update manpage for --window-borderless option 2019-11-15 18:59:47 +01:00
59bc5bc1f5 Add option to disable window decoration
Add --window-borderless parameter.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-15 18:59:40 +01:00
9fd7a80a89 Add option to specify the initial window size
Add --window-width and --window-height parameters.

If only one is provided, the other is computed so that the aspect ratio
is preserved.
2019-11-15 18:52:58 +01:00
b6e2f8ae00 Update manpage for --window-{x,y} options 2019-11-15 18:50:49 +01:00
ce5635f28c Add option to specify the initial window position
Add --window-x and --window-y parameters.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-15 18:50:49 +01:00
771bd8404d Do not write invalid packet duration
Configuration packets have no PTS. Do not compute a packet duration from
their PTS.

Fixes recording to mp4 on device rotation.
2019-11-13 16:14:46 +01:00
af3027a85b Merge pull request #920 into dev
Compare server and client version at the start of scrcpy
2019-11-13 12:01:02 +01:00
b963a3b9d5 Check client and server mismatch
Send client version as first parameter and check it at server start.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-13 12:00:50 +01:00
aa0f77c898 Accept resize shortcuts on maximized window
Allow "resize to fit" and "resize to pixel-perfect" on maximized window:
restore the window to normal size then resize.
2019-11-11 21:49:23 +01:00
35c05bb3ce Fix rotation while the window is maximized
Keep the windowed window size to handle maximized window the same way as
fullscreen window.

Fixes <https://github.com/Genymobile/scrcpy/issues/750>
2019-11-11 21:49:23 +01:00
f6f2868868 Handle window events from screen.c
Only the screen knows what to do on window events.

This paves the way to handle more window events.
2019-11-11 15:02:26 +01:00
6996cbf5d3 Log device disconnection
If scrcpy closes due to socket disconnection, log a warning.
2019-11-09 21:17:03 +01:00
e282100d0b Call Looper.prepareMainLooper() to avoid exception
Some devices internally create a Handler when creating an input Surface,
causing an exception:

> Surface: java.lang.RuntimeException: Can't create handler inside
> thread that has not called Looper.prepare()

As a workaround, call Looper.prepareMainLooper() beforehand.

Fixes:
 - <https://github.com/Genymobile/scrcpy/issues/240>
 - <https://github.com/Genymobile/scrcpy/issues/921>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-11-09 20:31:57 +01:00
b08a98324d Fix segfault on empty file recorded
Write the file trailer only if the file header have been written, to
avoid a segfault in libav.

Fixes <https://github.com/Genymobile/scrcpy/issues/918>.
2019-11-07 21:58:57 +01:00
c916af0984 Add --prefer-text option
Expose an option to configure how key/text events are forwarded to the
Android device.

Enabling the option avoids issues when combining multiple keys to enter
special characters, but breaks the expected behavior of alpha keys in
games (typically WASD).

Fixes <https://github.com/Genymobile/scrcpy/issues/650>
2019-11-07 19:01:35 +01:00
ff061b4f30 Deprecate short options for advanced features
The short options will be removed in the future (and may be reused for
other features).
2019-11-07 10:01:59 +01:00
157c60feb4 Fix indentation 2019-11-07 09:48:48 +01:00
2d90e1befd Fix include recorder.h 2019-11-06 22:22:46 +01:00
0e301ddf19 Factorize scrcpy options and command-line args
Do not duplicate all scrcpy options fields in the structure storing the
parsed command-line arguments.
2019-11-06 22:06:54 +01:00
c42ff75b74 Pass screen to mouse event converters
Mouse events coordinates depend on the screen size and location, so the
converter need to access the screen.

The fact that it needs the position or the size is an internal detail,
so pass a pointer to the whole screen structure.
2019-11-06 21:24:36 +01:00
b0db1178d1 Move event conversion to input_manager
Only keep helper functions separated.

This will help to convert coordinates internally when necessary.
2019-11-06 21:24:36 +01:00
8d601d3210 Rename "input_manager" variables to "im"
It is used a lot, a short name improves readability.
2019-11-06 21:24:36 +01:00
683f7ca848 Document how to attach a debugger to the server 2019-11-03 19:42:37 +01:00
120f08ee96 Fix manpage option parameter format
The parameter for --window-title was not underlined the same way as
others.
2019-11-03 16:51:47 +01:00
0415672a75 Merge branch 'master' into dev 2019-11-03 15:56:10 +01:00
3ea4742321 Call ninja without changing directory
In build instructions, use:

    ninja -Cx ...

instead of:

    cd x
    ninja ...
2019-10-31 21:05:04 +01:00
95fd64b5de Add scrcpy version in recorded video metadata
It might help to understand problems in recorded videos.
2019-10-31 20:57:57 +01:00
ab205084dc Merge pull request #896 from yangfl/upstream
Add manpage for scrcpy
2019-10-31 13:44:01 +01:00
4696878a97 Add manpage for scrcpy 2019-10-31 18:18:19 +08:00
3da95b52bd Rename scrcpy-server.jar to scrcpy-server
The server name ending with .jar has several drawbacks:
 - meson requires the jar executable to attempt to modify it:
     <https://github.com/Genymobile/scrcpy/issues/404#issuecomment-456065923>
     <https://github.com/mesonbuild/meson/issues/4844>
 - meson warns during "ninja install"
     <https://github.com/Genymobile/scrcpy/issues/458>
 - some users try to execute it on the computer as a java executable

Removing the extension solves all these problems.
2019-10-31 10:54:29 +01:00
c72f677435 Merge branch 'master' into dev 2019-10-30 23:29:44 +01:00
1380f6e00f Fix help for --record-format
Record format requires a parameter.
2019-10-30 22:51:40 +01:00
d841718956 Add a script to build the server without gradle
Gradle versions may sometimes cause problems. This script offers an
alternative.
2019-10-30 21:47:20 +01:00
17d53be3ef Fix mouse events conversion
The conversion from SDL mouse state to Android mouse state used wrong
constants as mask.

Fixes <https://github.com/Genymobile/scrcpy/issues/635>
2019-10-25 11:09:06 +02:00
f9938dbf88 Inject button state for touch/mouse events
The buttons state was forwarded, but ignored by the server.
2019-10-25 11:04:04 +02:00
f6c8460ebb Rename window size functions for clarity
Now, get_window_size() returns the current window size (fullscreen or
not), while get_windowed_window_size() always returned the windowed size
(the size when fullscreen is disabled).
2019-10-20 16:08:16 +02:00
c33a147fd0 Fix "turn screen off" on Android Q
Call getInternalDisplayToken(), which retrieve the id of the first
physical display (which is not necessarily 0 anymore).

Fixes <https://github.com/Genymobile/scrcpy/issues/835>
2019-10-17 23:25:00 +02:00
8b33c6c108 Adapt copy-paste methods for Android 10
The methods getPrimaryClip() and setPrimaryClip() expect an additional
parameter since Android 10.

Fixes <https://github.com/Genymobile/scrcpy/issues/796>.
2019-10-17 22:21:47 +02:00
5b7a0cd8e9 Extract String literal to static constant 2019-10-17 22:11:39 +02:00
bab9361948 Do not crash on control error
Some devices do not have some methods that we invoke via reflection, or
their call do not return the expected value. In that case, do not crash
the whole controller.
2019-10-17 22:09:54 +02:00
6220456def Merge mouse and touch events
Both are handled the very same way on the device.
2019-10-03 20:37:49 +02:00
7e1d52c119 Rename "touch pointer" to "pointer"
There are only touch pointers now, mouse pointers have been removed.
2019-10-03 20:27:28 +02:00
280d5b718c Use common pointers for mouse and touch
The mouse is a pointer like any other.
2019-10-03 20:05:29 +02:00
30168f0428 Ignore duplicate mouse events
In SDL, a touch event may simulate an identical mouse event. Since we
already handle touch event, ignore these duplicates.
2019-10-03 20:05:29 +02:00
b5a2d99bc2 Send touch events from the client
On SDL touch events, send control messages to the server.
2019-10-03 20:05:29 +02:00
f765aae352 Inject touch events on the server
On receiving an "inject touch" control message, update the local
pointers state and inject touches.
2019-10-03 20:05:29 +02:00
77f876e29c Add "inject touch" control message
Add a control message type in the protocol to forward touch events to
the device.
2019-10-03 20:05:27 +02:00
d90549d1e6 Rename "pointer" to "mouse pointer"
This will help to distinguish them from "touch pointers".
2019-10-02 21:40:26 +02:00
810ff80ba7 Add buffer_write64be()
Add a function to write 64 bits in big-endian from a uint64_t.
2019-10-02 21:40:26 +02:00
1f8ba1ca79 Include config.h everywhere
Ref: <https://github.com/Genymobile/scrcpy/issues/829>

Suggested-by: Louis Kruger <louisk@gmail.com>
2019-09-29 22:39:53 +02:00
129dabcfa4 Include config.h to fix HIDPI support
Ref: <https://github.com/Genymobile/scrcpy/issues/829>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-09-29 22:39:47 +02:00
f510f1de1c Remove "make" from build dependencies
The project is built with meson+ninja.
2019-09-28 14:46:44 +02:00
7d1932b907 Fix gradle warnings in tests 2019-09-28 12:23:54 +02:00
795d103032 input_manager.c: Correct log
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-09-28 10:37:25 +08:00
513d1ac96d Fix option "record-format" related short opt 2019-09-27 10:04:41 +08:00
ffdbf5990b Rename event converter functions
Rename "XXX_from_sdl_to_android" to "convert_XXX", to avoid huge
function names.
2019-09-15 17:29:03 +02:00
9463850c24 Rename "convert.h" to "event_converter.h"
The filename gave no hint about what was converted.
2019-09-15 17:29:03 +02:00
6e38e0cbfe Rename variable names "event" to "msg"
Some variable names had not been renamed when "event" was renamed to
"message" (28980bbc90).
2019-09-15 17:29:03 +02:00
7040e8abc4 Fix control message reader test
The mouse event test actually tested a key event control message.
2019-09-15 17:29:03 +02:00
da5b0ec0d5 Improve FAQ 2019-08-14 22:32:46 +02:00
a9c8fa305d Fix segfault on recording with old FFmpeg
The AVPacket fields side_data and side_data_elems were not initialized
by av_packet_ref() in old FFmpeg versions (prior to [1]).

As a consequence, on av_packet_unref(), side_data was freed, causing a
segfault.

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

[1]: <http://git.videolan.org/gitweb.cgi/ffmpeg.git/?p=ffmpeg.git;a=commitdiff;h=3b4026e15110547892d5d770b6b43c9e34df458f>
2019-08-13 18:58:36 +02:00
20b3f101a4 Print gradle output on compiling
Enable the attribute "console" of custom_target() introduced in meson
0.48. This allows to get a feedback of what gradle does (which can takes
a very long time).

This produces warnings because we declare to support meson >= 0.37, but
we don't want to stop supporting older versions for that. Older versions
just ignore the option:

> WARNING: Unknown keyword arguments in target scrcpy-server: console

Newer meson versions use it, but warn because we declare supporting
older versions:

> WARNING: Project targetting '>= 0.37' but tried to use feature
> introduced in '0.48.0': console arg in custom_target

Meson does not support conditional branches to suppress such warnings,
so just keep the warnings.
2019-08-09 15:15:28 +02:00
686733023b Merge pull request #708 from toddsierens/patch-1
Update WindowManager.java
2019-08-08 23:24:47 +02:00
27eacc3c11 Update WindowManager.java 2019-08-08 17:17:48 -04:00
8507fea271 Record a packet with its duration
Record a packet only once the following has been received, so that we
can set its duration before muxing it.

Fixes <https://github.com/Genymobile/scrcpy/issues/702>
2019-08-08 18:57:52 +02:00
8a08ca0f3d Merge pull request #692 from msfjarvis/msf/uprev-dependencies
Upgrade build dependencies
2019-08-07 23:42:06 +02:00
44fb692a64 Uprev Gradle wrapper to latest stable
Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-08-07 23:41:36 +02:00
3f77c29c95 Uprev AGP to latest stable
Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
2019-08-07 23:20:16 +02:00
9a50b65b33 Merge pull request #695 from schwabe/schwabe/fix_null_queue
Fix building on OS X (missing NULL in queue.h)
2019-08-05 15:26:06 +02:00
c05056343b Fix building on OS X (missing NULL in queue.h)
Headers seem to be a bit different in Apple land and you need to include
stddef.h explicitly to the NULL declaration.

This also makes the code a bit more correct, as stddef.h is the header
in the C standard that defines NULL
(https://en.cppreference.com/w/cpp/header/cstddef).
2019-08-05 15:02:05 +02:00
9bcee4ea42 Update links to v1.10 in README and BUILD 2019-08-04 22:03:45 +02:00
c28619e4e8 Bump version to 1.10 2019-08-04 16:41:04 +02:00
8969444ff2 List scrcpy characteristics in README
They were listed in the blog post introducing scrcpy:
<https://blog.rom1v.com/2018/03/introducing-scrcpy/>
2019-08-04 16:23:42 +02:00
b54f0bfe48 Upgrade SDL (2.0.10) for Windows
Include the latest version of SDL in Windows releases.
2019-08-04 16:23:42 +02:00
0aec1e502e Update platform-tools (29.0.2) for Windows
Include the latest version of adb in Windows releases.
2019-08-04 16:23:42 +02:00
c3a58ad10f Upgrade FFmpeg (4.1.4) for Windows
Include the latest version of FFmpeg in Windows releases.
2019-08-04 16:23:42 +02:00
b0184f2869 Initialize queue "last" field
The compiler is not always able to see that "last" is always initialized
before being used, so always initialize it.
2019-08-04 16:22:39 +02:00
128 changed files with 6386 additions and 1707 deletions

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,25 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
- [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md).
- [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues).
**Environment**
- OS: [e.g. Debian, Windows, macOS...]
- scrcpy version: [e.g. 1.12.1]
- installation method: [e.g. manual build, apt, snap, brew, Windows release...]
- device model:
- Android version: [e.g. 10]
**Describe the bug**
A clear and concise description of what the bug is.
On errors, please provide the output of the console (and `adb logcat` if relevant).
Format them between code blocks (delimited by ```).
Please do not post screenshots of your terminal, just post the content as text instead.

View File

@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
- [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ build/
/dist/
.idea/
.gradle/
/x/

View File

@ -8,6 +8,22 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
[prebuilt server]: #prebuilt-server
## Branches
### `master`
The `master` branch concerns the latest release, and is the home page of the
project on Github.
### `dev`
`dev` is the current development branch. Every commit present in `dev` will be
in the next release.
If you want to contribute code, please base your commits on the latest `dev`
branch.
## Requirements
@ -40,10 +56,10 @@ Install the required packages from your package manager.
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0-0
sudo apt install ffmpeg libsdl2-2.0-0 adb
# client build dependencies
sudo apt install make gcc git pkg-config meson ninja-build \
sudo apt install gcc git pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
@ -195,8 +211,7 @@ Then, build:
```bash
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
ninja -Cx
```
_Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja
@ -219,13 +234,13 @@ To run without installing:
After a successful build, you can install _scrcpy_ on the system:
```bash
sudo ninja install # without sudo on Windows
sudo ninja -Cx install # without sudo on Windows
```
This installs two files:
- `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server.jar`
- `/usr/local/share/scrcpy/scrcpy-server`
Just remove them to "uninstall" the application.
@ -234,18 +249,20 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
_(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_
- [`scrcpy-server-v1.13`][direct-scrcpy-server]
_(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
```bash
meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar
cd x
ninja
sudo ninja install
-Dprebuilt_server=/path/to/scrcpy-server
ninja -Cx
sudo ninja -Cx install
```
The server only works with a matching client version (this server works with the
`master` branch).

View File

@ -3,7 +3,7 @@
## Overview
This application is composed of two parts:
- the server (`scrcpy-server.jar`), to be executed on the device,
- the server (`scrcpy-server`), to be executed on the device,
- the client (the `scrcpy` binary), executed on the host computer.
The client is responsible to push the server to the device and start its
@ -49,7 +49,7 @@ application may not replace the server just before the client executes it._
Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing
`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle
build system, the server is built to an (unsigned) APK (renamed to
`scrcpy-server.jar`).
`scrcpy-server`).
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
[apk]: https://en.wikipedia.org/wiki/Android_application_package
@ -189,7 +189,7 @@ The client uses 4 threads:
recording,
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the client.
messages_ from the server.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to
@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one.
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw
H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
@ -268,3 +268,42 @@ For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and
contribute ;-)
### Debug the server
The server is pushed to the device by the client on startup.
To debug it, enable the server debugger during configuration:
```bash
meson x -Dserver_debugger=true
# or, if x is already configured
meson configure x -Dserver_debugger=true
```
If your device runs Android 8 or below, set the `server_debugger_method` to
`old` in addition:
```bash
meson x -Dserver_debugger=true -Dserver_debugger_method=old
# or, if x is already configured
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
```
Then recompile.
When you start scrcpy, it will start a debugger on port 5005 on the device.
Redirect that port to the computer:
```bash
adb forward tcp:5005 tcp:5005
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
- Host: `localhost`
- Port: `5005`
Then click on _Debug_.

84
FAQ.ko.md Normal file
View File

@ -0,0 +1,84 @@
# 자주하는 질문 (FAQ)
다음은 자주 제보되는 문제들과 그들의 현황입니다.
### Window 운영체제에서, 디바이스가 발견되지 않습니다.
가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다.
다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요:
adb devices
Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다.
[drivers]: https://developer.android.com/studio/run/oem-usb.html
### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다.
일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다.
개발자 옵션에서 (developer options) 다음을 활성화 하세요:
> **USB debugging (Security settings)**
> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
### 마우스 클릭이 다른 곳에 적용됩니다.
Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다.
[issue 15]를 참고하세요.
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다:
```bash
meson x --buildtype release -Dhidpi_support=false
```
하지만, 동영상은 낮은 해상도로 재생될 것 입니다.
### HiDPI display의 화질이 낮습니다.
Windows에서는, [scaling behavior] 환경을 설정해야 할 수도 있습니다.
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
> Override high DPI scaling behavior > Scaling performed by: _Application_.
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
### KWin compositor가 실행되지 않습니다
Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다.
차석책으로는, ["Block compositing"를 비활성화하세요][kwin].
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream).
여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가
주어진 해상도를 인코딩할 수 없는 경우입니다.
```
ERROR: Exception on thread Thread[main,5,main]
android.media.MediaCodec$CodecException: Error 0xfffffc0e
...
Exit due to uncaughtException in main thread:
ERROR: Could not open video stream
INFO: Initial texture: 1080x2336
```
더 낮은 해상도로 시도 해보세요:
```
scrcpy -m 1920
scrcpy -m 1024
scrcpy -m 800
```

181
FAQ.md
View File

@ -1,28 +1,110 @@
# Frequently Asked Questions
## Common issues
The application is very young, it is not unlikely that you encounter problems
with it.
Here are the common reported problems and their status.
### On Windows, my device is not detected
## `adb` issues
The most common is your device not being detected by `adb`, or is unauthorized.
Check everything is ok by calling:
`scrcpy` execute `adb` commands to initialize the connection with the device. If
`adb` fails, then scrcpy will not work.
adb devices
In that case, it will print this error:
Windows may need some [drivers] to detect your device.
> ERROR: "adb push" returned with value 1
This is typically not a bug in _scrcpy_, but a problem in your environment.
To find out the cause, execute:
```bash
adb devices
```
### `adb` not found
You need `adb` accessible from your `PATH`.
On Windows, the current directory is in your `PATH`, and `adb.exe` is included
in the release, so it should work out-of-the-box.
### Device unauthorized
Check [stackoverflow][device-unauthorized].
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
### Device not detected
If your device is not detected, you may need some [drivers] (on Windows).
[drivers]: https://developer.android.com/studio/run/oem-usb.html
### Mouse clicks do not work
### Several devices connected
If several devices are connected, you will encounter this error:
> adb: error: failed to get feature set: more than one device/emulator
the identifier of the device you want to mirror must be provided:
```bash
scrcpy -s 01234567890abcdef
```
Note that if your device is connected over TCP/IP, you'll get this message:
> adb: error: more than one device/emulator
> ERROR: "adb reverse" returned with value 1
> WARN: 'adb reverse' failed, fallback to 'adb forward'
This is expected (due to a bug on old Android versions, see [#5]), but in that
case, scrcpy fallbacks to a different method, which should work.
[#5]: https://github.com/Genymobile/scrcpy/issues/5
### Conflicts between adb versions
> adb server version (41) doesn't match this client (39); killing...
This error occurs when you use several `adb` versions simultaneously. You must
find the program using a different `adb` version, and use the same `adb` version
everywhere.
You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to
use a specific `adb` binary, by setting the `ADB` environment variable:
```bash
set ADB=/path/to/your/adb
scrcpy
```
### Device disconnected
If _scrcpy_ stops itself with the warning "Device disconnected", then the
`adb` connection has been closed.
Try with another USB cable or plug it into another USB port. See [#281] and
[#283].
[#281]: https://github.com/Genymobile/scrcpy/issues/281
[#283]: https://github.com/Genymobile/scrcpy/issues/283
## Control issues
### Mouse and keyboard do not work
On some devices, you may need to enable an option to allow [simulating input].
In developer options, enable:
> **USB debugging (Security settings)**
> _Allow granting permissions and simulating input via USB debugging_
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
@ -30,17 +112,49 @@ On some devices, you may need to enable an option to allow [simulating input].
### Mouse clicks at wrong location
On MacOS, with HiDPI support and multiple screens, input location are wrongly
scaled. See [issue 15].
scaled. See [#15].
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
[#15]: https://github.com/Genymobile/scrcpy/issues/15
A workaround is to build with HiDPI support disabled:
Open _scrcpy_ directly on the monitor you use it.
```bash
meson x --buildtype release -Dhidpi_support=false
### Special characters do not work
Injecting text input is [limited to ASCII characters][text-input]. A trick
allows to also inject some [accented characters][accented-characters], but
that's all. See [#37].
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37
## Client issues
### The quality is low
If the definition of your client window is smaller than that of your device
screen, then you might get poor quality, especially visible on text (see [#40]).
[#40]: https://github.com/Genymobile/scrcpy/issues/40
To improve downscaling quality, trilinear filtering is enabled automatically
if the renderer is OpenGL and if it supports mipmapping.
On Windows, you might want to force OpenGL:
```
scrcpy --render-driver=opengl
```
However, the video will be displayed at lower resolution.
You may also need to configure the [scaling behavior]:
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
> Override high DPI scaling behavior > Scaling performed by: _Application_.
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
### KWin compositor crashes
@ -50,3 +164,36 @@ On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
As a workaround, [disable "Block compositing"][kwin].
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
## Crashes
### Exception
There may be many reasons. One common cause is that the hardware encoder of your
device is not able to encode at the given definition:
> ```
> ERROR: Exception on thread Thread[main,5,main]
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
> ...
> Exit due to uncaughtException in main thread:
> ERROR: Could not open video stream
> INFO: Initial texture: 1080x2336
> ```
or
> ```
> ERROR: Exception on thread Thread[main,5,main]
> java.lang.IllegalStateException
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
> ```
Just try with a lower definition:
```
scrcpy -m 1920
scrcpy -m 1024
scrcpy -m 800
```

View File

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

View File

@ -3,7 +3,7 @@
#
# Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar).
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
@ -97,32 +97,33 @@ build-win64-noconsole: prepare-deps-win64
dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \

498
README.ko.md Normal file
View File

@ -0,0 +1,498 @@
# scrcpy (v1.11)
This document will be updated frequently along with the original Readme file
이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다
이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다.
_GNU/Linux_, _Windows__macOS_ 상에서 작동합니다.
(아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.)
[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
![screenshot](https://github.com/Genymobile/scrcpy/blob/master/assets/screenshot-debian-600.jpg?raw=true)
주요 기능은 다음과 같습니다.
- **가벼움** (기본적이며 디바이스의 화면만을 보여줌)
- **뛰어난 성능** (30~60fps)
- **높은 품질** (1920×1080 이상의 해상도)
- **빠른 반응 속도** ([35~70ms][lowlatency])
- **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨)
- **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨)
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
## 요구사항
안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다.
디바이스에 [adb debugging][enable-adb]이 가능한지 확인하십시오.
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
어떤 디바이스에서는, 키보드와 마우스를 사용하기 위해서 [추가 옵션][control] 이 필요하기도 합니다.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
## 앱 설치하기
### Linux (리눅스)
리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다.
[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md
[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link].
[snap-link]: https://snapstats.org/snaps/scrcpy
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
Arch Linux에서, [AUR] 패키지가 가능합니다 : [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link].
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
### Windows (윈도우)
윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) :
해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다.
- [`scrcpy-win`][direct-win]
[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows
[어플을 직접 설치][BUILD] 할 수도 있습니다.
### macOS (맥 OS)
이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 :
[Homebrew]: https://brew.sh/
```bash
brew install scrcpy
```
`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 :
```bash
brew cask install android-platform-tools
```
[어플을 직접 설치][BUILD] 할 수도 있습니다.
## 실행
안드로이드 디바이스를 연결하고 실행하십시오:
```bash
scrcpy
```
다음과 같이 명령창 옵션 기능도 제공합니다.
```bash
scrcpy --help
```
## 기능
### 캡쳐 환경 설정
###사이즈 재정의
가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다.
너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) :
```bash
scrcpy --max-size 1024
scrcpy -m 1024 # 축약 버전
```
이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다.
이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다.
### bit-rate 변경
기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경):
```bash
scrcpy --bit-rate 2M
scrcpy -b 2M # 축약 버전
```
###프레임 비율 제한
안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다:
```bash
scrcpy --max-fps 15
```
### Crop (잘라내기)
디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다.
예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 :
```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
scrcpy -c 1224:1440:0:0 # 축약 버전
```
만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다.
### 화면 녹화
미러링하는 동안 화면 녹화를 할 수 있습니다 :
```bash
scrcpy --record file.mp4
scrcpy -r file.mkv
```
녹화하는 동안 미러링을 멈출 수 있습니다 :
```bash
scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv
# Ctrl+C 로 녹화를 중단할 수 있습니다.
# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오.
```
"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay
variation] 은 녹화된 파일에 영향을 끼치지 않습니다.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
## 연결
### 무선연결
_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 :
1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다.
2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ).
3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`.
4. 디바이스 연결을 해제합니다.
5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` 대신)_.
6. `scrcpy` 실행합니다.
다음은 bit-rate 와 해상도를 줄이는데 유용합니다 :
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # 축약 버전
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
### 여러 디바이스 사용 가능
만약에 여러 디바이스들이 `adb devices` 목록에 표시되었다면, _serial_ 을 명시해야합니다:
```bash
scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # 축약 버전
```
_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다.
#### SSH tunnel
떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.):
```bash
adb kill-server # 5037의 로컬 local adb server를 중단
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
# 실행 유지
```
다른 터미널에서는 :
```bash
scrcpy
```
무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다:
```
scrcpy -b2M -m800 --max-fps 15
```
## Window에서의 배치
### 맞춤형 window 제목
기본적으로, window의 이름은 디바이스의 모델명 입니다.
다음의 명령어를 통해 변경하세요.
```bash
scrcpy --window-title 'My device'
```
### 배치와 크기
초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다:
```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
```
### 경계 없애기
윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다:
```bash
scrcpy --window-borderless
```
### 항상 모든 윈도우 위에 실행창 고정
이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다:
```bash
scrcpy --always-on-top
scrcpy -T # 축약 버전
```
### 전체 화면
이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다.
```bash
scrcpy --fullscreen
scrcpy -f # short version
```
전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다.
## 다른 미러링 옵션
### 읽기 전용(Read-only)
권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)):
```bash
scrcpy --no-control
scrcpy -n
```
### 화면 끄기
미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는
다음의 커맨드 라인 옵션을(command line option) 입력하세요:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다.
다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요.
### 유효기간이 지난 프레임 제공 (Render expired frames)
디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다
과거의 프레임은 하나씩 삭제합니다.
모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다)
다음의 명령어를 사용하세요:
```bash
scrcpy --render-expired-frames
```
### 화면에 터치 나타내기
발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다.
안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다.
_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다.
```bash
scrcpy --show-touches
scrcpy -t
```
화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위).
### 입력 제어
#### 복사-붙여넣기
컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다:
- `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다;
- `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다;
- `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 )
#### 텍스트 삽입 우선 순위
텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다:
- _key events_, 키가 눌려있는 지에 대한 신호;
- _text events_, 텍스트가 입력되었는지에 대한 신호.
기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ).
그러나 이는 [issues 를 발생][prefertext]시킵니다. 이와 관련된 문제를 접할 경우, 아래와 같이 피할 수 있습니다:
```bash
scrcpy --prefer-text
```
( 그러나 이는 게임에서의 처리를 중단할 수 있습니다 )
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
### 파일 드랍
### APK 실행하기
APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop)
시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다.
### 디바이스에 파일 push하기
디바이스의`/sdcard/`에 파일을 push하기 위해서는,
APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop).
시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다.
해당 디렉토리는 시작할 때 변경이 가능합니다:
```bash
scrcpy --push-target /sdcard/foo/bar/
```
### 오디오의 전달
_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요.
추가적으로 [issue #14]를 참고하세요.
[USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## 단축키
| 실행내용 | 단축키 | 단축키 (macOS)
| -------------------------------------- |:----------------------------- |:-----------------------------
| 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f`
| window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g`
| 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
|`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
| `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
| `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s`
| `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m`
| `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
| `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
| `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p`
| 전원 켜기 | _Right-click²_ | _Right-click²_
| 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o`
| 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n`
| 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c`
| 컴퓨터의 clipboard 디바이스에 붙여넣기 | `Ctrl`+`v` | `Cmd`+`v`
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_
_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다.
## 맞춤 경로 (custom path)
특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요.
`ADB`:
ADB=/path/to/adb scrcpy
`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## _scrcpy_ 인 이유?
한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다.
[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다.
[gnirehtet]: https://github.com/Genymobile/gnirehtet
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
## 빌드하는 방법?
[BUILD]을 참고하세요.
[BUILD]: BUILD.md
## 흔한 issue
[FAQ](FAQ.md)을 참고하세요.
## 개발자들
[developers page]를 참고하세요.
[developers page]: DEVELOP.md
## 라이선스
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## 관련 글 (articles)
- [scrcpy 소개][article-intro]
- [무선으로 연결하는 Scrcpy][article-tcpip]
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/

385
README.md
View File

@ -1,4 +1,4 @@
# scrcpy (v1.9)
# scrcpy (v1.13)
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.
@ -6,6 +6,17 @@ It works on _GNU/Linux_, _Windows_ and _macOS_.
![screenshot](assets/screenshot-debian-600.jpg)
It focuses on:
- **lightness** (native, displays only the device screen)
- **performance** (30~60fps)
- **quality** (1920×1080 or above)
- **low latency** ([35~70ms][lowlatency])
- **low startup time** (~1 second to display the first image)
- **non-intrusiveness** (nothing is left installed on the device)
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
## Requirements
@ -26,8 +37,11 @@ control it using keyboard and mouse.
### Linux
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard.
On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04):
```
apt install scrcpy
```
A [Snap] package is available: [`scrcpy`][snap-link].
@ -45,19 +59,38 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
You could also [build the app manually][BUILD] (don't worry, it's not that
hard).
### Windows
For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available:
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
- [`scrcpy-win32-v1.9.zip`][direct-win32]
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
- [`scrcpy-win64-v1.9.zip`][direct-win64]
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
- [`scrcpy-win64-v1.13.zip`][direct-win64]
_(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_
[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.9/scrcpy-win64-v1.9.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip
It is also available in [Chocolatey]:
[Chocolatey]: https://chocolatey.org/
```bash
choco install scrcpy
choco install adb # if you don't have it yet
```
And in [Scoop]:
```bash
scoop install scrcpy
scoop install adb # if you don't have it yet
```
[Scoop]: https://scoop.sh
You can also [build the app manually][BUILD].
@ -97,8 +130,9 @@ scrcpy --help
## Features
### Capture configuration
### Reduce size
#### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to
increase performance.
@ -114,7 +148,7 @@ 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.
### Change bit-rate
#### Change bit-rate
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
@ -123,8 +157,17 @@ scrcpy --bit-rate 2M
scrcpy -b 2M # short version
```
#### Limit frame rate
### Crop
The capture frame rate can be limited:
```bash
scrcpy --max-fps 15
```
This is officially supported since Android 10, but may work on earlier versions.
#### Crop
The device screen may be cropped to mirror only part of the screen.
@ -132,35 +175,27 @@ This is useful for example to mirror only one eye of the Oculus Go:
```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
scrcpy -c 1224:1440:0:0 # short version
```
If `--max-size` is also specified, resizing is applied after cropping.
### Wireless
#### Lock video orientation
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP:
1. Connect the device to the same Wi-Fi as your computer.
2. Get your device IP address (in Settings → About phone → Status).
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
4. Unplug your device.
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
6. Run `scrcpy` as usual.
It may be useful to decrease the bit-rate and the definition:
To lock the orientation of the mirroring:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # short version
scrcpy --lock-video-orientation 0 # natural orientation
scrcpy --lock-video-orientation 1 # 90° counterclockwise
scrcpy --lock-video-orientation 2 # 180°
scrcpy --lock-video-orientation 3 # 90° clockwise
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
This affects recording orientation.
### Record screen
### Recording
It is possible to record the screen while mirroring:
@ -185,7 +220,31 @@ variation] does not impact the recorded file.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
### Multi-devices
### Connection
#### Wireless
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP:
1. Connect the device to the same Wi-Fi as your computer.
2. Get your device IP address (in Settings → About phone → Status).
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
4. Unplug your device.
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
6. Run `scrcpy` as usual.
It may be useful to decrease the bit-rate and the definition:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # short version
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
#### Multi-devices
If several devices are listed in `adb devices`, you must specify the _serial_:
@ -194,10 +253,84 @@ scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # short version
```
If the device is connected over TCP/IP:
```bash
scrcpy --serial 192.168.0.1:5555
scrcpy -s 192.168.0.1:5555 # short version
```
You can start several instances of _scrcpy_ for several devices.
#### Autostart on device connection
### Fullscreen
You could use [AutoAdb]:
```bash
autoadb scrcpy -s '{}'
```
[AutoAdb]: https://github.com/rom1v/autoadb
#### SSH tunnel
To connect to a remote device, it is possible to connect a local `adb` client to
a remote `adb` server (provided they use the same version of the _adb_
protocol):
```bash
adb kill-server # kill the local adb server on 5037
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal:
```bash
scrcpy
```
Like for wireless connections, it may be useful to reduce quality:
```
scrcpy -b2M -m800 --max-fps 15
```
### Window configuration
#### Title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
```
#### Position and size
The initial window position and size may be specified:
```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
```
#### Borderless
To disable window decorations:
```bash
scrcpy --window-borderless
```
#### Always on top
To keep the scrcpy window always on top:
```bash
scrcpy --always-on-top
```
#### Fullscreen
The app may be started directly in fullscreen:
@ -208,56 +341,37 @@ scrcpy -f # short version
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
#### Rotation
### Always on top
The window of app can always be above others by:
The window may be rotated:
```bash
scrcpy --always-on-top
scrcpy -T # short version
scrcpy --rotation 1
```
Possibles values are:
- `0`: no rotation
- `1`: 90 degrees counterclockwise
- `2`: 180 degrees
- `3`: 90 degrees clockwise
### Show touches
The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and
`Ctrl`+`→` _(right)_.
For presentations, it may be useful to show physical touches (on the physical
device).
Android provides this feature in _Developers options_.
_Scrcpy_ provides an option to enable this feature on start and disable on exit:
```bash
scrcpy --show-touches
scrcpy -t
```
Note that it only shows _physical_ touches (with the finger on the device).
Note that _scrcpy_ manages 3 different rotations:
- `Ctrl`+`r` requests the device to switch between portrait and landscape (the
current running app may refuse, if it does support the requested
orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the
recording.
- `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This
affects only the display, not the recording.
### Install APK
### Other mirroring options
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
window.
There is no visual feedback, a log is printed to the console.
### Push file to device
To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
_scrcpy_ window.
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
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
@ -267,7 +381,34 @@ scrcpy --no-control
scrcpy -n
```
### Turn screen off
#### Display
If several displays are available, it is possible to select the display to
mirror:
```bash
scrcpy --display 1
```
The list of display ids can be retrieved by:
```
adb shell dumpsys display # search "mDisplayId=" in the output
```
#### Stay awake
To prevent the device to sleep after some delay:
```bash
scrcpy --stay-awake
scrcpy -w
```
The initial state is restored when scrcpy is closed.
#### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
command-line option:
@ -281,8 +422,15 @@ Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
It can be useful to also prevent the device to sleep:
### Render expired frames
```bash
scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
#### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
@ -294,16 +442,90 @@ latency), use:
scrcpy --render-expired-frames
```
### Custom window title
#### Show touches
By default, the window title is the device model. It can be changed:
For presentations, it may be useful to show physical touches (on the physical
device).
Android provides this feature in _Developers options_.
_Scrcpy_ provides an option to enable this feature on start and restore the
initial value on exit:
```bash
scrcpy --window-title 'My device'
scrcpy --show-touches
scrcpy -t
```
Note that it only shows _physical_ touches (with the finger on the device).
### Input control
#### Rotate device screen
Press `Ctrl`+`r` to switch between portrait and landscape modes.
Note that it rotates only if the application in foreground supports the
requested orientation.
#### Copy-paste
It is possible to synchronize clipboards between the computer and the device, in
both directions:
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
breaks non-ASCII characters).
#### Text injection preference
There are two kinds of [events][textevents] generated when typing text:
- _key events_, signaling that a key is pressed or released;
- _text events_, signaling that a text has been entered.
By default, letters are injected using key events, so that the keyboard behaves
as expected in games (typically for WASD keys).
But this may [cause issues][prefertext]. If you encounter such a problem, you
can avoid it by:
```bash
scrcpy --prefer-text
```
(but this will break keyboard behavior in games)
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
### File drop
#### Install APK
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
window.
There is no visual feedback, a log is printed to the console.
#### Push file to device
To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
_scrcpy_ window.
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/
```
### Forward audio
### Audio forwarding
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
@ -318,6 +540,8 @@ Also see [issue #14].
| Action | Shortcut | Shortcut (macOS)
| -------------------------------------- |:----------------------------- |:-----------------------------
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
@ -329,6 +553,7 @@ Also see [issue #14].
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
@ -347,7 +572,7 @@ To use a specific _adb_ binary, configure its path in the environment variable
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file, configure its path in
To override the path of the `scrcpy-server` file, configure its path in
`SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
@ -385,7 +610,7 @@ Read the [developers page].
## Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Copyright (C) 2018-2020 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,25 +1,27 @@
src = [
'src/main.c',
'src/cli.c',
'src/command.c',
'src/control_msg.c',
'src/controller.c',
'src/convert.c',
'src/decoder.c',
'src/device.c',
'src/device_msg.c',
'src/event_converter.c',
'src/file_handler.c',
'src/fps_counter.c',
'src/input_manager.c',
'src/net.c',
'src/opengl.c',
'src/receiver.c',
'src/recorder.c',
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/str_util.c',
'src/tiny_xpm.c',
'src/stream.c',
'src/tiny_xpm.c',
'src/video_buffer.c',
'src/util/net.c',
'src/util/str_util.c'
]
if not get_option('crossbuild_windows')
@ -75,17 +77,15 @@ cc = meson.get_compiler('c')
if host_machine.system() == 'windows'
src += [ 'src/sys/win/command.c' ]
src += [ 'src/sys/win/net.c' ]
dependencies += cc.find_library('ws2_32')
else
src += [ 'src/sys/unix/command.c' ]
src += [ 'src/sys/unix/net.c' ]
endif
conf = configuration_data()
# expose the build type
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
conf.set('NDEBUG', get_option('buildtype') != 'debug')
# the version, updated on release
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
@ -93,18 +93,25 @@ conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
# build a "portable" version (with scrcpy-server.jar accessible from the same
# build a "portable" version (with scrcpy-server accessible from the same
# directory as the executable)
conf.set('PORTABLE', get_option('portable'))
# the default client TCP port for the "adb reverse" tunnel
# the default client TCP port range for the "adb reverse" tunnel
# overridden by option --port
conf.set('DEFAULT_LOCAL_PORT', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# the default max video size for both dimensions, in pixels
# overridden by option --max-size
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# the default video orientation
# natural device orientation is 0 and each increment adds 90 degrees
# counterclockwise
# overridden by option --lock-video-orientation
conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked
# the default video bitrate, in bits/second
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
@ -115,15 +122,19 @@ conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src')
if get_option('windows_noconsole')
c_args = [ '-mwindows' ]
link_args = [ '-mwindows' ]
link_args = [ '-Wl,--subsystem,windows' ]
else
c_args = []
link_args = []
endif
@ -131,37 +142,51 @@ executable('scrcpy', src,
dependencies: dependencies,
include_directories: src_dir,
install: true,
c_args: c_args,
c_args: [],
link_args: link_args)
install_man('scrcpy.1')
### TESTS
tests = [
['test_cbuf', [
'tests/test_cbuf.c',
]],
['test_control_event_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
'src/str_util.c'
]],
['test_device_event_deserialize', [
'tests/test_device_msg_deserialize.c',
'src/device_msg.c'
]],
['test_queue', [
'tests/test_queue.c',
]],
['test_strutil', [
'tests/test_strutil.c',
'src/str_util.c'
]],
]
# do not build tests in release (assertions would not be executed at all)
if get_option('buildtype') == 'debug'
tests = [
['test_buffer_util', [
'tests/test_buffer_util.c'
]],
['test_cbuf', [
'tests/test_cbuf.c',
]],
['test_cli', [
'tests/test_cli.c',
'src/cli.c',
'src/util/str_util.c',
]],
['test_control_event_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
'src/util/str_util.c',
]],
['test_device_event_deserialize', [
'tests/test_device_msg_deserialize.c',
'src/device_msg.c',
]],
['test_queue', [
'tests/test_queue.c',
]],
['test_strutil', [
'tests/test_strutil.c',
'src/util/str_util.c',
]],
]
foreach t : tests
exe = executable(t[0], t[1],
include_directories: src_dir,
dependencies: dependencies)
test(t[0], exe)
endforeach
foreach t : tests
exe = executable(t[0], t[1],
include_directories: src_dir,
dependencies: dependencies,
c_args: ['-DSDL_MAIN_HANDLED'])
test(t[0], exe)
endforeach
endif

324
app/scrcpy.1 Normal file
View File

@ -0,0 +1,324 @@
.TH "scrcpy" "1"
.SH NAME
scrcpy \- Display and control your Android device
.SH SYNOPSIS
.B scrcpy
.RI [ options ]
.SH DESCRIPTION
.B scrcpy
provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access.
.SH OPTIONS
.TP
.B \-\-always\-on\-top
Make scrcpy window always on top (above other windows).
.TP
.BI "\-b, \-\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 8000000.
.TP
.BI "\-\-\codec\-options " key[:type]=value[,...]
Set a list of comma-separated key:type=value options for the device encoder.
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation:
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
.TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
.B \-\-max\-size
value is computed on the cropped size.
.TP
.BI "\-\-display " id
Specify the display id to mirror.
The list of possible display ids can be listed by "adb shell dumpsys display"
(search "mDisplayId=" in the output).
Default is 0.
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
.TP
.B \-h, \-\-help
Print this help.
.TP
.BI "\-\-lock\-video\-orientation " value
Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
Default is -1 (unlocked).
.TP
.BI "\-\-max\-fps " value
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
.TP
.BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
Default is 0 (unlimited).
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled).
.TP
.B \-\-no\-mipmaps
If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps.
.TP
.BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen.
Default is 27183:27199.
.TP
.B \-\-prefer\-text
Inject alpha characters and space as text events instead of key events.
This avoids issues when combining multiple keys to enter special characters,
but breaks the expected behavior of alpha keys in games (typically WASD).
.TP
.BI "\-\-push\-target " path
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
Default is "/sdcard/".
.TP
.BI "\-r, \-\-record " file
Record screen to
.IR file .
The format is determined by the
.B \-\-record\-format
option if set, or by the file extension (.mp4 or .mkv).
.TP
.BI "\-\-record\-format " format
Force recording format (either mp4 or mkv).
.TP
.BI "\-\-render\-driver " name
Request SDL to use the given render driver (this is just a hint).
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
.UE
.TP
.B \-\-render\-expired\-frames
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
.TP
.BI "\-\-rotation " value
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
.TP
.BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit..
It only shows physical touches (not clicks from scrcpy).
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running.
.TP
.B \-\-window\-borderless
Disable window decorations (display borderless window).
.TP
.BI "\-\-window\-title " text
Set a custom window title.
.TP
.BI "\-\-window\-x " value
Set the initial window horizontal position.
Default is "auto".\n
.TP
.BI "\-\-window\-y " value
Set the initial window vertical position.
Default is "auto".\n
.TP
.BI "\-\-window\-width " value
Set the initial window width.
Default is 0 (automatic).\n
.TP
.BI "\-\-window\-height " value
Set the initial window height.
Default is 0 (automatic).\n
.SH SHORTCUTS
.TP
.B Ctrl+f
Switch fullscreen mode
.TP
.B Ctrl+Left
Rotate display left
.TP
.B Ctrl+Right
Rotate display right
.TP
.B Ctrl+g
Resize window to 1:1 (pixel\-perfect)
.TP
.B Ctrl+x, Double\-click on black borders
Resize window to remove black borders
.TP
.B Ctrl+h, Home, Middle\-click
Click on HOME
.TP
.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on)
Click on BACK
.TP
.B Ctrl+s
Click on APP_SWITCH
.TP
.B Ctrl+m
Click on MENU
.TP
.B Ctrl+Up
Click on VOLUME_UP
.TP
.B Ctrl+Down
Click on VOLUME_DOWN
.TP
.B Ctrl+p
Click on POWER (turn screen on/off)
.TP
.B Right\-click (when screen is off)
Turn screen on
.TP
.B Ctrl+o
Turn device screen off (keep mirroring)
.TP
.B Ctrl+r
Rotate device screen
.TP
.B Ctrl+n
Expand notification panel
.TP
.B Ctrl+Shift+n
Collapse notification panel
.TP
.B Ctrl+c
Copy device clipboard to computer
.TP
.B Ctrl+v
Paste computer clipboard to device
.TP
.B Ctrl+Shift+v
Copy computer clipboard to device
.TP
.B Ctrl+i
Enable/disable FPS counter (print frames/second in logs)
.TP
.B Drag & drop APK file
Install APK from computer
.SH Environment variables
.TP
.B ADB
Specify the path to adb.
.TP
.B SCRCPY_SERVER_PATH
Specify the path to server binary.
.SH AUTHORS
.B scrcpy
is written by Romain Vimont.
This manual page was written by
.MT mmyangfl@gmail.com
Yangfl
.ME
for the Debian Project (and may be used by others).
.SH "REPORTING BUGS"
Report bugs to
.UR https://github.com/Genymobile/scrcpy/issues
.UE .
.SH COPYRIGHT
Copyright \(co 2018 Genymobile
.UR https://www.genymobile.com
Genymobile
.UE
Copyright \(co 2018\-2020
.MT rom@rom1v.com
Romain Vimont
.ME
Licensed under the Apache License, Version 2.0.
.SH WWW
.UR https://github.com/Genymobile/scrcpy
.UE

706
app/src/cli.c Normal file
View File

@ -0,0 +1,706 @@
#include "cli.h"
#include <assert.h>
#include <getopt.h>
#include <stdint.h>
#include <unistd.h>
#include "config.h"
#include "recorder.h"
#include "util/log.h"
#include "util/str_util.h"
void
scrcpy_print_usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr,
"Usage: %s [options]\n"
"\n"
"Options:\n"
"\n"
" --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -b, --bit-rate value\n"
" Encode the video at the given bit-rate, expressed in bits/s.\n"
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n"
"\n"
" --codec-options key[:type]=value[,...]\n"
" Set a list of comma-separated key:type=value options for the\n"
" device encoder.\n"
" The possible values for 'type' are 'int' (default), 'long',\n"
" 'float' and 'string'.\n"
" The list of possible codec options is available in the\n"
" Android documentation:\n"
" <https://d.android.com/reference/android/media/MediaFormat>\n"
"\n"
" --crop width:height:x:y\n"
" Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n"
"\n"
" --display id\n"
" Specify the display id to mirror.\n"
"\n"
" The list of possible display ids can be listed by:\n"
" adb shell dumpsys display\n"
" (search \"mDisplayId=\" in the output)\n"
"\n"
" Default is 0.\n"
"\n"
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
" -h, --help\n"
" Print this help.\n"
"\n"
" --lock-video-orientation value\n"
" Lock video orientation to value.\n"
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n"
" Natural device orientation is 0, and each increment adds a\n"
" 90 degrees rotation counterclockwise.\n"
" Default is %d%s.\n"
"\n"
" --max-fps value\n"
" Limit the frame rate of screen capture (officially supported\n"
" since Android 10, but may work on earlier versions).\n"
"\n"
" -m, --max-size value\n"
" Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n"
" is preserved.\n"
" Default is %d%s.\n"
"\n"
" -n, --no-control\n"
" Disable device control (mirror the device in read-only).\n"
"\n"
" -N, --no-display\n"
" Do not display device (only when screen recording is\n"
" enabled).\n"
"\n"
" --no-mipmaps\n"
" If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n"
" mipmaps are automatically generated to improve downscaling\n"
" quality. This option disables the generation of mipmaps.\n"
"\n"
" -p, --port port[:port]\n"
" Set the TCP port (range) used by the client to listen.\n"
" Default is %d:%d.\n"
"\n"
" --prefer-text\n"
" Inject alpha characters and space as text events instead of\n"
" key events.\n"
" This avoids issues when combining multiple keys to enter a\n"
" special character, but breaks the expected behavior of alpha\n"
" keys in games (typically WASD).\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"
" Record screen to file.\n"
" The format is determined by the --record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n"
"\n"
" --record-format format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" --render-driver name\n"
" Request SDL to use the given render driver (this is just a\n"
" hint).\n"
" Supported names are currently \"direct3d\", \"opengl\",\n"
" \"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
" <https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>\n"
"\n"
" --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" --rotation value\n"
" Set the initial display rotation.\n"
" Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n"
" degrees rotation counterclockwise.\n"
"\n"
" -s, --serial serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
" -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n"
"\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, restore the initial value\n"
" on exit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
" -w, --stay-awake\n"
" Keep the device on while scrcpy is running.\n"
"\n"
" --window-borderless\n"
" Disable window decorations (display borderless window).\n"
"\n"
" --window-title text\n"
" Set a custom window title.\n"
"\n"
" --window-x value\n"
" Set the initial window horizontal position.\n"
" Default is \"auto\".\n"
"\n"
" --window-y value\n"
" Set the initial window vertical position.\n"
" Default is \"auto\".\n"
"\n"
" --window-width value\n"
" Set the initial window width.\n"
" Default is 0 (automatic).\n"
"\n"
" --window-height value\n"
" Set the initial window width.\n"
" Default is 0 (automatic).\n"
"\n"
"Shortcuts:\n"
"\n"
" " CTRL_OR_CMD "+f\n"
" Switch fullscreen mode\n"
"\n"
" " CTRL_OR_CMD "+Left\n"
" Rotate display left\n"
"\n"
" " CTRL_OR_CMD "+Right\n"
" Rotate display right\n"
"\n"
" " CTRL_OR_CMD "+g\n"
" Resize window to 1:1 (pixel-perfect)\n"
"\n"
" " CTRL_OR_CMD "+x\n"
" Double-click on black borders\n"
" Resize window to remove black borders\n"
"\n"
" Ctrl+h\n"
" Middle-click\n"
" Click on HOME\n"
"\n"
" " CTRL_OR_CMD "+b\n"
" " CTRL_OR_CMD "+Backspace\n"
" Right-click (when screen is on)\n"
" Click on BACK\n"
"\n"
" " CTRL_OR_CMD "+s\n"
" Click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" Click on MENU\n"
"\n"
" " CTRL_OR_CMD "+Up\n"
" Click on VOLUME_UP\n"
"\n"
" " CTRL_OR_CMD "+Down\n"
" Click on VOLUME_DOWN\n"
"\n"
" " CTRL_OR_CMD "+p\n"
" Click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" Power on\n"
"\n"
" " CTRL_OR_CMD "+o\n"
" Turn device screen off (keep mirroring)\n"
"\n"
" " CTRL_OR_CMD "+r\n"
" Rotate device screen\n"
"\n"
" " CTRL_OR_CMD "+n\n"
" Expand notification panel\n"
"\n"
" " CTRL_OR_CMD "+Shift+n\n"
" Collapse notification panel\n"
"\n"
" " CTRL_OR_CMD "+c\n"
" Copy device clipboard to computer\n"
"\n"
" " CTRL_OR_CMD "+v\n"
" Paste computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+Shift+v\n"
" Copy computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+i\n"
" Enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
" Install APK from computer\n"
"\n",
arg0,
DEFAULT_BIT_RATE,
DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)",
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST);
}
static bool
parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
long max, const char *name) {
long value;
bool ok;
if (accept_suffix) {
ok = parse_integer_with_suffix(s, &value);
} else {
ok = parse_integer(s, &value);
}
if (!ok) {
LOGE("Could not parse %s: %s", name, s);
return false;
}
if (value < min || value > max) {
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
name, value, min, max);
return false;
}
*out = value;
return true;
}
static size_t
parse_integers_arg(const char *s, size_t max_items, long *out, long min,
long max, const char *name) {
size_t count = parse_integers(s, ':', max_items, out);
if (!count) {
LOGE("Could not parse %s: %s", name, s);
return 0;
}
for (size_t i = 0; i < count; ++i) {
long value = out[i];
if (value < min || value > max) {
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
name, value, min, max);
return 0;
}
}
return count;
}
static bool
parse_bit_rate(const char *s, uint32_t *bit_rate) {
long value;
// long may be 32 bits (it is the case on mingw), so do not use more than
// 31 bits (long is signed)
bool ok = parse_integer_arg(s, &value, true, 0, 0x7FFFFFFF, "bit-rate");
if (!ok) {
return false;
}
*bit_rate = (uint32_t) value;
return true;
}
static bool
parse_max_size(const char *s, uint16_t *max_size) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size");
if (!ok) {
return false;
}
*max_size = (uint16_t) value;
return true;
}
static bool
parse_max_fps(const char *s, uint16_t *max_fps) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps");
if (!ok) {
return false;
}
*max_fps = (uint16_t) value;
return true;
}
static bool
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
long value;
bool ok = parse_integer_arg(s, &value, false, -1, 3,
"lock video orientation");
if (!ok) {
return false;
}
*lock_video_orientation = (int8_t) value;
return true;
}
static bool
parse_rotation(const char *s, uint8_t *rotation) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
if (!ok) {
return false;
}
*rotation = (uint8_t) value;
return true;
}
static bool
parse_window_position(const char *s, int16_t *position) {
// special value for "auto"
static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value");
if (!strcmp(s, "auto")) {
*position = WINDOW_POSITION_UNDEFINED;
return true;
}
long value;
bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF,
"window position");
if (!ok) {
return false;
}
*position = (int16_t) value;
return true;
}
static bool
parse_window_dimension(const char *s, uint16_t *dimension) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF,
"window dimension");
if (!ok) {
return false;
}
*dimension = (uint16_t) value;
return true;
}
static bool
parse_port_range(const char *s, struct port_range *port_range) {
long values[2];
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
if (!count) {
return false;
}
uint16_t v0 = (uint16_t) values[0];
if (count == 1) {
port_range->first = v0;
port_range->last = v0;
return true;
}
assert(count == 2);
uint16_t v1 = (uint16_t) values[1];
if (v0 < v1) {
port_range->first = v0;
port_range->last = v1;
} else {
port_range->first = v1;
port_range->last = v0;
}
return true;
}
static bool
parse_display_id(const char *s, uint16_t *display_id) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id");
if (!ok) {
return false;
}
*display_id = (uint16_t) value;
return true;
}
static bool
parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) {
*format = RECORDER_FORMAT_MP4;
return true;
}
if (!strcmp(optarg, "mkv")) {
*format = RECORDER_FORMAT_MKV;
return true;
}
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
}
static enum recorder_format
guess_record_format(const char *filename) {
size_t len = strlen(filename);
if (len < 4) {
return 0;
}
const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) {
return RECORDER_FORMAT_MP4;
}
if (!strcmp(ext, ".mkv")) {
return RECORDER_FORMAT_MKV;
}
return 0;
}
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006
#define OPT_WINDOW_X 1007
#define OPT_WINDOW_Y 1008
#define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012
#define OPT_LOCK_VIDEO_ORIENTATION 1013
#define OPT_DISPLAY_ID 1014
#define OPT_ROTATION 1015
#define OPT_RENDER_DRIVER 1016
#define OPT_NO_MIPMAPS 1017
#define OPT_CODEC_OPTIONS 1018
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'},
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
{"crop", required_argument, NULL, OPT_CROP},
{"display", required_argument, NULL, OPT_DISPLAY_ID},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"lock-video-orientation", required_argument, NULL,
OPT_LOCK_VIDEO_ORIENTATION},
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'},
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
{"port", required_argument, NULL, 'p'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"render-driver", required_argument, NULL, OPT_RENDER_DRIVER},
{"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES},
{"rotation", required_argument, NULL, OPT_ROTATION},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-x", required_argument, NULL, OPT_WINDOW_X},
{"window-y", required_argument, NULL, OPT_WINDOW_Y},
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
{"window-borderless", no_argument, NULL,
OPT_WINDOW_BORDERLESS},
{NULL, 0, NULL, 0 },
};
struct scrcpy_options *opts = &args->opts;
optind = 0; // reset to start from the first argument in tests
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options,
NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
return false;
}
break;
case 'c':
LOGW("Deprecated option -c. Use --crop instead.");
// fall through
case OPT_CROP:
opts->crop = optarg;
break;
case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
}
break;
case 'f':
opts->fullscreen = true;
break;
case 'F':
LOGW("Deprecated option -F. Use --record-format instead.");
// fall through
case OPT_RECORD_FORMAT:
if (!parse_record_format(optarg, &opts->record_format)) {
return false;
}
break;
case 'h':
args->help = true;
break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
break;
case 'm':
if (!parse_max_size(optarg, &opts->max_size)) {
return false;
}
break;
case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) {
return false;
}
break;
case 'n':
opts->control = false;
break;
case 'N':
opts->display = false;
break;
case 'p':
if (!parse_port_range(optarg, &opts->port_range)) {
return false;
}
break;
case 'r':
opts->record_filename = optarg;
break;
case 's':
opts->serial = optarg;
break;
case 'S':
opts->turn_screen_off = true;
break;
case 't':
opts->show_touches = true;
break;
case 'T':
LOGW("Deprecated option -T. Use --always-on-top instead.");
// fall through
case OPT_ALWAYS_ON_TOP:
opts->always_on_top = true;
break;
case 'v':
args->version = true;
break;
case 'w':
opts->stay_awake = true;
break;
case OPT_RENDER_EXPIRED_FRAMES:
opts->render_expired_frames = true;
break;
case OPT_WINDOW_TITLE:
opts->window_title = optarg;
break;
case OPT_WINDOW_X:
if (!parse_window_position(optarg, &opts->window_x)) {
return false;
}
break;
case OPT_WINDOW_Y:
if (!parse_window_position(optarg, &opts->window_y)) {
return false;
}
break;
case OPT_WINDOW_WIDTH:
if (!parse_window_dimension(optarg, &opts->window_width)) {
return false;
}
break;
case OPT_WINDOW_HEIGHT:
if (!parse_window_dimension(optarg, &opts->window_height)) {
return false;
}
break;
case OPT_WINDOW_BORDERLESS:
opts->window_borderless = true;
break;
case OPT_PUSH_TARGET:
opts->push_target = optarg;
break;
case OPT_PREFER_TEXT:
opts->prefer_text = true;
break;
case OPT_ROTATION:
if (!parse_rotation(optarg, &opts->rotation)) {
return false;
}
break;
case OPT_RENDER_DRIVER:
opts->render_driver = optarg;
break;
case OPT_NO_MIPMAPS:
opts->mipmaps = false;
break;
case OPT_CODEC_OPTIONS:
opts->codec_options = optarg;
break;
default:
// getopt prints the error message on stderr
return false;
}
}
if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return false;
}
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (opts->record_filename && !opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)",
opts->record_filename);
return false;
}
}
if (!opts->control && opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
if (!opts->control && opts->stay_awake) {
LOGE("Could not request to stay awake if control is disabled");
return false;
}
return true;
}

21
app/src/cli.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef SCRCPY_CLI_H
#define SCRCPY_CLI_H
#include <stdbool.h>
#include "config.h"
#include "scrcpy.h"
struct scrcpy_cli_args {
struct scrcpy_options opts;
bool help;
bool version;
};
void
scrcpy_print_usage(const char *arg0);
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]);
#endif

View File

@ -5,9 +5,10 @@
#include <stdlib.h>
#include <string.h>
#include "config.h"
#include "common.h"
#include "log.h"
#include "str_util.h"
#include "util/log.h"
#include "util/str_util.h"
static const char *adb_command;
@ -54,6 +55,32 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
return idx;
}
static void
show_adb_installation_msg() {
#ifndef __WINDOWS__
static const struct {
const char *binary;
const char *command;
} pkg_managers[] = {
{"apt", "apt install adb"},
{"apt-get", "apt-get install adb"},
{"brew", "brew cask install android-platform-tools"},
{"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"},
};
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
if (cmd_search(pkg_managers[i].binary)) {
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
return;
}
}
#endif
LOGI("You may download and install 'adb' from "
"https://developer.android.com/studio/releases/platform-tools");
}
static void
show_adb_err_msg(enum process_result err, const char *const argv[]) {
char buf[512];
@ -67,6 +94,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)");
show_adb_installation_msg();
break;
case PROCESS_SUCCESS:
// do nothing
@ -90,7 +118,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd[0], cmd, &process);
enum process_result r = cmd_execute(cmd, &process);
if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, cmd);
return PROCESS_NONE;

View File

@ -18,6 +18,7 @@
# define PRIsizet PRIu32
# endif
# define PROCESS_NONE NULL
# define NO_EXIT_CODE -1u // max value as unsigned
typedef HANDLE process_t;
typedef DWORD exit_code_t;
@ -28,12 +29,13 @@
# define PRIsizet "zu"
# define PRIexitcode "d"
# define PROCESS_NONE -1
# define NO_EXIT_CODE -1
typedef pid_t process_t;
typedef int exit_code_t;
#endif
# define NO_EXIT_CODE -1
#include "config.h"
enum process_result {
PROCESS_SUCCESS,
@ -41,8 +43,13 @@ enum process_result {
PROCESS_ERROR_MISSING_BINARY,
};
#ifndef __WINDOWS__
bool
cmd_search(const char *file);
#endif
enum process_result
cmd_execute(const char *path, const char *const argv[], process_t *process);
cmd_execute(const char *const argv[], process_t *process);
bool
cmd_terminate(process_t pid);
@ -83,4 +90,8 @@ process_check_success(process_t proc, const char *name);
char *
get_executable_path(void);
// returns true if the file exists and is not a directory
bool
is_regular_file(const char *path);
#endif

View File

@ -3,6 +3,8 @@
#include <stdint.h>
#include "config.h"
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)
@ -25,4 +27,9 @@ struct position {
struct point point;
};
struct port_range {
uint16_t first;
uint16_t last;
};
#endif

View File

@ -1,10 +1,12 @@
#include "control_msg.h"
#include <assert.h>
#include <string.h>
#include "buffer_util.h"
#include "log.h"
#include "str_util.h"
#include "config.h"
#include "util/buffer_util.h"
#include "util/log.h"
#include "util/str_util.h"
static void
write_position(uint8_t *buf, const struct position *position) {
@ -23,6 +25,16 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
return 2 + len;
}
static uint16_t
to_fixed_point_16(float f) {
assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
u = 0xffff;
}
return (uint16_t) u;
}
size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buf[0] = msg->type;
@ -33,15 +45,20 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
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]);
size_t len =
write_string(msg->inject_text.text,
CONTROL_MSG_INJECT_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_TOUCH_EVENT:
buf[1] = msg->inject_touch_event.action;
buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id);
write_position(&buf[10], &msg->inject_touch_event.position);
uint16_t pressure =
to_fixed_point_16(msg->inject_touch_event.pressure);
buffer_write16be(&buf[22], pressure);
buffer_write32be(&buf[24], msg->inject_touch_event.buttons);
return 28;
case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
buffer_write32be(&buf[13],
@ -62,6 +79,7 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data
return 1;
default:

View File

@ -5,19 +5,22 @@
#include <stddef.h>
#include <stdint.h>
#include "config.h"
#include "android/input.h"
#include "android/keycodes.h"
#include "common.h"
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_INJECT_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)
#define POINTER_ID_MOUSE UINT64_C(-1);
enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_KEYCODE,
CONTROL_MSG_TYPE_INJECT_TEXT,
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
@ -25,6 +28,7 @@ enum control_msg_type {
CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
CONTROL_MSG_TYPE_ROTATE_DEVICE,
};
enum screen_power_mode {
@ -47,8 +51,10 @@ struct control_msg {
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
uint64_t pointer_id;
struct position position;
} inject_mouse_event;
float pressure;
} inject_touch_event;
struct {
struct position position;
int32_t hscroll;

View File

@ -1,10 +1,10 @@
#include "controller.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include "config.h"
#include "lock_util.h"
#include "log.h"
#include "util/lock.h"
#include "util/log.h"
bool
controller_init(struct controller *controller, socket_t control_socket) {
@ -85,7 +85,8 @@ run_controller(void *data) {
}
struct control_msg msg;
bool non_empty = cbuf_take(&controller->queue, &msg);
SDL_assert(non_empty);
assert(non_empty);
(void) non_empty;
mutex_unlock(controller->mutex);
bool ok = process_msg(controller, &msg);

View File

@ -5,10 +5,11 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "cbuf.h"
#include "config.h"
#include "control_msg.h"
#include "net.h"
#include "receiver.h"
#include "util/cbuf.h"
#include "util/net.h"
struct control_msg_queue CBUF(struct control_msg, 64);

View File

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

View File

@ -2,20 +2,18 @@
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "buffer_util.h"
#include "compat.h"
#include "events.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h"
#include "video_buffer.h"
#include "util/buffer_util.h"
#include "util/log.h"
// set the decoded frame as ready for rendering, and notify
static void

View File

@ -4,6 +4,8 @@
#include <stdbool.h>
#include <libavformat/avformat.h>
#include "config.h"
struct video_buffer;
struct decoder {

View File

@ -1,5 +1,7 @@
#include "device.h"
#include "log.h"
#include "config.h"
#include "util/log.h"
bool
device_read_info(socket_t device_socket, char *device_name, struct size *size) {

View File

@ -3,8 +3,9 @@
#include <stdbool.h>
#include "config.h"
#include "common.h"
#include "net.h"
#include "util/net.h"
#define DEVICE_NAME_FIELD_LENGTH 64

View File

@ -1,10 +1,10 @@
#include "device_msg.h"
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "buffer_util.h"
#include "log.h"
#include "config.h"
#include "util/buffer_util.h"
#include "util/log.h"
ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len,

View File

@ -5,6 +5,8 @@
#include <stdint.h>
#include <unistd.h>
#include "config.h"
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)

View File

@ -1,9 +1,11 @@
#include "convert.h"
#include "event_converter.h"
#include "config.h"
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
static bool
bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
@ -31,8 +33,7 @@ autocomplete_metastate(enum android_metastate metastate) {
return metastate;
}
static enum android_metastate
enum android_metastate
convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) {
@ -73,8 +74,9 @@ convert_meta_state(SDL_Keymod mod) {
return autocomplete_metastate(metastate);
}
static bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text) {
switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
@ -91,6 +93,29 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
}
if (!(mod & (KMOD_NUM | KMOD_SHIFT))) {
// Handle Numpad events when Num Lock is disabled
// If SHIFT is pressed, a text event will be sent instead
switch(from) {
MAP(SDLK_KP_0, AKEYCODE_INSERT);
MAP(SDLK_KP_1, AKEYCODE_MOVE_END);
MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN);
MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN);
MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT);
MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT);
MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME);
MAP(SDLK_KP_8, AKEYCODE_DPAD_UP);
MAP(SDLK_KP_9, AKEYCODE_PAGE_UP);
MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL);
}
}
if (prefer_text) {
// do not forward alpha and space key events
return false;
}
if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) {
return false;
}
@ -127,16 +152,7 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod) {
}
}
static bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
FAIL;
}
}
static enum android_motionevent_buttons
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) {
enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) {
@ -148,81 +164,30 @@ convert_mouse_buttons(uint32_t state) {
if (state & SDL_BUTTON_MMASK) {
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
}
if (state & SDL_BUTTON_X1) {
if (state & SDL_BUTTON_X1MASK) {
buttons |= AMOTION_EVENT_BUTTON_BACK;
}
if (state & SDL_BUTTON_X2) {
if (state & SDL_BUTTON_X2MASK) {
buttons |= AMOTION_EVENT_BUTTON_FORWARD;
}
return buttons;
}
bool
input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
FAIL;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod)) {
return false;
}
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
bool
mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
if (!convert_mouse_action(from->type, &to->inject_mouse_event.action)) {
return false;
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
switch (from) {
MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN);
MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP);
FAIL;
}
to->inject_mouse_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
return true;
}
bool
mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT;
to->inject_mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_mouse_event.buttons = convert_mouse_buttons(from->state);
to->inject_mouse_event.position.screen_size = screen_size;
to->inject_mouse_event.position.point.x = from->x;
to->inject_mouse_event.position.point.y = from->y;
return true;
}
bool
mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->inject_scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->inject_scroll_event.hscroll = -mul * from->x;
to->inject_scroll_event.vscroll = mul * from->y;
return true;
}

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

@ -0,0 +1,29 @@
#ifndef CONVERT_H
#define CONVERT_H
#include <stdbool.h>
#include <SDL2/SDL_events.h>
#include "config.h"
#include "control_msg.h"
bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to);
enum android_metastate
convert_meta_state(SDL_Keymod mod);
bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text);
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state);
bool
convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to);
bool
convert_touch_action(SDL_EventType from, enum android_motionevent_action *to);
#endif

View File

@ -1,12 +1,12 @@
#include "file_handler.h"
#include <assert.h>
#include <string.h>
#include <SDL2/SDL_assert.h>
#include "config.h"
#include "command.h"
#include "lock_util.h"
#include "log.h"
#include "util/lock.h"
#include "util/log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/"
@ -120,7 +120,8 @@ run_file_handler(void *data) {
}
struct file_handler_request req;
bool non_empty = cbuf_take(&file_handler->queue, &req);
SDL_assert(non_empty);
assert(non_empty);
(void) non_empty;
process_t process;
if (req.action == ACTION_INSTALL_APK) {

View File

@ -5,8 +5,9 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "cbuf.h"
#include "config.h"
#include "command.h"
#include "util/cbuf.h"
typedef enum {
ACTION_INSTALL_APK,

View File

@ -1,10 +1,11 @@
#include "fps_counter.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include <SDL2/SDL_timer.h>
#include "lock_util.h"
#include "log.h"
#include "config.h"
#include "util/lock.h"
#include "util/log.h"
#define FPS_COUNTER_INTERVAL_MS 1000
@ -22,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) {
}
counter->thread = NULL;
SDL_AtomicSet(&counter->started, 0);
atomic_init(&counter->started, 0);
// no need to initialize the other fields, they are unused until started
return true;
@ -34,6 +35,16 @@ fps_counter_destroy(struct fps_counter *counter) {
SDL_DestroyMutex(counter->mutex);
}
static inline bool
is_started(struct fps_counter *counter) {
return atomic_load_explicit(&counter->started, memory_order_acquire);
}
static inline void
set_started(struct fps_counter *counter, bool started) {
atomic_store_explicit(&counter->started, started, memory_order_release);
}
// must be called with mutex locked
static void
display_fps(struct fps_counter *counter) {
@ -69,14 +80,14 @@ run_fps_counter(void *data) {
mutex_lock(counter->mutex);
while (!counter->interrupted) {
while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) {
while (!counter->interrupted && !is_started(counter)) {
cond_wait(counter->state_cond, counter->mutex);
}
while (!counter->interrupted && SDL_AtomicGet(&counter->started)) {
while (!counter->interrupted && is_started(counter)) {
uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now);
SDL_assert(counter->next_timestamp > now);
assert(counter->next_timestamp > now);
uint32_t remaining = counter->next_timestamp - now;
// ignore the reason (timeout or signaled), we just loop anyway
@ -95,7 +106,7 @@ fps_counter_start(struct fps_counter *counter) {
counter->nr_skipped = 0;
mutex_unlock(counter->mutex);
SDL_AtomicSet(&counter->started, 1);
set_started(counter, true);
cond_signal(counter->state_cond);
// counter->thread is always accessed from the same thread, no need to lock
@ -113,13 +124,13 @@ fps_counter_start(struct fps_counter *counter) {
void
fps_counter_stop(struct fps_counter *counter) {
SDL_AtomicSet(&counter->started, 0);
set_started(counter, false);
cond_signal(counter->state_cond);
}
bool
fps_counter_is_started(struct fps_counter *counter) {
return SDL_AtomicGet(&counter->started);
return is_started(counter);
}
void
@ -144,7 +155,7 @@ fps_counter_join(struct fps_counter *counter) {
void
fps_counter_add_rendered_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) {
if (!is_started(counter)) {
return;
}
@ -157,7 +168,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) {
void
fps_counter_add_skipped_frame(struct fps_counter *counter) {
if (!SDL_AtomicGet(&counter->started)) {
if (!is_started(counter)) {
return;
}

View File

@ -1,12 +1,14 @@
#ifndef FPSCOUNTER_H
#define FPSCOUNTER_H
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
struct fps_counter {
SDL_Thread *thread;
SDL_mutex *mutex;
@ -14,7 +16,7 @@ struct fps_counter {
// atomic so that we can check without locking the mutex
// if the FPS counter is disabled, we don't want to lock unnecessarily
SDL_atomic_t started;
atomic_bool started;
// the following fields are protected by the mutex
bool interrupted;

View File

@ -1,9 +1,11 @@
#include "input_manager.h"
#include <SDL2/SDL_assert.h>
#include "convert.h"
#include "lock_util.h"
#include "log.h"
#include <assert.h>
#include "config.h"
#include "event_converter.h"
#include "util/lock.h"
#include "util/log.h"
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
// coordinates (as provided in SDL mouse events)
@ -102,7 +104,7 @@ press_back_or_turn_screen_on(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'turn screen on'");
LOGW("Could not request 'press back or turn screen on'");
}
}
@ -209,15 +211,40 @@ clipboard_paste(struct controller *controller) {
}
}
void
input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
SDL_assert(event->text[1] == '\0');
// letters and space are handled as raw key event
return;
static void
rotate_device(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device rotation");
}
}
static void
rotate_client_left(struct screen *screen) {
unsigned new_rotation = (screen->rotation + 1) % 4;
screen_set_rotation(screen, new_rotation);
}
static void
rotate_client_right(struct screen *screen) {
unsigned new_rotation = (screen->rotation + 3) % 4;
screen_set_rotation(screen, new_rotation);
}
void
input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) {
if (!im->prefer_text) {
char c = event->text[0];
if (isalpha(c) || c == ' ') {
assert(event->text[1] == '\0');
// letters and space are handled as raw key event
return;
}
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text);
@ -225,14 +252,34 @@ input_manager_process_text_input(struct input_manager *input_manager,
LOGW("Could not strdup input text");
return;
}
if (!controller_push_msg(input_manager->controller, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
SDL_free(msg.inject_text.text);
LOGW("Could not request 'inject text'");
}
}
static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
return false;
}
uint16_t mod = from->keysym.mod;
if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
prefer_text)) {
return false;
}
to->inject_keycode.metastate = convert_meta_state(mod);
return true;
}
void
input_manager_process_key(struct input_manager *input_manager,
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event,
bool control) {
// control: indicates the state of the command-line option --no-control
@ -259,7 +306,7 @@ input_manager_process_key(struct input_manager *input_manager,
return;
}
struct controller *controller = input_manager->controller;
struct controller *controller = im->controller;
// capture all Ctrl events
if (ctrl || cmd) {
@ -316,6 +363,16 @@ input_manager_process_key(struct input_manager *input_manager,
action_volume_up(controller, action);
}
return;
case SDLK_LEFT:
if (cmd && !shift && down) {
rotate_client_left(im->screen);
}
return;
case SDLK_RIGHT:
if (cmd && !shift && down) {
rotate_client_right(im->screen);
}
return;
case SDLK_c:
if (control && cmd && !shift && !repeat && down) {
request_device_clipboard(controller);
@ -334,23 +391,23 @@ input_manager_process_key(struct input_manager *input_manager,
return;
case SDLK_f:
if (!shift && cmd && !repeat && down) {
screen_switch_fullscreen(input_manager->screen);
screen_switch_fullscreen(im->screen);
}
return;
case SDLK_x:
if (!shift && cmd && !repeat && down) {
screen_resize_to_fit(input_manager->screen);
screen_resize_to_fit(im->screen);
}
return;
case SDLK_g:
if (!shift && cmd && !repeat && down) {
screen_resize_to_pixel_perfect(input_manager->screen);
screen_resize_to_pixel_perfect(im->screen);
}
return;
case SDLK_i:
if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter =
input_manager->video_buffer->fps_counter;
im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter);
}
return;
@ -363,6 +420,11 @@ input_manager_process_key(struct input_manager *input_manager,
}
}
return;
case SDLK_r:
if (control && cmd && !shift && !repeat && down) {
rotate_device(controller);
}
return;
}
return;
@ -373,56 +435,129 @@ input_manager_process_key(struct input_manager *input_manager,
}
struct control_msg msg;
if (input_key_from_sdl_to_android(event, &msg)) {
if (convert_input_key(event, &msg, im->prefer_text)) {
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'");
}
}
}
static bool
convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, from->x, from->y);
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
return true;
}
void
input_manager_process_mouse_motion(struct input_manager *input_manager,
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
// do not send motion events when no button is pressed
return;
}
if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate
return;
}
struct control_msg msg;
if (mouse_motion_from_sdl_to_android(event,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
if (convert_mouse_motion(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'");
}
}
}
static bool
is_outside_device_screen(struct input_manager *input_manager, int x, int y)
{
return x < 0 || x >= input_manager->screen->frame_size.width ||
y < 0 || y >= input_manager->screen->frame_size.height;
convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = screen->frame_size;
// SDL touch event coordinates are normalized in the range [0; 1]
float x = from->x * screen->content_size.width;
float y = from->y * screen->content_size.height;
to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, x, y);
to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0;
return true;
}
void
input_manager_process_mouse_button(struct input_manager *input_manager,
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event) {
struct control_msg msg;
if (convert_touch(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject touch event'");
}
}
}
static bool
is_outside_device_screen(struct input_manager *im, int x, int y)
{
return x < 0 || x >= im->screen->content_size.width ||
y < 0 || y >= im->screen->content_size.height;
}
static bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
struct control_msg *to) {
to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
return false;
}
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, from->x, from->y);
to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button));
return true;
}
void
input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event,
bool control) {
if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate
return;
}
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
press_back_or_turn_screen_on(im->controller);
return;
}
if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller, ACTION_DOWN | ACTION_UP);
action_home(im->controller, ACTION_DOWN | ACTION_UP);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside =
is_outside_device_screen(input_manager, event->x, event->y);
is_outside_device_screen(im, event->x, event->y);
if (outside) {
screen_resize_to_fit(input_manager->screen);
screen_resize_to_fit(im->screen);
return;
}
}
@ -434,25 +569,36 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
}
struct control_msg msg;
if (mouse_button_from_sdl_to_android(event,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
if (convert_mouse_button(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'");
}
}
}
void
input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event) {
static bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
struct control_msg *to) {
struct position position = {
.screen_size = input_manager->screen->frame_size,
.point = get_mouse_point(input_manager->screen),
.screen_size = screen->frame_size,
.point = get_mouse_point(screen),
};
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
to->inject_scroll_event.position = position;
to->inject_scroll_event.hscroll = from->x;
to->inject_scroll_event.vscroll = from->y;
return true;
}
void
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event) {
struct control_msg msg;
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
if (convert_mouse_wheel(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'");
}
}

View File

@ -3,6 +3,7 @@
#include <stdbool.h>
#include "config.h"
#include "common.h"
#include "controller.h"
#include "fps_counter.h"
@ -13,28 +14,33 @@ struct input_manager {
struct controller *controller;
struct video_buffer *video_buffer;
struct screen *screen;
bool prefer_text;
};
void
input_manager_process_text_input(struct input_manager *input_manager,
input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event);
void
input_manager_process_key(struct input_manager *input_manager,
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event,
bool control);
void
input_manager_process_mouse_motion(struct input_manager *input_manager,
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event);
void
input_manager_process_mouse_button(struct input_manager *input_manager,
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event);
void
input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event,
bool control);
void
input_manager_process_mouse_wheel(struct input_manager *input_manager,
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event);
#endif

View File

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

View File

@ -1,191 +1,15 @@
#include "scrcpy.h"
#include <getopt.h>
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
#include "compat.h"
#include "config.h"
#include "log.h"
#include "recorder.h"
struct args {
const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
bool fullscreen;
bool no_control;
bool no_display;
bool help;
bool version;
bool show_touches;
uint16_t port;
uint16_t max_size;
uint32_t bit_rate;
bool always_on_top;
bool turn_screen_off;
bool render_expired_frames;
};
static void usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr,
"Usage: %s [options]\n"
"\n"
"Options:\n"
"\n"
" -b, --bit-rate value\n"
" Encode the video at the given bit-rate, expressed in bits/s.\n"
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n"
"\n"
" -c, --crop width:height:x:y\n"
" Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n"
"\n"
" -f, --fullscreen\n"
" Start in fullscreen.\n"
"\n"
" -F, --record-format\n"
" Force recording format (either mp4 or mkv).\n"
"\n"
" -h, --help\n"
" Print this help.\n"
"\n"
" -m, --max-size value\n"
" Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n"
" is preserved.\n"
" Default is %d%s.\n"
"\n"
" -n, --no-control\n"
" Disable device control (mirror the device in read-only).\n"
"\n"
" -N, --no-display\n"
" Do not display device (only when screen recording is\n"
" enabled).\n"
"\n"
" -p, --port port\n"
" Set the TCP port the client listens on.\n"
" Default is %d.\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"
" Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n"
" set, or by the file extension (.mp4 or .mkv).\n"
"\n"
" --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" -s, --serial serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
" -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n"
"\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -T, --always-on-top\n"
" Make scrcpy window always on top (above other windows).\n"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
" --window-title text\n"
" Set a custom window title.\n"
"\n"
"Shortcuts:\n"
"\n"
" " CTRL_OR_CMD "+f\n"
" switch fullscreen mode\n"
"\n"
" " CTRL_OR_CMD "+g\n"
" resize window to 1:1 (pixel-perfect)\n"
"\n"
" " CTRL_OR_CMD "+x\n"
" Double-click on black borders\n"
" resize window to remove black borders\n"
"\n"
" Ctrl+h\n"
" Middle-click\n"
" click on HOME\n"
"\n"
" " CTRL_OR_CMD "+b\n"
" " CTRL_OR_CMD "+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n"
"\n"
" " CTRL_OR_CMD "+s\n"
" click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" click on MENU\n"
"\n"
" " CTRL_OR_CMD "+Up\n"
" click on VOLUME_UP\n"
"\n"
" " CTRL_OR_CMD "+Down\n"
" click on VOLUME_DOWN\n"
"\n"
" " CTRL_OR_CMD "+p\n"
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" power on\n"
"\n"
" " CTRL_OR_CMD "+o\n"
" turn device screen off (keep mirroring)\n"
"\n"
" " CTRL_OR_CMD "+n\n"
" expand notification panel\n"
"\n"
" " CTRL_OR_CMD "+Shift+n\n"
" collapse notification panel\n"
"\n"
" " CTRL_OR_CMD "+c\n"
" copy device clipboard to computer\n"
"\n"
" " CTRL_OR_CMD "+v\n"
" paste computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+Shift+v\n"
" copy computer clipboard to device\n"
"\n"
" " CTRL_OR_CMD "+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Drag & drop APK file\n"
" install APK from computer\n"
"\n",
arg0,
DEFAULT_BIT_RATE,
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT);
}
#include "cli.h"
#include "compat.h"
#include "util/log.h"
static void
print_version(void) {
@ -205,250 +29,6 @@ print_version(void) {
LIBAVUTIL_VERSION_MICRO);
}
static bool
parse_bit_rate(char *optarg, uint32_t *bit_rate) {
char *endptr;
if (*optarg == '\0') {
LOGE("Bit-rate parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
int mul = 1;
if (*endptr != '\0') {
if (optarg == endptr) {
LOGE("Invalid bit-rate: %s", optarg);
return false;
}
if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') {
mul = 1000000;
} else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') {
mul = 1000;
} else {
LOGE("Invalid bit-rate unit: %s", optarg);
return false;
}
}
if (value < 0 || ((uint32_t) -1) / mul < value) {
LOGE("Bitrate must be positive and less than 2^32: %s", optarg);
return false;
}
*bit_rate = (uint32_t) value * mul;
return true;
}
static bool
parse_max_size(char *optarg, uint16_t *max_size) {
char *endptr;
if (*optarg == '\0') {
LOGE("Max size parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid max size: %s", optarg);
return false;
}
if (value & ~0xffff) {
LOGE("Max size must be between 0 and 65535: %ld", value);
return false;
}
*max_size = (uint16_t) value;
return true;
}
static bool
parse_port(char *optarg, uint16_t *port) {
char *endptr;
if (*optarg == '\0') {
LOGE("Invalid port parameter is empty");
return false;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid port: %s", optarg);
return false;
}
if (value & ~0xffff) {
LOGE("Port out of range: %ld", value);
return false;
}
*port = (uint16_t) value;
return true;
}
static bool
parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(optarg, "mp4")) {
*format = RECORDER_FORMAT_MP4;
return true;
}
if (!strcmp(optarg, "mkv")) {
*format = RECORDER_FORMAT_MKV;
return true;
}
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
}
static enum recorder_format
guess_record_format(const char *filename) {
size_t len = strlen(filename);
if (len < 4) {
return 0;
}
const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) {
return RECORDER_FORMAT_MP4;
}
if (!strcmp(ext, ".mkv")) {
return RECORDER_FORMAT_MKV;
}
return 0;
}
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
static bool
parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, 'T'},
{"bit-rate", required_argument, NULL, 'b'},
{"crop", required_argument, NULL, 'c'},
{"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL,
OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'},
{"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL,
OPT_WINDOW_TITLE},
{NULL, 0, NULL, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) {
return false;
}
break;
case 'c':
args->crop = optarg;
break;
case 'f':
args->fullscreen = true;
break;
case 'F':
if (!parse_record_format(optarg, &args->record_format)) {
return false;
}
break;
case 'h':
args->help = true;
break;
case 'm':
if (!parse_max_size(optarg, &args->max_size)) {
return false;
}
break;
case 'n':
args->no_control = true;
break;
case 'N':
args->no_display = true;
break;
case 'p':
if (!parse_port(optarg, &args->port)) {
return false;
}
break;
case 'r':
args->record_filename = optarg;
break;
case 's':
args->serial = optarg;
break;
case 'S':
args->turn_screen_off = true;
break;
case 't':
args->show_touches = true;
break;
case 'T':
args->always_on_top = true;
break;
case 'v':
args->version = true;
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:
// getopt prints the error message on stderr
return false;
}
}
if (args->no_display && !args->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
if (args->no_display && args->fullscreen) {
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
return false;
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return false;
}
if (args->record_format && !args->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (args->record_filename && !args->record_format) {
args->record_format = guess_record_format(args->record_filename);
if (!args->record_format) {
LOGE("No format specified for \"%s\" (try with -F mkv)",
args->record_filename);
return false;
}
}
if (args->no_control && args->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
return true;
}
int
main(int argc, char *argv[]) {
#ifdef __WINDOWS__
@ -457,31 +37,23 @@ main(int argc, char *argv[]) {
setbuf(stdout, NULL);
setbuf(stderr, NULL);
#endif
struct args args = {
.serial = NULL,
.crop = NULL,
.record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0,
#ifndef NDEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = false,
.show_touches = false,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
.always_on_top = false,
.no_control = false,
.no_display = false,
.turn_screen_off = false,
.render_expired_frames = false,
};
if (!parse_args(&args, argc, argv)) {
if (!scrcpy_parse_args(&args, argc, argv)) {
return 1;
}
if (args.help) {
usage(argv[0]);
scrcpy_print_usage(argv[0]);
return 0;
}
@ -500,29 +72,7 @@ main(int argc, char *argv[]) {
return 1;
}
#ifdef BUILD_DEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_options options = {
.serial = args.serial,
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
.fullscreen = args.fullscreen,
.always_on_top = args.always_on_top,
.control = !args.no_control,
.display = !args.no_display,
.turn_screen_off = args.turn_screen_off,
.render_expired_frames = args.render_expired_frames,
};
int res = scrcpy(&options) ? 0 : 1;
int res = scrcpy(&args.opts) ? 0 : 1;
avformat_network_deinit(); // ignore failure

56
app/src/opengl.c Normal file
View File

@ -0,0 +1,56 @@
#include "opengl.h"
#include <assert.h>
#include <stdio.h>
#include "SDL2/SDL.h"
void
sc_opengl_init(struct sc_opengl *gl) {
gl->GetString = SDL_GL_GetProcAddress("glGetString");
assert(gl->GetString);
gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf");
assert(gl->TexParameterf);
gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri");
assert(gl->TexParameteri);
// optional
gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap");
const char *version = (const char *) gl->GetString(GL_VERSION);
assert(version);
gl->version = version;
#define OPENGL_ES_PREFIX "OpenGL ES "
/* starts with "OpenGL ES " */
gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX,
sizeof(OPENGL_ES_PREFIX) - 1);
if (gl->is_opengles) {
/* skip the prefix */
version += sizeof(PREFIX) - 1;
}
int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor);
if (r != 2) {
// failed to parse the version
gl->version_major = 0;
gl->version_minor = 0;
}
}
bool
sc_opengl_version_at_least(struct sc_opengl *gl,
int minver_major, int minver_minor,
int minver_es_major, int minver_es_minor)
{
if (gl->is_opengles) {
return gl->version_major > minver_es_major
|| (gl->version_major == minver_es_major
&& gl->version_minor >= minver_es_minor);
}
return gl->version_major > minver_major
|| (gl->version_major == minver_major
&& gl->version_minor >= minver_minor);
}

36
app/src/opengl.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef SC_OPENGL_H
#define SC_OPENGL_H
#include <stdbool.h>
#include <SDL2/SDL_opengl.h>
#include "config.h"
struct sc_opengl {
const char *version;
bool is_opengles;
int version_major;
int version_minor;
const GLubyte *
(*GetString)(GLenum name);
void
(*TexParameterf)(GLenum target, GLenum pname, GLfloat param);
void
(*TexParameteri)(GLenum target, GLenum pname, GLint param);
void
(*GenerateMipmap)(GLenum target);
};
void
sc_opengl_init(struct sc_opengl *gl);
bool
sc_opengl_version_at_least(struct sc_opengl *gl,
int minver_major, int minver_minor,
int minver_es_major, int minver_es_minor);
#endif

View File

@ -1,12 +1,12 @@
#include "receiver.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include <SDL2/SDL_clipboard.h>
#include "config.h"
#include "device_msg.h"
#include "lock_util.h"
#include "log.h"
#include "util/lock.h"
#include "util/log.h"
bool
receiver_init(struct receiver *receiver, socket_t control_socket) {
@ -23,7 +23,7 @@ receiver_destroy(struct receiver *receiver) {
}
static void
process_msg(struct receiver *receiver, struct device_msg *msg) {
process_msg(struct device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD:
LOGI("Device clipboard copied");
@ -33,7 +33,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) {
}
static ssize_t
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
process_msgs(const unsigned char *buf, size_t len) {
size_t head = 0;
for (;;) {
struct device_msg msg;
@ -45,11 +45,11 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
return head;
}
process_msg(receiver, &msg);
process_msg(&msg);
device_msg_destroy(&msg);
head += r;
SDL_assert(head <= len);
assert(head <= len);
if (head == len) {
return head;
}
@ -64,7 +64,7 @@ run_receiver(void *data) {
size_t head = 0;
for (;;) {
SDL_assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE);
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) {
@ -72,7 +72,7 @@ run_receiver(void *data) {
break;
}
ssize_t consumed = process_msgs(receiver, buf, r);
ssize_t consumed = process_msgs(buf, r);
if (consumed == -1) {
// an error occurred
break;

View File

@ -5,7 +5,8 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
#include "config.h"
#include "util/net.h"
// receive events from the device
// managed by the controller

View File

@ -1,12 +1,12 @@
#include "recorder.h"
#include <assert.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include "compat.h"
#include "config.h"
#include "lock_util.h"
#include "log.h"
#include "compat.h"
#include "util/lock.h"
#include "util/log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@ -33,6 +33,11 @@ record_packet_new(const AVPacket *packet) {
if (!rec) {
return NULL;
}
// av_packet_ref() does not initialize all fields in old FFmpeg versions
// See <https://github.com/Genymobile/scrcpy/issues/707>
av_init_packet(&rec->packet);
if (av_packet_ref(&rec->packet, packet)) {
SDL_free(rec);
return NULL;
@ -87,6 +92,7 @@ recorder_init(struct recorder *recorder,
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
recorder->previous = NULL;
return true;
}
@ -110,7 +116,7 @@ recorder_get_format_name(enum recorder_format format) {
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
const char *format_name = recorder_get_format_name(recorder->format);
SDL_assert(format_name);
assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
@ -129,6 +135,9 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
recorder->ctx->oformat = (AVOutputFormat *) format;
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
avformat_free_context(recorder->ctx);
@ -165,9 +174,14 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
void
recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
avio_close(recorder->ctx->pb);
@ -257,6 +271,19 @@ run_recorder(void *data) {
if (recorder->stopped && queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex);
struct record_packet *last = recorder->previous;
if (last) {
// assign an arbitrary duration to the last packet
last->packet.duration = 100000;
bool ok = recorder_write(recorder, &last->packet);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
LOGW("Could not record last packet");
}
record_packet_delete(last);
}
break;
}
@ -265,8 +292,24 @@ run_recorder(void *data) {
mutex_unlock(recorder->mutex);
bool ok = recorder_write(recorder, &rec->packet);
record_packet_delete(rec);
// recorder->previous is only written from this thread, no need to lock
struct record_packet *previous = recorder->previous;
recorder->previous = rec;
if (!previous) {
// we just received the first packet
continue;
}
// config packets have no PTS, we must ignore them
if (rec->packet.pts != AV_NOPTS_VALUE
&& previous->packet.pts != AV_NOPTS_VALUE) {
// we now know the duration of the previous packet
previous->packet.duration = rec->packet.pts - previous->packet.pts;
}
bool ok = recorder_write(recorder, &previous->packet);
record_packet_delete(previous);
if (!ok) {
LOGE("Could not record packet");
@ -314,7 +357,7 @@ recorder_join(struct recorder *recorder) {
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex);
SDL_assert(!recorder->stopped);
assert(!recorder->stopped);
if (recorder->failed) {
// reject any new packet (this will stop the stream)

View File

@ -6,11 +6,13 @@
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "common.h"
#include "queue.h"
#include "util/queue.h"
enum recorder_format {
RECORDER_FORMAT_MP4 = 1,
RECORDER_FORMAT_AUTO,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV,
};
@ -34,6 +36,12 @@ struct recorder {
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
// we can write a packet only once we received the next one so that we can
// set its duration (next_pts - current_pts)
// "previous" is only accessed from the recorder thread, so it does not
// need to be protected by the mutex
struct record_packet *previous;
};
bool

View File

@ -7,6 +7,7 @@
#include <sys/time.h>
#include <SDL2/SDL.h>
#include "config.h"
#include "command.h"
#include "common.h"
#include "compat.h"
@ -17,15 +18,15 @@
#include "file_handler.h"
#include "fps_counter.h"
#include "input_manager.h"
#include "log.h"
#include "lock_util.h"
#include "net.h"
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "stream.h"
#include "tiny_xpm.h"
#include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h"
#include "util/net.h"
static struct server server = SERVER_INITIALIZER;
static struct screen screen = SCREEN_INITIALIZER;
@ -41,11 +42,12 @@ static struct input_manager input_manager = {
.controller = &controller,
.video_buffer = &video_buffer,
.screen = &screen,
.prefer_text = false, // initialized later
};
// init SDL and set appropriate hints
static bool
sdl_init_and_configure(bool display) {
sdl_init_and_configure(bool display, const char *render_driver) {
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
if (SDL_Init(flags)) {
LOGC("Could not initialize SDL: %s", SDL_GetError());
@ -58,9 +60,13 @@ sdl_init_and_configure(bool display) {
return true;
}
// Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
LOGW("Could not enable bilinear filtering");
if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) {
LOGW("Could not set render driver");
}
// Linear filtering
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering");
}
#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
@ -101,10 +107,12 @@ sdl_init_and_configure(bool display) {
// <https://stackoverflow.com/a/40693139/1987178>
static int
event_watcher(void *data, SDL_Event *event) {
(void) data;
if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
// called from another thread, not very safe, but it's a workaround!
screen_render(&screen);
&& event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
// In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround.
screen_handle_window_event(&screen, &event->window);
}
return 0;
}
@ -142,12 +150,7 @@ handle_event(SDL_Event *event, bool control) {
}
break;
case SDL_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
screen_handle_window_event(&screen, &event->window);
break;
case SDL_TEXTINPUT:
if (!control) {
@ -180,6 +183,11 @@ handle_event(SDL_Event *event, bool control) {
input_manager_process_mouse_button(&input_manager, &event->button,
control);
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
input_manager_process_touch(&input_manager, &event->tfinger);
break;
case SDL_DROPFILE: {
if (!control) {
break;
@ -199,6 +207,7 @@ handle_event(SDL_Event *event, bool control) {
static bool
event_loop(bool display, bool control) {
(void) display;
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (display) {
SDL_AddEventWatch(event_watcher, NULL);
@ -211,6 +220,7 @@ event_loop(bool display, bool control) {
case EVENT_RESULT_STOPPED_BY_USER:
return true;
case EVENT_RESULT_STOPPED_BY_EOS:
LOGW("Device disconnected");
return false;
case EVENT_RESULT_CONTINUE:
break;
@ -219,21 +229,6 @@ event_loop(bool display, bool control) {
return false;
}
static process_t
set_show_touches_enabled(const char *serial, bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void
wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
@ -253,6 +248,7 @@ sdl_priority_from_av_level(int level) {
static void
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
(void) avcl;
SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) {
return;
@ -274,23 +270,21 @@ scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
struct server_params params = {
.crop = options->crop,
.local_port = options->port,
.port_range = options->port_range,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.codec_options = options->codec_options,
};
if (!server_start(&server, options->serial, &params)) {
return false;
}
process_t proc_show_touches = PROCESS_NONE;
bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, true);
show_touches_waited = false;
}
bool ret = false;
bool fps_counter_initialized = false;
@ -301,7 +295,7 @@ scrcpy(const struct scrcpy_options *options) {
bool controller_initialized = false;
bool controller_started = false;
if (!sdl_init_and_configure(options->display)) {
if (!sdl_init_and_configure(options->display, options->render_driver)) {
goto end;
}
@ -384,7 +378,11 @@ scrcpy(const struct scrcpy_options *options) {
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, options->window_x,
options->window_y, options->window_width,
options->window_height,
options->window_borderless,
options->rotation, options-> mipmaps)) {
goto end;
}
@ -403,10 +401,7 @@ scrcpy(const struct scrcpy_options *options) {
}
}
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = true;
}
input_manager.prefer_text = options->prefer_text;
ret = event_loop(options->display, options->control);
LOGD("quit...");
@ -462,16 +457,6 @@ end:
fps_counter_destroy(&fps_counter);
}
if (options->show_touches) {
if (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, false);
wait_show_touches(proc_show_touches);
}
server_destroy(&server);
return ret;

View File

@ -3,7 +3,11 @@
#include <stdbool.h>
#include <stdint.h>
#include <recorder.h>
#include "config.h"
#include "common.h"
#include "input_manager.h"
#include "recorder.h"
struct scrcpy_options {
const char *serial;
@ -11,10 +15,20 @@ struct scrcpy_options {
const char *record_filename;
const char *window_title;
const char *push_target;
const char *render_driver;
const char *codec_options;
enum recorder_format record_format;
uint16_t port;
struct port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
int8_t lock_video_orientation;
uint8_t rotation;
int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width;
uint16_t window_height;
uint16_t display_id;
bool show_touches;
bool fullscreen;
bool always_on_top;
@ -22,8 +36,48 @@ struct scrcpy_options {
bool display;
bool turn_screen_off;
bool render_expired_frames;
bool prefer_text;
bool window_borderless;
bool mipmaps;
bool stay_awake;
};
#define SCRCPY_OPTIONS_DEFAULT { \
.serial = NULL, \
.crop = NULL, \
.record_filename = NULL, \
.window_title = NULL, \
.push_target = NULL, \
.render_driver = NULL, \
.codec_options = NULL, \
.record_format = RECORDER_FORMAT_AUTO, \
.port_range = { \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
}, \
.max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.rotation = 0, \
.window_x = WINDOW_POSITION_UNDEFINED, \
.window_y = WINDOW_POSITION_UNDEFINED, \
.window_width = 0, \
.window_height = 0, \
.display_id = 0, \
.show_touches = false, \
.fullscreen = false, \
.always_on_top = false, \
.control = true, \
.display = true, \
.turn_screen_off = false, \
.render_expired_frames = false, \
.prefer_text = false, \
.window_borderless = false, \
.mipmaps = true, \
.stay_awake = false, \
}
bool
scrcpy(const struct scrcpy_options *options);

View File

@ -1,21 +1,36 @@
#include "screen.h"
#include <assert.h>
#include <string.h>
#include <SDL2/SDL.h>
#include "config.h"
#include "common.h"
#include "compat.h"
#include "icon.xpm"
#include "lock_util.h"
#include "log.h"
#include "tiny_xpm.h"
#include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h"
#define DISPLAY_MARGINS 96
static inline struct size
get_rotated_size(struct size size, int rotation) {
struct size rotated_size;
if (rotation & 1) {
rotated_size.width = size.height;
rotated_size.height = size.width;
} else {
rotated_size.width = size.width;
rotated_size.height = size.height;
}
return rotated_size;
}
// get the window size in a struct size
static struct size
get_native_window_size(SDL_Window *window) {
get_window_size(SDL_Window *window) {
int width;
int height;
SDL_GetWindowSize(window, &width, &height);
@ -28,11 +43,20 @@ get_native_window_size(SDL_Window *window) {
// get the windowed window size
static struct size
get_window_size(const struct screen *screen) {
if (screen->fullscreen) {
get_windowed_window_size(const struct screen *screen) {
if (screen->fullscreen || screen->maximized) {
return screen->windowed_window_size;
}
return get_native_window_size(screen->window);
return get_window_size(screen->window);
}
// apply the windowed window size if fullscreen and maximized are disabled
static void
apply_windowed_size(struct screen *screen) {
if (!screen->fullscreen && !screen->maximized) {
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
screen->windowed_window_size.height);
}
}
// set the window size to be applied when fullscreen is disabled
@ -40,12 +64,8 @@ static void
set_window_size(struct screen *screen, struct size new_size) {
// setting the window size during fullscreen is implementation defined,
// so apply the resize only after fullscreen is disabled
if (screen->fullscreen) {
// SDL_SetWindowSize will be called when fullscreen will be disabled
screen->windowed_window_size = new_size;
} else {
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
}
screen->windowed_window_size = new_size;
apply_windowed_size(screen);
}
// get the preferred display bounds (i.e. the screen bounds with some margins)
@ -73,8 +93,8 @@ get_preferred_display_bounds(struct size *bounds) {
// - it keeps the aspect ratio
// - it scales down to make it fit in the display_size
static struct size
get_optimal_size(struct size current_size, struct size frame_size) {
if (frame_size.width == 0 || frame_size.height == 0) {
get_optimal_size(struct size current_size, struct size content_size) {
if (content_size.width == 0 || content_size.height == 0) {
// avoid division by 0
return current_size;
}
@ -93,32 +113,60 @@ get_optimal_size(struct size current_size, struct size frame_size) {
h = MIN(current_size.height, display_size.height);
}
bool keep_width = frame_size.width * h > frame_size.height * w;
if (h == w * content_size.height / content_size.width
|| w == h * content_size.width / content_size.height) {
// The size is already optimal, if we ignore rounding errors due to
// integer window dimensions
return (struct size) {w, h};
}
bool keep_width = content_size.width * h > content_size.height * w;
if (keep_width) {
// remove black borders on top and bottom
h = frame_size.height * w / frame_size.width;
h = content_size.height * w / content_size.width;
} else {
// remove black borders on left and right (or none at all if it already
// fits)
w = frame_size.width * h / frame_size.height;
w = content_size.width * h / content_size.height;
}
// w and h must fit into 16 bits
SDL_assert_release(w < 0x10000 && h < 0x10000);
assert(w < 0x10000 && h < 0x10000);
return (struct size) {w, h};
}
// same as get_optimal_size(), but read the current size from the window
static inline struct size
get_optimal_window_size(const struct screen *screen, struct size frame_size) {
struct size current_size = get_window_size(screen);
return get_optimal_size(current_size, frame_size);
get_optimal_window_size(const struct screen *screen, struct size content_size) {
struct size windowed_size = get_windowed_window_size(screen);
return get_optimal_size(windowed_size, content_size);
}
// initially, there is no current size, so use the frame size as current size
// req_width and req_height, if not 0, are the sizes requested by the user
static inline struct size
get_initial_optimal_size(struct size frame_size) {
return get_optimal_size(frame_size, frame_size);
get_initial_optimal_size(struct size content_size, uint16_t req_width,
uint16_t req_height) {
struct size window_size;
if (!req_width && !req_height) {
window_size = get_optimal_size(content_size, content_size);
} else {
if (req_width) {
window_size.width = req_width;
} else {
// compute from the requested height
window_size.width = (uint32_t) req_height * content_size.width
/ content_size.height;
}
if (req_height) {
window_size.height = req_height;
} else {
// compute from the requested width
window_size.height = (uint32_t) req_width * content_size.height
/ content_size.width;
}
}
return window_size;
}
void
@ -127,18 +175,48 @@ screen_init(struct screen *screen) {
}
static inline SDL_Texture *
create_texture(SDL_Renderer *renderer, struct size frame_size) {
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
create_texture(struct screen *screen) {
SDL_Renderer *renderer = screen->renderer;
struct size size = screen->frame_size;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
if (!texture) {
return NULL;
}
if (screen->mipmaps) {
struct sc_opengl *gl = &screen->gl;
SDL_GL_BindTexture(texture, NULL, NULL);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f);
SDL_GL_UnbindTexture(texture);
}
return texture;
}
bool
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,
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless,
uint8_t rotation, bool mipmaps) {
screen->frame_size = frame_size;
screen->rotation = rotation;
if (rotation) {
LOGI("Initial display rotation set to %u", rotation);
}
struct size content_size = get_rotated_size(frame_size, screen->rotation);
screen->content_size = content_size;
struct size window_size = get_initial_optimal_size(frame_size);
struct size window_size =
get_initial_optimal_size(content_size, window_width, window_height);
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
@ -151,9 +229,15 @@ screen_init_rendering(struct screen *screen, const char *window_title,
"(compile with SDL >= 2.0.5 to enable it)");
#endif
}
if (window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
int x = window_x != WINDOW_POSITION_UNDEFINED
? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = window_y != WINDOW_POSITION_UNDEFINED
? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height,
window_flags);
if (!screen->window) {
@ -169,13 +253,44 @@ screen_init_rendering(struct screen *screen, const char *window_title,
return false;
}
if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width,
frame_size.height)) {
SDL_RendererInfo renderer_info;
int r = SDL_GetRendererInfo(screen->renderer, &renderer_info);
const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width,
content_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
// starts with "opengl"
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (screen->use_opengl) {
struct sc_opengl *gl = &screen->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
screen->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else {
LOGW("Trilinear filtering disabled (not an OpenGL renderer)");
}
SDL_Surface *icon = read_xpm(icon_xpm);
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
@ -186,13 +301,15 @@ screen_init_rendering(struct screen *screen, const char *window_title,
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
frame_size.height);
screen->texture = create_texture(screen->renderer, frame_size);
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
screen->windowed_window_size = window_size;
return true;
}
@ -214,13 +331,51 @@ screen_destroy(struct screen *screen) {
}
}
void
screen_set_rotation(struct screen *screen, unsigned rotation) {
assert(rotation < 4);
if (rotation == screen->rotation) {
return;
}
struct size old_content_size = screen->content_size;
struct size new_content_size =
get_rotated_size(screen->frame_size, rotation);
if (SDL_RenderSetLogicalSize(screen->renderer,
new_content_size.width,
new_content_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
return;
}
struct size windowed_size = get_windowed_window_size(screen);
struct size target_size = {
.width = (uint32_t) windowed_size.width * new_content_size.width
/ old_content_size.width,
.height = (uint32_t) windowed_size.height * new_content_size.height
/ old_content_size.height,
};
target_size = get_optimal_size(target_size, new_content_size);
set_window_size(screen, target_size);
screen->content_size = new_content_size;
screen->rotation = rotation;
LOGI("Display rotation set to %u", rotation);
screen_render(screen);
}
// recreate the texture and resize the window if the frame size has changed
static bool
prepare_for_frame(struct screen *screen, struct size new_frame_size) {
if (screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width,
new_frame_size.height)) {
struct size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
if (SDL_RenderSetLogicalSize(screen->renderer,
new_content_size.width,
new_content_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
return false;
}
@ -228,21 +383,23 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) {
// frame dimension changed, destroy texture
SDL_DestroyTexture(screen->texture);
struct size current_size = get_window_size(screen);
struct size content_size = screen->content_size;
struct size windowed_size = get_windowed_window_size(screen);
struct size target_size = {
(uint32_t) current_size.width * new_frame_size.width
/ screen->frame_size.width,
(uint32_t) current_size.height * new_frame_size.height
/ screen->frame_size.height,
(uint32_t) windowed_size.width * new_content_size.width
/ content_size.width,
(uint32_t) windowed_size.height * new_content_size.height
/ content_size.height,
};
target_size = get_optimal_size(target_size, new_frame_size);
target_size = get_optimal_size(target_size, new_content_size);
set_window_size(screen, target_size);
screen->frame_size = new_frame_size;
screen->content_size = new_content_size;
LOGI("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height);
screen->texture = create_texture(screen->renderer, new_frame_size);
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
return false;
@ -259,6 +416,13 @@ update_texture(struct screen *screen, const AVFrame *frame) {
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (screen->mipmaps) {
assert(screen->use_opengl);
SDL_GL_BindTexture(screen->texture, NULL, NULL);
screen->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(screen->texture);
}
}
bool
@ -280,16 +444,33 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) {
void
screen_render(struct screen *screen) {
SDL_RenderClear(screen->renderer);
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
if (screen->rotation == 0) {
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
} else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - screen->rotation) % 4;
double angle = 90 * cw_rotation;
SDL_Rect *dstrect = NULL;
SDL_Rect rect;
if (screen->rotation & 1) {
struct size size = screen->content_size;
rect.x = (size.width - size.height) / 2;
rect.y = (size.height - size.width) / 2;
rect.w = size.height;
rect.h = size.width;
dstrect = &rect;
}
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
angle, NULL, 0);
}
SDL_RenderPresent(screen->renderer);
}
void
screen_switch_fullscreen(struct screen *screen) {
if (!screen->fullscreen) {
// going to fullscreen, store the current windowed window size
screen->windowed_window_size = get_native_window_size(screen->window);
}
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
@ -297,11 +478,7 @@ screen_switch_fullscreen(struct screen *screen) {
}
screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen) {
// fullscreen disabled, restore expected windowed window size
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
screen->windowed_window_size.height);
}
apply_windowed_size(screen);
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
screen_render(screen);
@ -309,20 +486,107 @@ screen_switch_fullscreen(struct screen *screen) {
void
screen_resize_to_fit(struct screen *screen) {
if (!screen->fullscreen) {
struct size optimal_size = get_optimal_window_size(screen,
screen->frame_size);
SDL_SetWindowSize(screen->window, optimal_size.width,
optimal_size.height);
LOGD("Resized to optimal size");
if (screen->fullscreen) {
return;
}
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
struct size optimal_size =
get_optimal_window_size(screen, screen->content_size);
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
LOGD("Resized to optimal size: %ux%u", optimal_size.width,
optimal_size.height);
}
void
screen_resize_to_pixel_perfect(struct screen *screen) {
if (!screen->fullscreen) {
SDL_SetWindowSize(screen->window, screen->frame_size.width,
screen->frame_size.height);
LOGD("Resized to pixel-perfect");
if (screen->fullscreen) {
return;
}
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
struct size content_size = screen->content_size;
SDL_SetWindowSize(screen->window, content_size.width, content_size.height);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
content_size.height);
}
void
screen_handle_window_event(struct screen *screen,
const SDL_WindowEvent *event) {
switch (event->event) {
case SDL_WINDOWEVENT_EXPOSED:
screen_render(screen);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
if (!screen->fullscreen && !screen->maximized) {
// Backup the previous size: if we receive the MAXIMIZED event,
// then the new size must be ignored (it's the maximized size).
// We could not rely on the window flags due to race conditions
// (they could be updated asynchronously, at least on X11).
screen->windowed_window_size_backup =
screen->windowed_window_size;
// Save the windowed size, so that it is available once the
// window is maximized or fullscreen is enabled.
screen->windowed_window_size = get_window_size(screen->window);
}
screen_render(screen);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
// The backup size must be non-nul.
assert(screen->windowed_window_size_backup.width);
assert(screen->windowed_window_size_backup.height);
// Revert the last size, it was updated while screen was maximized.
screen->windowed_window_size = screen->windowed_window_size_backup;
#ifdef DEBUG
// Reset the backup to invalid values to detect unexpected usage
screen->windowed_window_size_backup.width = 0;
screen->windowed_window_size_backup.height = 0;
#endif
screen->maximized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
screen->maximized = false;
apply_windowed_size(screen);
break;
}
}
struct point
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
unsigned rotation = screen->rotation;
assert(rotation < 4);
int32_t w = screen->content_size.width;
int32_t h = screen->content_size.height;
struct point result;
switch (rotation) {
case 0:
result.x = x;
result.y = y;
break;
case 1:
result.x = h - y;
result.y = x;
break;
case 2:
result.x = w - x;
result.y = h - y;
break;
default:
assert(rotation == 3);
result.x = y;
result.y = w - x;
break;
}
return result;
}

View File

@ -5,7 +5,11 @@
#include <SDL2/SDL.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "common.h"
#include "opengl.h"
#define WINDOW_POSITION_UNDEFINED (-0x8000)
struct video_buffer;
@ -13,29 +17,52 @@ struct screen {
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
bool use_opengl;
struct sc_opengl gl;
struct size frame_size;
//used only in fullscreen mode to know the windowed window size
struct size content_size; // rotated frame_size
// The window size the last time it was not maximized or fullscreen.
struct size windowed_window_size;
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
// able to revert the size to its non-maximized value.
struct size windowed_window_size_backup;
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
unsigned rotation;
bool has_frame;
bool fullscreen;
bool maximized;
bool no_window;
bool mipmaps;
};
#define SCREEN_INITIALIZER { \
.window = NULL, \
.renderer = NULL, \
.texture = NULL, \
.frame_size = { \
.width = 0, \
.height = 0, \
}, \
#define SCREEN_INITIALIZER { \
.window = NULL, \
.renderer = NULL, \
.texture = NULL, \
.use_opengl = false, \
.gl = {0}, \
.frame_size = { \
.width = 0, \
.height = 0, \
}, \
.content_size = { \
.width = 0, \
.height = 0, \
}, \
.windowed_window_size = { \
.width = 0, \
.height = 0, \
}, \
.has_frame = false, \
.fullscreen = false, \
.no_window = false, \
.width = 0, \
.height = 0, \
}, \
.windowed_window_size_backup = { \
.width = 0, \
.height = 0, \
}, \
.rotation = 0, \
.has_frame = false, \
.fullscreen = false, \
.maximized = false, \
.no_window = false, \
.mipmaps = false, \
}
// initialize default values
@ -43,9 +70,13 @@ void
screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden)
// window_x and window_y accept WINDOW_POSITION_UNDEFINED
bool
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,
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless,
uint8_t rotation, bool mipmaps);
// show the window
void
@ -75,4 +106,17 @@ screen_resize_to_fit(struct screen *screen);
void
screen_resize_to_pixel_perfect(struct screen *screen);
// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise)
void
screen_set_rotation(struct screen *screen, unsigned rotation);
// react to window events
void
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
// convert point from window coordinates to frame coordinates
// x and y are expressed in pixels
struct point
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y);
#endif

View File

@ -1,38 +1,60 @@
#include "server.h"
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <libgen.h>
#include <stdio.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_timer.h>
#include <SDL2/SDL_platform.h>
#include "config.h"
#include "command.h"
#include "log.h"
#include "net.h"
#include "util/log.h"
#include "util/net.h"
#include "util/str_util.h"
#define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar"
#define SERVER_FILENAME "scrcpy-server"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static const char *
static char *
get_server_path(void) {
#ifdef __WINDOWS__
const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH");
#else
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
#endif
if (server_path_env) {
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env);
// if the envvar is set, use it
return server_path_env;
#ifdef __WINDOWS__
char *server_path = utf8_from_wide_char(server_path_env);
#else
char *server_path = SDL_strdup(server_path_env);
#endif
if (!server_path) {
LOGE("Could not allocate memory");
return NULL;
}
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
return server_path;
}
#ifndef PORTABLE
LOGD("Using server: " DEFAULT_SERVER_PATH);
char *server_path = SDL_strdup(DEFAULT_SERVER_PATH);
if (!server_path) {
LOGE("Could not allocate memory");
return NULL;
}
// the absolute path is hardcoded
return DEFAULT_SERVER_PATH;
return server_path;
#else
// use scrcpy-server.jar in the same directory as the executable
// use scrcpy-server in the same directory as the executable
char *executable_path = get_executable_path();
if (!executable_path) {
LOGE("Could not get executable path, "
@ -67,7 +89,17 @@ get_server_path(void) {
static bool
push_server(const char *serial) {
process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH);
char *server_path = get_server_path();
if (!server_path) {
return false;
}
if (!is_regular_file(server_path)) {
LOGE("'%s' does not exist or is not a regular file\n", server_path);
SDL_free(server_path);
return false;
}
process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH);
SDL_free(server_path);
return process_check_success(process, "adb push");
}
@ -95,17 +127,6 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
return process_check_success(process, "adb forward --remove");
}
static bool
enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return true;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = true;
return enable_tunnel_forward(server->serial, server->local_port);
}
static bool
disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
@ -114,35 +135,157 @@ disable_tunnel(struct server *server) {
return disable_tunnel_reverse(server->serial);
}
static socket_t
listen_on_port(uint16_t port) {
#define IPV4_LOCALHOST 0x7F000001
return net_listen(IPV4_LOCALHOST, port, 1);
}
static bool
enable_tunnel_reverse_any_port(struct server *server,
struct port_range port_range) {
uint16_t port = port_range.first;
for (;;) {
if (!enable_tunnel_reverse(server->serial, port)) {
// the command itself failed, it will fail on any port
return false;
}
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(port);
if (server->server_socket != INVALID_SOCKET) {
// success
server->local_port = port;
return true;
}
// failure, disable tunnel and try another port
if (!disable_tunnel_reverse(server->serial)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
// check before incrementing to avoid overflow on port 65535
if (port < port_range.last) {
LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
port, (uint16_t) (port + 1));
port++;
continue;
}
if (port_range.first == port_range.last) {
LOGE("Could not listen on port %" PRIu16, port_range.first);
} else {
LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
port_range.first, port_range.last);
}
return false;
}
}
static bool
enable_tunnel_forward_any_port(struct server *server,
struct port_range port_range) {
server->tunnel_forward = true;
uint16_t port = port_range.first;
for (;;) {
if (enable_tunnel_forward(server->serial, port)) {
// success
server->local_port = port;
return true;
}
if (port < port_range.last) {
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
port, port + 1);
port++;
continue;
}
if (port_range.first == port_range.last) {
LOGE("Could not forward port %" PRIu16, port_range.first);
} else {
LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
port_range.first, port_range.last);
}
return false;
}
}
static bool
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
if (enable_tunnel_reverse_any_port(server, port_range)) {
return true;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
LOGW("'adb reverse' failed, fallback to 'adb forward'");
return enable_tunnel_forward_any_port(server, port_range);
}
static process_t
execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
char lock_video_orientation_string[3];
char display_id_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu16, params->display_id);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/" SERVER_FILENAME,
"CLASSPATH=" DEVICE_SERVER_PATH,
"app_process",
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
# ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
# else
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
SERVER_DEBUGGER_PORT,
#endif
"/", // unused
"com.genymobile.scrcpy.Server",
SCRCPY_VERSION,
max_size_string,
bit_rate_string,
max_fps_string,
lock_video_orientation_string,
server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
display_id_string,
params->show_touches ? "true" : "false",
params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-",
};
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
// From the computer, run
// adb forward tcp:5005 tcp:5005
// Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with:
// Host: localhost
// Port: 5005
// Then click on "Debug"
#endif
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
#define IPV4_LOCALHOST 0x7F000001
static socket_t
listen_on_port(uint16_t port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t
connect_and_read_byte(uint16_t port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
@ -178,14 +321,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
}
static void
close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
close_socket(socket_t socket) {
assert(socket != INVALID_SOCKET);
net_shutdown(socket, SHUT_RDWR);
if (!net_close(socket)) {
LOGW("Could not close socket");
return;
}
*socket = INVALID_SOCKET;
}
void
@ -193,10 +334,26 @@ server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER;
}
static int
run_wait_server(void *data) {
struct server *server = data;
cmd_simple_wait(server->process, NULL); // ignore exit code
// no need for synchronization, server_socket is initialized before this
// thread was created
if (server->server_socket != INVALID_SOCKET
&& !atomic_flag_test_and_set(&server->server_socket_closed)) {
// On Linux, accept() is unblocked by shutdown(), but on Windows, it is
// unblocked by closesocket(). Therefore, call both (close_socket()).
close_socket(server->server_socket);
}
LOGD("Server terminated");
return 0;
}
bool
server_start(struct server *server, const char *serial,
const struct server_params *params) {
server->local_port = params->local_port;
server->port_range = params->port_range;
if (serial) {
server->serial = SDL_strdup(serial);
@ -206,49 +363,50 @@ server_start(struct server *server, const char *serial,
}
if (!push_server(serial)) {
SDL_free(server->serial);
return false;
goto error1;
}
if (!enable_tunnel(server)) {
SDL_free(server->serial);
return false;
}
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// "adb forward", so the app socket is the client
if (!server->tunnel_forward) {
// At the application level, the device part is "the server" because it
// serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no
// need to try to connect until the server socket is listening on the
// device.
server->server_socket = listen_on_port(params->local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, params->local_port);
disable_tunnel(server);
SDL_free(server->serial);
return false;
}
if (!enable_tunnel_any_port(server, params->port_range)) {
goto error1;
}
// server will connect to our server socket
server->process = execute_server(server, params);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
SDL_free(server->serial);
return false;
goto error2;
}
// If the server process dies before connecting to the server socket, then
// the client will be stuck forever on accept(). To avoid the problem, we
// must be able to wake up the accept() call when the server dies. To keep
// things simple and multiplatform, just spawn a new thread waiting for the
// server process and calling shutdown()/close() on the server socket if
// necessary to wake up any accept() blocking call.
server->wait_server_thread =
SDL_CreateThread(run_wait_server, "wait-server", server);
if (!server->wait_server_thread) {
cmd_terminate(server->process);
cmd_simple_wait(server->process, NULL); // ignore exit code
goto error2;
}
server->tunnel_enabled = true;
return true;
error2:
if (!server->tunnel_forward) {
bool was_closed =
atomic_flag_test_and_set(&server->server_socket_closed);
// the thread is not started, the flag could not be already set
assert(!was_closed);
(void) was_closed;
close_socket(server->server_socket);
}
disable_tunnel(server);
error1:
SDL_free(server->serial);
return false;
}
bool
@ -261,12 +419,16 @@ server_connect_to(struct server *server) {
server->control_socket = net_accept(server->server_socket);
if (server->control_socket == INVALID_SOCKET) {
// the video_socket will be clean up on destroy
// the video_socket will be cleaned up on destroy
return false;
}
// we don't need the server socket anymore
close_socket(&server->server_socket);
if (!atomic_flag_test_and_set(&server->server_socket_closed)) {
// close it from here
close_socket(server->server_socket);
// otherwise, it is closed by run_wait_server()
}
} else {
uint32_t attempts = 100;
uint32_t delay = 100; // ms
@ -293,29 +455,27 @@ server_connect_to(struct server *server) {
void
server_stop(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
if (server->server_socket != INVALID_SOCKET
&& !atomic_flag_test_and_set(&server->server_socket_closed)) {
close_socket(server->server_socket);
}
if (server->video_socket != INVALID_SOCKET) {
close_socket(&server->video_socket);
close_socket(server->video_socket);
}
if (server->control_socket != INVALID_SOCKET) {
close_socket(&server->control_socket);
close_socket(server->control_socket);
}
SDL_assert(server->process != PROCESS_NONE);
assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
LOGW("Could not terminate server");
}
cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated");
cmd_terminate(server->process);
if (server->tunnel_enabled) {
// ignore failure
disable_tunnel(server);
}
SDL_WaitThread(server->wait_server_thread, NULL);
}
void

View File

@ -1,40 +1,59 @@
#ifndef SERVER_H
#define SERVER_H
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "command.h"
#include "net.h"
#include "common.h"
#include "util/net.h"
struct server {
char *serial;
process_t process;
SDL_Thread *wait_server_thread;
atomic_flag server_socket_closed;
socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket;
socket_t control_socket;
uint16_t local_port;
struct port_range port_range;
uint16_t local_port; // selected from port_range
bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
};
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.wait_server_thread = NULL, \
.server_socket_closed = ATOMIC_FLAG_INIT, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
.port_range = { \
.first = 0, \
.last = 0, \
}, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
}
struct server_params {
const char *crop;
uint16_t local_port;
const char *codec_options;
struct port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
uint16_t display_id;
bool show_touches;
bool stay_awake;
};
// init default values

View File

@ -1,21 +1,20 @@
#include "stream.h"
#include <assert.h>
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "compat.h"
#include "config.h"
#include "buffer_util.h"
#include "compat.h"
#include "decoder.h"
#include "events.h"
#include "lock_util.h"
#include "log.h"
#include "recorder.h"
#include "util/buffer_util.h"
#include "util/log.h"
#define BUFSIZE 0x10000
@ -44,7 +43,8 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]);
SDL_assert(len);
assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0);
assert(len);
if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet");
@ -52,12 +52,12 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
}
r = net_recv_all(stream->socket, packet->data, len);
if (r < len) {
if (r < 0 || ((uint32_t) r) < len) {
av_packet_unref(packet);
return false;
}
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE;
return true;
}
@ -107,8 +107,9 @@ stream_parse(struct stream *stream, AVPacket *packet) {
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
SDL_assert(r == in_len);
SDL_assert(out_len == in_len);
assert(r == in_len);
(void) r;
assert(out_len == in_len);
if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;

View File

@ -7,7 +7,8 @@
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "net.h"
#include "config.h"
#include "util/net.h"
struct video_buffer;

View File

@ -7,18 +7,60 @@
#include "command.h"
#include "config.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include "log.h"
#include "util/log.h"
bool
cmd_search(const char *file) {
char *path = getenv("PATH");
if (!path)
return false;
path = strdup(path);
if (!path)
return false;
bool ret = false;
size_t file_len = strlen(file);
char *saveptr;
for (char *dir = strtok_r(path, ":", &saveptr); dir;
dir = strtok_r(NULL, ":", &saveptr)) {
size_t dir_len = strlen(dir);
char *fullpath = malloc(dir_len + file_len + 2);
if (!fullpath)
continue;
memcpy(fullpath, dir, dir_len);
fullpath[dir_len] = '/';
memcpy(fullpath + dir_len + 1, file, file_len + 1);
struct stat sb;
bool fullpath_executable = stat(fullpath, &sb) == 0 &&
sb.st_mode & S_IXUSR;
free(fullpath);
if (fullpath_executable) {
ret = true;
break;
}
}
free(path);
return ret;
}
enum process_result
cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
cmd_execute(const char *const argv[], pid_t *pid) {
int fd[2];
if (pipe(fd) == -1) {
@ -49,7 +91,7 @@ cmd_execute(const char *path, const char *const argv[], pid_t *pid) {
// child close read side
close(fd[0]);
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
execvp(path, (char *const *)argv);
execvp(argv[0], (char *const *)argv);
if (errno == ENOENT) {
ret = PROCESS_ERROR_MISSING_BINARY;
} else {
@ -124,3 +166,14 @@ get_executable_path(void) {
return NULL;
#endif
}
bool
is_regular_file(const char *path) {
struct stat path_stat;
if (stat(path, &path_stat)) {
perror("stat");
return false;
}
return S_ISREG(path_stat.st_mode);
}

View File

@ -1,19 +0,0 @@
#include "net.h"
#include <unistd.h>
bool
net_init(void) {
// do nothing
return true;
}
void
net_cleanup(void) {
// do nothing
}
bool
net_close(socket_t socket) {
return !close(socket);
}

View File

@ -1,8 +1,10 @@
#include "command.h"
#include <sys/stat.h>
#include "config.h"
#include "log.h"
#include "str_util.h"
#include "util/log.h"
#include "util/str_util.h"
static int
build_cmd(char *cmd, size_t len, const char *const argv[]) {
@ -19,7 +21,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
}
enum process_result
cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
cmd_execute(const char *const argv[], HANDLE *handle) {
STARTUPINFOW si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
@ -90,3 +92,22 @@ get_executable_path(void) {
buf[len] = '\0';
return utf8_from_wide_char(buf);
}
bool
is_regular_file(const char *path) {
wchar_t *wide_path = utf8_to_wide_char(path);
if (!wide_path) {
LOGC("Could not allocate wide char string");
return false;
}
struct _stat path_stat;
int r = _wstat(wide_path, &path_stat);
SDL_free(wide_path);
if (r) {
perror("stat");
return false;
}
return S_ISREG(path_stat.st_mode);
}

View File

@ -1,24 +0,0 @@
#include "net.h"
#include "log.h"
bool
net_init(void) {
WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) {
LOGC("WSAStartup failed with error %d", res);
return false;
}
return true;
}
void
net_cleanup(void) {
WSACleanup();
}
bool
net_close(socket_t socket) {
return !closesocket(socket);
}

View File

@ -1,11 +1,13 @@
#include "tiny_xpm.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "log.h"
#include "config.h"
#include "util/log.h"
struct index {
char c;
@ -35,7 +37,7 @@ find_color(struct index *index, int len, char c, uint32_t *color) {
// (non-const) "char *"
SDL_Surface *
read_xpm(char *xpm[]) {
#if SDL_ASSERT_LEVEL >= 2
#ifndef NDEBUG
// patch the XPM to change the icon color in debug mode
xpm[2] = ". c #CC00CC";
#endif
@ -50,24 +52,26 @@ read_xpm(char *xpm[]) {
int chars = strtol(endptr + 1, &endptr, 10);
// sanity checks
SDL_assert(0 <= width && width < 256);
SDL_assert(0 <= height && height < 256);
SDL_assert(0 <= colors && colors < 256);
SDL_assert(chars == 1); // this implementation does not support more
assert(0 <= width && width < 256);
assert(0 <= height && height < 256);
assert(0 <= colors && colors < 256);
assert(chars == 1); // this implementation does not support more
(void) chars;
// init index
struct index index[colors];
for (int i = 0; i < colors; ++i) {
const char *line = xpm[1+i];
index[i].c = line[0];
SDL_assert(line[1] == '\t');
SDL_assert(line[2] == 'c');
SDL_assert(line[3] == ' ');
assert(line[1] == '\t');
assert(line[2] == 'c');
assert(line[3] == ' ');
if (line[4] == '#') {
index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10);
SDL_assert(*endptr == '\0');
assert(*endptr == '\0');
} else {
SDL_assert(!strcmp("None", &line[4]));
assert(!strcmp("None", &line[4]));
index[i].color = 0;
}
}
@ -84,7 +88,8 @@ read_xpm(char *xpm[]) {
char c = line[x];
uint32_t color;
bool color_found = find_color(index, colors, c, &color);
SDL_assert(color_found);
assert(color_found);
(void) color_found;
pixels[y * width + x] = color;
}
}

View File

@ -3,6 +3,8 @@
#include <SDL2/SDL.h>
#include "config.h"
SDL_Surface *
read_xpm(char *xpm[]);

View File

@ -4,6 +4,8 @@
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
static inline void
buffer_write16be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
@ -18,6 +20,12 @@ buffer_write32be(uint8_t *buf, uint32_t value) {
buf[3] = value;
}
static inline void
buffer_write64be(uint8_t *buf, uint64_t value) {
buffer_write32be(buf, value >> 32);
buffer_write32be(&buf[4], (uint32_t) value);
}
static inline uint16_t
buffer_read16be(const uint8_t *buf) {
return (buf[0] << 8) | buf[1];
@ -28,8 +36,8 @@ buffer_read32be(const uint8_t *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
}
static inline
uint64_t buffer_read64be(const uint8_t *buf) {
static inline uint64_t
buffer_read64be(const uint8_t *buf) {
uint32_t msb = buffer_read32be(buf);
uint32_t lsb = buffer_read32be(&buf[4]);
return ((uint64_t) msb << 32) | lsb;

View File

@ -5,6 +5,8 @@
#include <stdbool.h>
#include <unistd.h>
#include "config.h"
// To define a circular buffer type of 20 ints:
// struct cbuf_int CBUF(int, 20);
//

74
app/src/util/lock.h Normal file
View File

@ -0,0 +1,74 @@
#ifndef LOCK_H
#define LOCK_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "log.h"
static inline void
mutex_lock(SDL_mutex *mutex) {
int r = SDL_LockMutex(mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not lock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline void
mutex_unlock(SDL_mutex *mutex) {
int r = SDL_UnlockMutex(mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not unlock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
int r = SDL_CondWait(cond, mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not wait on condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline int
cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) {
int r = SDL_CondWaitTimeout(cond, mutex, ms);
#ifndef NDEBUG
if (r < 0) {
LOGC("Could not wait on condition with timeout: %s", SDL_GetError());
abort();
}
#endif
return r;
}
static inline void
cond_signal(SDL_cond *cond) {
int r = SDL_CondSignal(cond);
#ifndef NDEBUG
if (r) {
LOGC("Could not signal a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
#endif

View File

@ -1,7 +1,9 @@
#include "net.h"
#include <stdio.h>
#include <SDL2/SDL_platform.h>
#include "config.h"
#include "log.h"
#ifdef __WINDOWS__
@ -114,3 +116,32 @@ bool
net_shutdown(socket_t socket, int how) {
return !shutdown(socket, how);
}
bool
net_init(void) {
#ifdef __WINDOWS__
WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) {
LOGC("WSAStartup failed with error %d", res);
return false;
}
#endif
return true;
}
void
net_cleanup(void) {
#ifdef __WINDOWS__
WSACleanup();
#endif
}
bool
net_close(socket_t socket) {
#ifdef __WINDOWS__
return !closesocket(socket);
#else
return !close(socket);
#endif
}

View File

@ -17,6 +17,8 @@
typedef int socket_t;
#endif
#include "config.h"
bool
net_init(void);

View File

@ -2,8 +2,11 @@
#ifndef QUEUE_H
#define QUEUE_H
#include <assert.h>
#include <stdbool.h>
#include <SDL2/SDL_assert.h>
#include <stddef.h>
#include "config.h"
// To define a queue type of "struct foo":
// struct queue_foo QUEUE(struct foo);
@ -13,7 +16,7 @@
}
#define queue_init(PQ) \
(void) ((PQ)->first = NULL)
(void) ((PQ)->first = (PQ)->last = NULL)
#define queue_is_empty(PQ) \
!(PQ)->first
@ -64,7 +67,7 @@
// type so that we can "return" it)
#define queue_take(PQ, NEXTFIELD, PITEM) \
(void) ({ \
SDL_assert(!queue_is_empty(PQ)); \
assert(!queue_is_empty(PQ)); \
*(PITEM) = (PQ)->first; \
(PQ)->first = (PQ)->first->NEXTFIELD; \
})

View File

@ -1,5 +1,7 @@
#include "str_util.h"
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
@ -10,6 +12,8 @@
#include <SDL2/SDL_stdinc.h>
#include "config.h"
size_t
xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
@ -58,6 +62,88 @@ strquote(const char *src) {
return quoted;
}
bool
parse_integer(const char *s, long *out) {
char *endptr;
if (*s == '\0') {
return false;
}
errno = 0;
long value = strtol(s, &endptr, 0);
if (errno == ERANGE) {
return false;
}
if (*endptr != '\0') {
return false;
}
*out = value;
return true;
}
size_t
parse_integers(const char *s, const char sep, size_t max_items, long *out) {
size_t count = 0;
char *endptr;
do {
errno = 0;
long value = strtol(s, &endptr, 0);
if (errno == ERANGE) {
return 0;
}
if (endptr == s || (*endptr != sep && *endptr != '\0')) {
return 0;
}
out[count++] = value;
if (*endptr == sep) {
if (count >= max_items) {
// max items already reached, could not accept a new item
return 0;
}
// parse the next token during the next iteration
s = endptr + 1;
}
} while (*endptr != '\0');
return count;
}
bool
parse_integer_with_suffix(const char *s, long *out) {
char *endptr;
if (*s == '\0') {
return false;
}
errno = 0;
long value = strtol(s, &endptr, 0);
if (errno == ERANGE) {
return false;
}
int mul = 1;
if (*endptr != '\0') {
if (s == endptr) {
return false;
}
if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') {
mul = 1000000;
} else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') {
mul = 1000;
} else {
return false;
}
}
if ((value < 0 && LONG_MIN / mul > value) ||
(value > 0 && LONG_MAX / mul < value)) {
return false;
}
*out = value * mul;
return true;
}
size_t
utf8_truncation_index(const char *utf8, size_t max_len) {
size_t len = strlen(utf8);

View File

@ -1,8 +1,11 @@
#ifndef STRUTIL_H
#define STRUTIL_H
#include <stdbool.h>
#include <stddef.h>
#include "config.h"
// like strncpy, except:
// - it copies at most n-1 chars
// - the dest string is nul-terminated
@ -23,6 +26,23 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
char *
strquote(const char *src);
// parse s as an integer into value
// returns true if the conversion succeeded, false otherwise
bool
parse_integer(const char *s, long *out);
// parse s as integers separated by sep (for example '1234:2000')
// returns the number of integers on success, 0 on failure
size_t
parse_integers(const char *s, const char sep, size_t max_items, long *out);
// parse s as an integer into value
// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as
// suffix
// returns true if the conversion succeeded, false otherwise
bool
parse_integer_with_suffix(const char *s, long *out);
// 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);

View File

@ -1,13 +1,13 @@
#include "video_buffer.h"
#include <SDL2/SDL_assert.h>
#include <assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "lock_util.h"
#include "log.h"
#include "util/lock.h"
#include "util/log.h"
bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
@ -91,7 +91,7 @@ video_buffer_offer_decoded_frame(struct video_buffer *vb,
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) {
SDL_assert(!vb->rendering_frame_consumed);
assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true;
fps_counter_add_rendered_frame(vb->fps_counter);
if (vb->render_expired_frames) {

View File

@ -4,6 +4,7 @@
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "fps_counter.h"
// forward declarations

View File

@ -0,0 +1,76 @@
#include <assert.h>
#include "util/buffer_util.h"
static void test_buffer_write16be(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
buffer_write16be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
}
static void test_buffer_write32be(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
buffer_write32be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
}
static void test_buffer_write64be(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
buffer_write64be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
assert(buf[4] == 0x56);
assert(buf[5] == 0x78);
assert(buf[6] == 0x90);
assert(buf[7] == 0xEF);
}
static void test_buffer_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
uint16_t val = buffer_read16be(buf);
assert(val == 0xABCD);
}
static void test_buffer_read32be(void) {
uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34};
uint32_t val = buffer_read32be(buf);
assert(val == 0xABCD1234);
}
static void test_buffer_read64be(void) {
uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34,
0x56, 0x78, 0x90, 0xEF};
uint64_t val = buffer_read64be(buf);
assert(val == 0xABCD1234567890EF);
}
int main(void) {
test_buffer_write16be();
test_buffer_write32be();
test_buffer_write64be();
test_buffer_read16be();
test_buffer_read32be();
test_buffer_read64be();
return 0;
}

View File

@ -1,7 +1,7 @@
#include <assert.h>
#include <string.h>
#include "cbuf.h"
#include "util/cbuf.h"
struct int_queue CBUF(int, 32);

131
app/tests/test_cli.c Normal file
View File

@ -0,0 +1,131 @@
#include <assert.h>
#include "cli.h"
#include "common.h"
static void test_flag_version(void) {
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = false,
};
char *argv[] = {"scrcpy", "-v"};
bool ok = scrcpy_parse_args(&args, 2, argv);
assert(ok);
assert(!args.help);
assert(args.version);
}
static void test_flag_help(void) {
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = false,
};
char *argv[] = {"scrcpy", "-v"};
bool ok = scrcpy_parse_args(&args, 2, argv);
assert(ok);
assert(!args.help);
assert(args.version);
}
static void test_options(void) {
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = false,
};
char *argv[] = {
"scrcpy",
"--always-on-top",
"--bit-rate", "5M",
"--crop", "100:200:300:400",
"--fullscreen",
"--max-fps", "30",
"--max-size", "1024",
"--lock-video-orientation", "2",
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-display" is not compatible with "--fulscreen"
"--port", "1234:1236",
"--push-target", "/sdcard/Movies",
"--record", "file",
"--record-format", "mkv",
"--render-expired-frames",
"--serial", "0123456789abcdef",
"--show-touches",
"--turn-screen-off",
"--prefer-text",
"--window-title", "my device",
"--window-x", "100",
"--window-y", "-1",
"--window-width", "600",
"--window-height", "0",
"--window-borderless",
};
bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv);
assert(ok);
const struct scrcpy_options *opts = &args.opts;
assert(opts->always_on_top);
fprintf(stderr, "%d\n", (int) opts->bit_rate);
assert(opts->bit_rate == 5000000);
assert(!strcmp(opts->crop, "100:200:300:400"));
assert(opts->fullscreen);
assert(opts->max_fps == 30);
assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file"));
assert(opts->record_format == RECORDER_FORMAT_MKV);
assert(opts->render_expired_frames);
assert(!strcmp(opts->serial, "0123456789abcdef"));
assert(opts->show_touches);
assert(opts->turn_screen_off);
assert(opts->prefer_text);
assert(!strcmp(opts->window_title, "my device"));
assert(opts->window_x == 100);
assert(opts->window_y == -1);
assert(opts->window_width == 600);
assert(opts->window_height == 0);
assert(opts->window_borderless);
}
static void test_options2(void) {
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = false,
};
char *argv[] = {
"scrcpy",
"--no-control",
"--no-display",
"--record", "file.mp4", // cannot enable --no-display without recording
};
bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv);
assert(ok);
const struct scrcpy_options *opts = &args.opts;
assert(!opts->control);
assert(!opts->display);
assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == RECORDER_FORMAT_MP4);
}
int main(void) {
test_flag_version();
test_flag_help();
test_options();
test_options2();
return 0;
};

View File

@ -49,53 +49,57 @@ static void test_serialize_inject_text(void) {
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];
char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1];
memset(text, 'a', sizeof(text));
text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0';
text[CONTROL_MSG_INJECT_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);
assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH];
unsigned char expected[3 + CONTROL_MSG_INJECT_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);
memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_inject_mouse_event(void) {
static void test_serialize_inject_touch_event(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
.inject_mouse_event = {
.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = {
.action = AMOTION_EVENT_ACTION_DOWN,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
.pointer_id = 0x1234567887654321L,
.position = {
.point = {
.x = 260,
.y = 1026,
.x = 100,
.y = 200,
},
.screen_size = {
.width = 1080,
.height = 1920,
},
},
.pressure = 1.0f,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
},
};
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf);
assert(size == 18);
assert(size == 28);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_MOUSE_EVENT,
CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
0x00, // AKEY_EVENT_ACTION_DOWN
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
0x04, 0x38, 0x07, 0x80, // 1080 1920
0xff, 0xff, // pressure
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
@ -232,11 +236,26 @@ static void test_serialize_set_screen_power_mode(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_rotate_device(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_ROTATE_DEVICE,
};
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_ROTATE_DEVICE,
};
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_touch_event();
test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel();
@ -244,5 +263,6 @@ int main(void) {
test_serialize_get_clipboard();
test_serialize_set_clipboard();
test_serialize_set_screen_power_mode();
test_serialize_rotate_device();
return 0;
}

View File

@ -1,6 +1,6 @@
#include <assert.h>
#include <queue.h>
#include "util/queue.h"
struct foo {
int value;

View File

@ -1,7 +1,10 @@
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <SDL2/SDL.h>
#include "str_util.h"
#include "util/str_util.h"
static void test_xstrncpy_simple(void) {
char s[] = "xxxxxxxxxx";
@ -126,6 +129,16 @@ static void test_xstrjoin_truncated_after_sep(void) {
assert(!strcmp("abc de ", s));
}
static void test_strquote(void) {
const char *s = "abcde";
char *out = strquote(s);
// add '"' at the beginning and the end
assert(!strcmp("\"abcde\"", out));
SDL_free(out);
}
static void test_utf8_truncate(void) {
const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
@ -157,6 +170,122 @@ static void test_utf8_truncate(void) {
assert(count == 7); // no more chars
}
static void test_parse_integer(void) {
long value;
bool ok = parse_integer("1234", &value);
assert(ok);
assert(value == 1234);
ok = parse_integer("-1234", &value);
assert(ok);
assert(value == -1234);
ok = parse_integer("1234k", &value);
assert(!ok);
ok = parse_integer("123456789876543212345678987654321", &value);
assert(!ok); // out-of-range
}
static void test_parse_integers(void) {
long values[5];
size_t count = parse_integers("1234", ':', 5, values);
assert(count == 1);
assert(values[0] == 1234);
count = parse_integers("1234:5678", ':', 5, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == 5678);
count = parse_integers("1234:5678", ':', 2, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == 5678);
count = parse_integers("1234:-5678", ':', 2, values);
assert(count == 2);
assert(values[0] == 1234);
assert(values[1] == -5678);
count = parse_integers("1:2:3:4:5", ':', 5, values);
assert(count == 5);
assert(values[0] == 1);
assert(values[1] == 2);
assert(values[2] == 3);
assert(values[3] == 4);
assert(values[4] == 5);
count = parse_integers("1234:5678", ':', 1, values);
assert(count == 0); // max_items == 1
count = parse_integers("1:2:3:4:5", ':', 3, values);
assert(count == 0); // max_items == 3
count = parse_integers(":1234", ':', 5, values);
assert(count == 0); // invalid
count = parse_integers("1234:", ':', 5, values);
assert(count == 0); // invalid
count = parse_integers("1234:", ':', 1, values);
assert(count == 0); // invalid, even when max_items == 1
count = parse_integers("1234::5678", ':', 5, values);
assert(count == 0); // invalid
}
static void test_parse_integer_with_suffix(void) {
long value;
bool ok = parse_integer_with_suffix("1234", &value);
assert(ok);
assert(value == 1234);
ok = parse_integer_with_suffix("-1234", &value);
assert(ok);
assert(value == -1234);
ok = parse_integer_with_suffix("1234k", &value);
assert(ok);
assert(value == 1234000);
ok = parse_integer_with_suffix("1234m", &value);
assert(ok);
assert(value == 1234000000);
ok = parse_integer_with_suffix("-1234k", &value);
assert(ok);
assert(value == -1234000);
ok = parse_integer_with_suffix("-1234m", &value);
assert(ok);
assert(value == -1234000000);
ok = parse_integer_with_suffix("123456789876543212345678987654321", &value);
assert(!ok); // out-of-range
char buf[32];
sprintf(buf, "%ldk", LONG_MAX / 2000);
ok = parse_integer_with_suffix(buf, &value);
assert(ok);
assert(value == LONG_MAX / 2000 * 1000);
sprintf(buf, "%ldm", LONG_MAX / 2000);
ok = parse_integer_with_suffix(buf, &value);
assert(!ok);
sprintf(buf, "%ldk", LONG_MIN / 2000);
ok = parse_integer_with_suffix(buf, &value);
assert(ok);
assert(value == LONG_MIN / 2000 * 1000);
sprintf(buf, "%ldm", LONG_MIN / 2000);
ok = parse_integer_with_suffix(buf, &value);
assert(!ok);
}
int main(void) {
test_xstrncpy_simple();
test_xstrncpy_just_fit();
@ -166,6 +295,10 @@ int main(void) {
test_xstrjoin_truncated_in_token();
test_xstrjoin_truncated_before_sep();
test_xstrjoin_truncated_after_sep();
test_strquote();
test_utf8_truncate();
test_parse_integer();
test_parse_integers();
test_parse_integer_with_suffix();
return 0;
}

View File

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

View File

@ -54,7 +54,7 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="SuppressWarningsHolder"/>
<!-- Checks for imports -->
<!-- See http://checkstyle.sf.net/config_import.html -->
<!-- See http://checkstyle.sf.net/config_imports.html -->
<module name="AvoidStarImport">
<property name="allowStaticMemberImports" value="true" />
</module>
@ -99,7 +99,7 @@ page at http://checkstyle.sourceforge.net/config.html -->
<module name="WhitespaceAround" />
<!-- Modifier Checks -->
<!-- See http://checkstyle.sf.net/config_modifiers.html -->
<!-- See http://checkstyle.sf.net/config_modifier.html -->
<module name="ModifierOrder" />
<module name="RedundantModifier" />
@ -129,11 +129,6 @@ page at http://checkstyle.sourceforge.net/config.html -->
</module>
<module name="IllegalInstantiation" />
<module name="InnerAssignment" />
<module name="MagicNumber">
<property name="severity" value="info" />
<property name="ignoreHashCodeMethod" value="true" />
<property name="ignoreAnnotation" value="true" />
</module>
<module name="MissingSwitchDefault" />
<module name="SimplifyBooleanExpression" />
<module name="SimplifyBooleanReturn" />

View File

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

View File

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

Binary file not shown.

View File

@ -1,6 +1,5 @@
#Thu Apr 18 11:45:59 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

137
gradlew vendored
View File

@ -1,4 +1,20 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
@ -6,42 +22,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@ -60,6 +40,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -85,7 +105,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@ -105,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
@ -134,27 +154,30 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=`save "$@"`
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

33
gradlew.bat vendored
View File

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -8,14 +24,17 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@ -46,10 +65,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@ -60,11 +78,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View File

@ -1,7 +1,10 @@
project('scrcpy', 'c',
version: '1.9',
meson_version: '>= 0.37',
default_options: 'c_std=c11')
version: '1.13',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',
'warning_level=2',
])
if get_option('compile_app')
subdir('app')

View File

@ -3,5 +3,7 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')

View File

@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \
8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \
ffmpeg-4.1.3-win32-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \
ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \
ffmpeg-4.2.2-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \
e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \
ffmpeg-4.1.3-win32-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \
8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \
ffmpeg-4.2.2-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \
0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \
ffmpeg-4.1.3-win64-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \
5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \
ffmpeg-4.2.2-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \
334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \
ffmpeg-4.1.3-win64-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \
f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \
ffmpeg-4.2.2-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \
ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \
SDL2-2.0.8
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \
e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \
SDL2-2.0.12
prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \
2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
platform-tools

View File

@ -23,21 +23,21 @@ cd -
make -f Makefile.CrossWindows
# the generated server must be the same everywhere
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win32/scrcpy-server.jar
cmp "$BUILDDIR/server/scrcpy-server.jar" dist/scrcpy-win64/scrcpy-server.jar
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server
# get version name
TAG=$(git describe --tags --always)
# create release directory
mkdir -p "release-$TAG"
cp "$BUILDDIR/server/scrcpy-server.jar" "release-$TAG/scrcpy-server-$TAG.jar"
cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG"
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
# generate checksums
cd "release-$TAG"
sha256sum "scrcpy-server-$TAG.jar" \
sha256sum "scrcpy-server-$TAG" \
"scrcpy-win32-$TAG.zip" \
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt

2
run
View File

@ -20,4 +20,4 @@ then
exit 1
fi
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server.jar" "$BUILDDIR/app/scrcpy" "$@"
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@"

View File

@ -1,2 +1,2 @@
#!/bin/bash
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server.jar" "$MESON_BUILD_ROOT/app/scrcpy"
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"

View File

@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 29
versionCode 10
versionName "1.9"
versionCode 15
versionName "1.13"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

66
server/build_without_gradle.sh Executable file
View File

@ -0,0 +1,66 @@
#!/bin/bash
#
# This script generates the scrcpy binary "manually" (without gradle).
#
# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and
# ANDROID_BUILD_TOOLS environment variables).
#
# Then execute:
#
# BUILD_DIR=my_build_dir ./build_without_gradle.sh
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.13
PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy;
public final class BuildConfig {
public static final boolean DEBUG = $SCRCPY_DEBUG;
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
}
EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/view/IRotationWatcher.aidl
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \
-cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
echo "Dexing..."
cd "$CLASSES_DIR"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \
android/view/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
echo "Archiving..."
cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View File

@ -3,9 +3,12 @@
prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == ''
custom_target('scrcpy-server',
build_always: true, # gradle is responsible for tracking source changes
output: 'scrcpy-server.jar',
# gradle is responsible for tracking source changes
build_by_default: true,
build_always_stale: true,
output: 'scrcpy-server',
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
console: true,
install: true,
install_dir: 'share/scrcpy')
else
@ -15,7 +18,7 @@ else
endif
custom_target('scrcpy-server-prebuilt',
input: prebuilt_server,
output: 'scrcpy-server.jar',
output: 'scrcpy-server',
command: ['cp', '@INPUT@', '@OUTPUT@'],
install: true,
install_dir: 'share/scrcpy')

View File

@ -0,0 +1,77 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import java.io.File;
import java.io.IOException;
/**
* Handle the cleanup of scrcpy, even if the main process is killed.
* <p>
* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process).
*/
public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
private CleanUp() {
// not instantiable
}
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
}
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
builder.start();
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
public static void main(String... args) {
unlinkSelf();
try {
// Wait for the server to die
System.in.read();
} catch (IOException e) {
// Expected when the server is dead
}
Ln.i("Cleaning up");
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
if (disableShowTouches || restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
}
if (restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
}
}
}
}
}

View File

@ -0,0 +1,112 @@
package com.genymobile.scrcpy;
import java.util.ArrayList;
import java.util.List;
public class CodecOption {
private String key;
private Object value;
public CodecOption(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Object getValue() {
return value;
}
public static List<CodecOption> parse(String codecOptions) {
if ("-".equals(codecOptions)) {
return null;
}
List<CodecOption> result = new ArrayList<>();
boolean escape = false;
StringBuilder buf = new StringBuilder();
for (char c : codecOptions.toCharArray()) {
switch (c) {
case '\\':
if (escape) {
buf.append('\\');
escape = false;
} else {
escape = true;
}
break;
case ',':
if (escape) {
buf.append(',');
escape = false;
} else {
// This comma is a separator between codec options
String codecOption = buf.toString();
result.add(parseOption(codecOption));
// Clear buf
buf.setLength(0);
}
break;
default:
buf.append(c);
break;
}
}
if (buf.length() > 0) {
String codecOption = buf.toString();
result.add(parseOption(codecOption));
}
return result;
}
private static CodecOption parseOption(String option) {
int equalSignIndex = option.indexOf('=');
if (equalSignIndex == -1) {
throw new IllegalArgumentException("'=' expected");
}
String keyAndType = option.substring(0, equalSignIndex);
if (keyAndType.length() == 0) {
throw new IllegalArgumentException("Key may not be null");
}
String key;
String type;
int colonIndex = keyAndType.indexOf(':');
if (colonIndex != -1) {
key = keyAndType.substring(0, colonIndex);
type = keyAndType.substring(colonIndex + 1);
} else {
key = keyAndType;
type = "int"; // assume int by default
}
Object value;
String valueString = option.substring(equalSignIndex + 1);
switch (type) {
case "int":
value = Integer.parseInt(valueString);
break;
case "long":
value = Long.parseLong(valueString);
break;
case "float":
value = Float.parseFloat(valueString);
break;
case "string":
value = valueString;
break;
default:
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
}
return new CodecOption(key, value);
}
}

View File

@ -7,7 +7,7 @@ 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_TOUCH_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;
@ -15,6 +15,7 @@ public final class ControlMessage {
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;
public static final int TYPE_ROTATE_DEVICE = 10;
private int type;
private String text;
@ -22,6 +23,8 @@ public final class ControlMessage {
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private long pointerId;
private float pressure;
private Position position;
private int hScroll;
private int vScroll;
@ -30,60 +33,62 @@ public final class 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;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_KEYCODE;
msg.action = action;
msg.keycode = keycode;
msg.metaState = metaState;
return msg;
}
public static ControlMessage createInjectText(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_INJECT_TEXT;
event.text = text;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TEXT;
msg.text = text;
return msg;
}
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 createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action;
msg.pointerId = pointerId;
msg.pressure = pressure;
msg.position = position;
msg.buttons = buttons;
return msg;
}
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;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_SCROLL_EVENT;
msg.position = position;
msg.hScroll = hScroll;
msg.vScroll = vScroll;
return msg;
}
public static ControlMessage createSetClipboard(String text) {
ControlMessage event = new ControlMessage();
event.type = TYPE_SET_CLIPBOARD;
event.text = text;
return event;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;
msg.text = text;
return msg;
}
/**
* @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;
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_SCREEN_POWER_MODE;
msg.action = mode;
return msg;
}
public static ControlMessage createEmpty(int type) {
ControlMessage event = new ControlMessage();
event.type = type;
return event;
ControlMessage msg = new ControlMessage();
msg.type = type;
return msg;
}
public int getType() {
@ -110,6 +115,14 @@ public final class ControlMessage {
return buttons;
}
public long getPointerId() {
return pointerId;
}
public float getPressure() {
return pressure;
}
public Position getPosition() {
return position;
}

View File

@ -8,13 +8,13 @@ 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;
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
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;
public static final int INJECT_TEXT_MAX_LENGTH = 300;
private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
@ -59,8 +59,8 @@ public class ControlMessageReader {
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
msg = parseInjectMouseEvent();
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
msg = parseInjectTouchEvent();
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
@ -75,6 +75,7 @@ public class ControlMessageReader {
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);
break;
default:
@ -120,14 +121,19 @@ public class ControlMessageReader {
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectMouseEvent() {
if (buffer.remaining() < INJECT_MOUSE_EVENT_PAYLOAD_LENGTH) {
private ControlMessage parseInjectTouchEvent() {
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int buttons = buffer.getInt();
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
return ControlMessage.createInjectMouseEvent(action, buttons, position);
// 16 bits fixed-point
int pressureInt = toUnsigned(buffer.getShort());
// convert it to a float between 0 and 1 (0x1p16f is 2^16 as float)
float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f);
int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
}
private ControlMessage parseInjectScrollEvent() {
@ -164,12 +170,10 @@ public class ControlMessageReader {
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

@ -13,47 +13,40 @@ import java.io.IOException;
public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1;
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
private long lastMouseDown;
private final MotionEvent.PointerProperties[] pointerProperties = {new MotionEvent.PointerProperties()};
private final MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
private long lastTouchDown;
private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
public Controller(Device device, DesktopConnection connection) {
this.device = device;
this.connection = connection;
initPointer();
initPointers();
sender = new DeviceMessageSender(connection);
}
private void initPointer() {
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
private void initPointers() {
for (int i = 0; i < PointersState.MAX_POINTERS; ++i) {
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.orientation = 0;
coords.pressure = 1;
coords.size = 1;
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = 0;
coords.size = 1;
pointerProperties[i] = props;
pointerCoords[i] = coords;
}
}
private void setPointerCoords(Point point) {
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
}
private void setScroll(int hScroll, int vScroll) {
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
}
@SuppressWarnings("checkstyle:MagicNumber")
public void control() throws IOException {
// on start, power on the device
if (!device.isScreenOn()) {
@ -82,19 +75,29 @@ public class Controller {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
}
break;
case ControlMessage.TYPE_INJECT_TEXT:
injectText(msg.getText());
if (device.supportsInputEvents()) {
injectText(msg.getText());
}
break;
case ControlMessage.TYPE_INJECT_MOUSE_EVENT:
injectMouse(msg.getAction(), msg.getButtons(), msg.getPosition());
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
if (device.supportsInputEvents()) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
}
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
pressBackOrTurnScreenOn();
if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn();
}
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
device.expandNotificationPanel();
@ -110,7 +113,12 @@ public class Controller {
device.setClipboardText(msg.getText());
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
device.setScreenPowerMode(msg.getAction());
if (device.supportsInputEvents()) {
device.setScreenPowerMode(msg.getAction());
}
break;
case ControlMessage.TYPE_ROTATE_DEVICE:
device.rotateDevice();
break;
default:
// do nothing
@ -148,19 +156,43 @@ public class Controller {
return successCount;
}
private boolean injectMouse(int action, int buttons, Position position) {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis();
if (action == MotionEvent.ACTION_DOWN) {
lastMouseDown = now;
}
Point point = device.getPhysicalPoint(position);
if (point == null) {
// ignore event
return false;
}
setPointerCoords(point);
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, action, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, 0, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
int pointerIndex = pointersState.getPointerIndex(pointerId);
if (pointerIndex == -1) {
Ln.w("Too many pointers for touch event");
return false;
}
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now;
}
} else {
// secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex
if (action == MotionEvent.ACTION_UP) {
action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
} else if (action == MotionEvent.ACTION_DOWN) {
action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
}
}
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event);
}
@ -171,10 +203,19 @@ public class Controller {
// ignore event
return false;
}
setPointerCoords(point);
setScroll(hScroll, vScroll);
MotionEvent event = MotionEvent.obtain(lastMouseDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, 0,
0, InputDevice.SOURCE_MOUSE, 0);
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_MOUSE, 0);
return injectEvent(event);
}
@ -186,8 +227,7 @@ public class Controller {
}
private boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0)
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
private boolean injectEvent(InputEvent event) {

View File

@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable {
controlSocket.close();
}
@SuppressWarnings("checkstyle:MagicNumber")
private void send(String deviceName, int width, int height) throws IOException {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];

View File

@ -1,7 +1,10 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.graphics.Rect;
import android.os.Build;
@ -24,13 +27,36 @@ public final class Device {
private ScreenInfo screenInfo;
private RotationListener rotationListener;
/**
* Logical display identifier
*/
private final int displayId;
/**
* The surface flinger layer stack associated with this logical display
*/
private final int layerStack;
private final boolean supportsInputEvents;
public Device(Options options) {
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
displayId = options.getDisplayId();
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
int displayInfoFlags = displayInfo.getFlags();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
layerStack = displayInfo.getLayerStack();
registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) throws RemoteException {
synchronized (Device.this) {
screenInfo = screenInfo.withRotation(rotation);
screenInfo = screenInfo.withDeviceRotation(rotation);
// notify
if (rotationListener != null) {
@ -38,89 +64,69 @@ public final class Device {
}
}
}
});
}, displayId);
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}
// main display or any display on Android >= Q
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
}
public synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {
if (rotated) {
// the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop);
}
if (!contentRect.intersect(crop)) {
// intersect() changes contentRect so that it is intersected with crop
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
contentRect = new Rect(); // empty
}
}
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotated);
}
private static String formatCrop(Rect rect) {
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");
}
boolean portrait = h > w;
int major = portrait ? h : w;
int minor = portrait ? w : h;
if (major > maxSize) {
int minorExact = minor * maxSize / major;
// +4 to round the value to the nearest multiple of 8
minor = (minorExact + 4) & ~7;
major = maxSize;
}
w = portrait ? minor : major;
h = portrait ? major : minor;
}
return new Size(w, h);
public int getLayerStack() {
return layerStack;
}
public Point getPhysicalPoint(Position position) {
// it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
Size videoSize = screenInfo.getVideoSize();
Size clientVideoSize = position.getScreenSize();
if (!videoSize.equals(clientVideoSize)) {
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
int reverseVideoRotation = screenInfo.getReverseVideoRotation();
// reverse the video rotation to apply the events
Position devicePosition = position.rotate(reverseVideoRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!unlockedVideoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event
return null;
}
Rect contentRect = screenInfo.getContentRect();
Point point = position.getPoint();
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
return new Point(scaledX, scaledY);
Point point = devicePosition.getPoint();
int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth();
int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight();
return new Point(convertedX, convertedY);
}
public static String getDeviceName() {
return Build.MODEL;
}
public boolean supportsInputEvents() {
return supportsInputEvents;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
}
@ -128,8 +134,8 @@ public final class Device {
return serviceManager.getPowerManager().isScreenOn();
}
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}
public synchronized void setRotationListener(RotationListener rotationListener) {
@ -153,20 +159,49 @@ public final class Device {
}
public void setClipboardText(String text) {
serviceManager.getClipboardManager().setText(text);
Ln.i("Device clipboard set");
boolean ok = serviceManager.getClipboardManager().setText(text);
if (ok) {
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"));
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");
return;
}
boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
if (ok) {
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
}
static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
/**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/
public void rotateDevice() {
WindowManager wm = serviceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen();
int currentRotation = wm.getRotation();
int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0
String newRotationString = newRotation == 0 ? "portrait" : "landscape";
Ln.i("Device rotation requested: " + newRotationString);
wm.freezeRotation(newRotation);
// restore auto-rotate if necessary
if (accelerometerRotation) {
wm.thawRotation();
}
}
public ContentProvider createSettingsProvider() {
return serviceManager.getActivityManager().createSettingsProvider();
}
}

Some files were not shown because too many files have changed in this diff Show More