Compare commits

...

601 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
e2ac996183 Use Cmd instead of Ctrl on macOS when possible
Fixes <https://github.com/Genymobile/scrcpy/issues/642>
2019-08-03 23:13:44 +02:00
5e4ccfd832 Use generic FIFO queue for recording
Replace the specific recording queue by the new generic FIFO queue
implementation.
2019-08-01 23:15:47 +02:00
53b6ee2cf4 Add generic intrusive FIFO queue
We need several FIFO queues (a queue of packets, a queue of messages,
etc.).

Some of them are implemented using cbuf, a generic circular buffer. But
for recording, we need to store the packets in an unbounded queue until
they are written, so the queue was implemented manually.

Create a generic implementation (using macros) to avoid reimplementing
it every time.
2019-08-01 23:14:50 +02:00
26213f1031 Fix cbuf documentation 2019-08-01 22:50:03 +02:00
96b5067cbf Remove unnecessary backslash in cbuf 2019-08-01 22:08:34 +02:00
421e4be399 Remove root directory from Windows zip releases
Put the scrcpy files at the root of the zip archive. This avoids an
unnecessary level of directories when extracting.
2019-07-31 16:35:14 +02:00
6abb4902c6 Log recording failure
If recording fails, log "recording failed" instead of "recording
complete".
2019-07-31 11:04:38 +02:00
d4ed8b6f26 Log scrcpy version and URL on start
Keep --version which also print the version of dependencies.
2019-07-31 01:55:43 +02:00
35d9185f6c Record asynchronously
The record file was written from the stream thread. As a consequence,
any blocking I/O to write the file delayed the decoder.

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

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

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

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

Add an option to customize the target directory.

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

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

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

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

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

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

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

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

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

Several users experienced freezes with SDL 2.0.9.

This reverts commit a5787dccd6.

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

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

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

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

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

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

> Invalid return value 0 for stream protocol

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

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

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

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

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

And android-tools-adb:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

For example, make:

    2 * SERIALIZED_EVENT_MAX_SIZE

expand to:

    2 * (3 + TEXT_MAX_LENGTH)

instead of:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-03-10 23:19:09 +01:00
71fd238b0a Update developer documentation for v1.8 2019-03-07 20:48:43 +01:00
d795144a36 Add note about Ctrl+C on Windows while recording
Ctrl+C kills the app on Windows, so the recorded file is broken.
2019-03-07 20:42:08 +01:00
c287826f8e Update links to v1.8 in README and BUILD 2019-03-07 20:42:02 +01:00
1323e3c43e Bump version to 1.8 2019-03-07 20:21:07 +01:00
50dac2eaee Log "new texture" at INFO level
The "initial texture" is logged at INFO level. For consistency, log "new
texture" at the same level.
2019-03-07 20:18:03 +01:00
b8ff35efe6 Remove empty line 2019-03-07 19:03:13 +01:00
7fad611dfb Merge branch 'dev' 2019-03-07 19:02:40 +01:00
a7b3901c31 Add more consts
Some decoder and recorder functions must not write to AVCodec and
AVPacket.
2019-03-03 12:02:41 +01:00
fc81d0d771 Merge pull request #442 from npes87184/master
server/meson.build: support relative path for prebuilt_server
2019-03-03 11:16:13 +01:00
f7efafd846 Explicitly pass control flag to input manager
Replace the "global" control flag in the input_manager by a function
parameter to make explicit that the behavior depends whether
--no-control has been set.
2019-03-03 11:05:26 +01:00
c456e38264 server/meson.build: support relative path for prebuilt_server
If we don't do this trick, the prebuilt_server will be
../server/[the_user_defined_path]. In general, we will not give an relative path
based on build directory, which leads to wrong prebuilt_server path.

The building error:

ninja: error: '../scrcpy-server-v1.7.jar', needed by
'server/scrcpy-server.jar', missing and no known rule to make it

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-03-03 12:27:56 +08:00
6baed8a06f Do not init SDL video subsystem if no display
The SDL video subsystem is not necessary if we don't display the video.

Move the sdl_init_and_configure() function from screen.c to scrcpy.c,
because it is not only related to the screen display.
2019-03-03 01:41:35 +01:00
8595862005 Use explicit output parameter for skipped frame
The function video_buffer_offer_decoded_frame() returned a bool to
indicate whether the previous frame had been consumed.

This was confusing, because we could expect the returned bool report
whether the action succeeded.

Make the semantic explicit by using an output parameter.

Also revert the flag (report if the frame has been skipped instead of
consumed) to avoid confusion for the first frame (the previous is
neither skipped nor consumed because there is no previous frame).
2019-03-03 00:35:20 +01:00
9ef345fdd0 Make owned serial a pointer-to-non-const
The server owns the serial, so it needs to free it. Therefore, it should
not be a pointer-to-const.
2019-03-03 00:01:16 +01:00
dfed1b250e Replace SDL types by C99 standard types
Scrcpy is a C11 project. Use the C99 standard types instead of the
SDL-specific types:

    SDL_bool -> bool
    SintXX   -> intXX_t
    UintXX   -> uintXX_t
2019-03-02 23:55:23 +01:00
8655ba7197 Add option to mirror in read-only
Add an option to disable device control: -n/--no-control.
2019-03-02 23:10:21 +01:00
163cd36ccc Rename -n/--no-window to -N/--no-display
The description of scrcpy is "Display and control your Android device".
We want an option to disable display, another one to disable control.

For naming consistency, name it --no-display.

Also change the shortname to -N, so that we can use -n for --no-control
later.
2019-03-02 22:46:46 +01:00
db6644f1f9 Add missing no_window initialization
Initialize the field no_window in "struct args"
2019-03-02 22:42:28 +01:00
36191b7eec Avoid unnecessary call if display is disabled
If --no-window is passed, there is no need to register an event watcher.
2019-03-02 21:51:07 +01:00
33ccb1368f Extract event processing out of event_loop()
To avoid too many levels of nested blocks, move the event handling logic
in a separate function.
2019-03-02 21:51:07 +01:00
aeda583a2c Update code style
Limit source code to 80 chars, and declare functions return type and
modifiers on a separate line.

This allows to avoid very long lines, and all function names are
aligned.

(We do this on VLC, and I like it.)
2019-03-02 20:28:46 +01:00
b2fe005498 Replace uint64_t by Uint64 for consistency
The field pts is declared Uint64 in struct frame_meta.
2019-03-02 20:10:34 +01:00
ef118cc633 Describe the --no-window feature in README 2019-03-02 18:45:56 +01:00
89812e4eee Implement the --no-window flag
Disable decoder, screen (display), file_handler and controller when
--no-window is requested.

<https://github.com/Genymobile/scrcpy/pull/418>
2019-03-02 18:45:56 +01:00
421a1141e2 Add a new option: -n/--no-window
This option will allow scrcpy to record the stream without display.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2019-03-02 18:45:56 +01:00
e6e011baaf Add stream layer
The decoder initially read from the socket, decoded the video and sent
the decoded frames to the screen:

              +---------+      +----------+
  socket ---> | decoder | ---> |  screen  |
              +---------+      +----------+

The design was simple, but the decoder had several responsabilities.

Then we added the recording feature, so we added a recorder, which
reused the packets received from the socket managed by the decoder:

                                    +----------+
                               ---> |  screen  |
              +---------+     /     +----------+
  socket ---> | decoder | ----
              +---------+     \     +----------+
                               ---> | recorder |
                                    +----------+

This lack of separation of concerns now have concrete implications: we
could not (properly) disable the decoder/display to only record the
video.

Therefore, split the decoder to extract the stream:

                                    +----------+      +----------+
                               ---> | decoder  | ---> |  screen  |
              +---------+     /     +----------+      +----------+
  socket ---> | stream  | ----
              +---------+     \     +----------+
                               ---> | recorder |
                                    +----------+

This will allow to record the stream without decoding the video.
2019-03-02 18:45:45 +01:00
e7b7b083aa Store the recording request in a local bool
This avoids to test explicitly whether options->record_filename is NULL.
2019-03-02 18:21:20 +01:00
8aeb5c0e3c Fix cleanup order
The order of cleanup was not the reverse as the initialization order. As
a consequence, recorder_destroy() could theoretically be called even if
recorder_init() failed.
2019-03-02 18:13:11 +01:00
bcd4090d51 Fix recording with old decoding/encoding API
The deprecated avcodec_decode_video2() should always the whole packet,
so there is no need to loop (cf doc/examples/demuxing_decoding.c in
FFmpeg).

This hack changed the packet size and data pointer. This broke recording
which used the same packet.
2019-03-02 17:03:15 +01:00
84270e2d18 Rename "stop" to "interrupt"
The purpose of video_buffer_stop() is to interrupt any blocking call, so
rename it to video_buffer_interrupt().
2019-03-02 17:03:15 +01:00
fff87095d9 Rename "frames" to "video_buffer"
It better describes the purpose of the structure.
2019-03-02 15:24:33 +01:00
aacb09a3d6 Remove unused mutex field in decoder 2019-03-02 14:48:34 +01:00
7d10ec2b5a Add shortcut to expand/collapse notification panel
Use Ctrl+n to expand, Ctrl+Shift+n to collapse.

Fixes <https://github.com/Genymobile/scrcpy/issues/392>
2019-02-26 20:35:37 +01:00
1c1fe5ec53 Use "always on top" only for SDL >= 2.0.5
The flag SDL_WINDOW_ALWAYS_ON_TOP is available since SDL 2.0.5.

Do not use it if SDL is older, to fix compilation failure.

Fixes <https://github.com/Genymobile/scrcpy/issues/432>
2019-02-16 15:28:56 +01:00
751600a7f9 Move all compat ifdefs definitions to compat.h
This allows to give a proper name to features requirements.
2019-02-16 15:28:56 +01:00
3fc11ee465 Update links to v1.7 in README and BUILD 2019-02-16 01:07:15 +01:00
b7472a545e Bump version to 1.7 2019-02-16 00:53:19 +01:00
f5f4e6b1c5 Allocate extradata with av_malloc()
The extradata buffer is owned by libav, so it must be allocated with
av_malloc(), not SDL_malloc().

This fixes a crash on Windows during avformat_free_context().
2019-02-16 00:53:12 +01:00
6c40dbd27d Regroup Windows-ifdefs in command.h 2019-02-10 14:33:59 +01:00
477c0a2cab Create process with wide chars on Windows
Windows does not support UTF-8, so pushing a file with non-ASCII
characters failed.

Convert the UTF-8 command line to a wide characters string and call
CreateProcessW().

Fixes <https://github.com/Genymobile/scrcpy/issues/422>
2019-02-10 13:08:28 +01:00
c0b65b14df Merge branch 'master' into dev 2019-02-10 12:23:47 +01:00
b23cacfc1a Add recording logs
Log when recording is started and stopped.
2019-02-10 11:29:34 +01:00
0ed2373952 Support recording to MKV
Implement recording to Matroska files.

The format to use is determined by the option -F/--record-format if set,
or by the file extension (".mp4" or ".mkv").
2019-02-09 15:56:24 +01:00
1aaad6ba35 Rescale packet timestamp to container time base
Some containers force their own time base. For example, matroska
overwrite time_base to (AVRational) {1, 1000}.

Therefore, rescale our packet timestamps to the output stream time base.

Suggested-by: Steve Lhomme <robux4@ycbcr.xyz>
2019-02-09 15:51:18 +01:00
c8f0805b89 Write header file with correct extradata
When recording, the header must be written with extradata set to the
content of the very first packet.

Suggested-by: Steve Lhomme <robux4@ycbcr.xyz>

Fixes <https://github.com/Genymobile/scrcpy/issues/351>
Fixes <https://github.com/Genymobile/scrcpy/issues/416>
2019-02-09 15:50:24 +01:00
ee3cba57a8 Forward FFmpeg logs
FFmpeg logs can be useful to understand the cause of issues.
2019-02-09 12:27:13 +01:00
c11905b860 Add log verbose macro
This was the only log priority missing.
2019-02-09 12:27:13 +01:00
1a5ba59504 Fix memory leak on close
The buffer associated to the AVIOContext must be freed.
2019-02-09 12:27:13 +01:00
aa07dfd2ca Merge pull request #412 from npes87184/dev
app: add always_on_top
2019-01-27 14:09:19 +01:00
eca82e09c3 app: add always_on_top
It is very convenient when I play mobile game and watch video at the
same time.

Tested on Linux mint Cinnamon as well as Windows 10.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-01-27 20:54:24 +08:00
eea478b9dc Add release script
Add a script to generate the whole release properly.

It first builds locally in release mode, then execute tests. Then it
builds archives for Windows. Finally, it puts all release files (Windows
archives, prebuilt server and checksums) in a separate release
directory.
2019-01-26 15:31:14 +01:00
43ad402356 Merge pull request #411 from npes87184/master
tests: fix test_control_event_serialize
2019-01-26 13:22:42 +01:00
4d30fa93ba tests: fix test_control_event_serialize
commit fefb9816a changed the protocol, fix the related testing case.

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-01-26 20:09:32 +08:00
b35733edb6 Fix expected mouse event sizes
Commit fefb9816a9 modified mouse events
serialization. The server-side parsing was updated to correctly read the
position, but the expected size of these events was not updated.

As a result, the server might try to parse incomplete events, leading
to BufferUnderflowException.

Fixes
<https://github.com/Genymobile/scrcpy/issues/350#issuecomment-456298816>.
2019-01-22 09:47:26 +01:00
7764a836f1 Fix incorrect comment
Comment had not been updated along with the code.
2019-01-22 09:38:51 +01:00
0bfaf7b7ff Update links to v1.6 in README and BUILD 2019-01-20 22:05:32 +01:00
446c682374 Bump version to 1.6 2019-01-20 21:35:42 +01:00
3aacb7967d Merge branch 'dev' into master 2019-01-19 21:40:55 +01:00
06a0bbbc71 Update FFmpeg (4.1) for Windows
Include the last version of FFmpeg in Windows releases.
2019-01-19 18:14:05 +01:00
d71e036f3a Do not disable screensaver
Keep screensaver enabled while scrcpy is running.

Fixes <https://github.com/Genymobile/scrcpy/issues/380>.
2019-01-18 23:17:31 +01:00
b343a5dc59 Merge pull request #401 from npes87184/dev
input_manager: don't ignore double click event when clicking inside device
2019-01-18 13:34:41 +01:00
c5ec1a194c input_manager: don't ignore double click event when clicking inside device
Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
2019-01-18 20:21:25 +08:00
4f254d8232 Merge pull request #396 from npes87184/master
prepare-dep: use variable for better readability
2019-01-16 21:16:04 +01:00
1fdde490fd Mirror "secure" content
Some applications, like Silence, prevent the content of a window from
being viewed on non-secure displays:
<https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_SECURE>

We can mirror it by just creating a "secure" display:
<https://developer.android.com/reference/android/view/Display#FLAG_SECURE>
2019-01-16 14:21:18 +01:00
fb73aa101a prepare-dep: use variable for better readability
The arguments are saved to variable when script started. Instead of
using $1, $2 and $3, we can use these variables.

Signed-off-by: yuchenlin <npes87184@gmail.com>
2019-01-15 21:32:46 +08:00
39c5e71605 Make the server unlink itself
To clean up the device, the client executed "adb shell rm" once the
server was guaranteed to be started (after the connection succeeded).

This implied to track whether the installation state, and failed if an
additional tunnel was used in "forward" mode:
<https://github.com/Genymobile/scrcpy/issues/386#issuecomment-453936034>

Instead, make the server unlink itself on start.
2019-01-14 21:12:23 +01:00
fefb9816a9 Handle mouse events outside device screen
Mouse events position were unsigned (so negative values could not be
handled properly).

To avoid issues with negative values, mouse events outside the device
screen were ignored (commit a7fe9ad779).

But as a consequence, drag&drop were "broken" if the "drop" occurred
outside the device screen.

Instead, use signed 32-bits to store the position, and forward events
outside the device screen.

Fixes <https://github.com/Genymobile/scrcpy/issues/357>.
2018-11-27 08:59:22 +01:00
7830859c21 Merge branch 'master' into dev 2018-11-27 08:38:57 +01:00
0e019f8ab8 Add a note to allow simulating input in README 2018-11-25 11:29:27 +01:00
f7d02cad4b Add ninja-build to the packages list to install
The package ninja-build should be installed automatically as a meson
dependency, but some users need to install a newer meson from pip3, so
ninja must be installed explicitly.
2018-11-19 09:43:05 +01:00
a7fe9ad779 Ignore mouse events outside device screen
Never create a "struct point" with a position possibly outside the
device screen (i.e. in the black borders area), and do not transmit such
events.

This fixes an assertion failure on mouse wheel events outside the device
screen area.
2018-11-18 21:31:57 +01:00
1e22ebcac2 Always use non-empty arguments
The client passes parameters to the server via "adb shell" arguments.

Use "-" instead of "" when no crop is specified to avoid empty
arguments, which are not handled the same way on all devices.

Fixed <https://github.com/Genymobile/scrcpy/issues/337>.
2018-11-16 18:39:41 +01:00
d81729ba39 Always expect 5 parameters for the server
The client always sends all the arguments, so there is no need to check.
2018-11-16 18:36:01 +01:00
b2c3df7550 Point out that ninja must not be run as root
See https://github.com/Genymobile/scrcpy/issues/335.
2018-11-15 21:11:58 +01:00
46fec41b7b Move drag&drop features in README
Present how to install an APK and how to push a file in the "features"
section (instead of "shortcuts").
2018-11-13 20:39:18 +01:00
2876463d39 Fix read_packet() return value on error or EOF
Fix warning on error or EOF:

> Invalid return value 0 for stream protocol

See <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=a606f27f4c610708fa96e35eed7b7537d3d8f712>.

Fixes <https://github.com/Genymobile/scrcpy/issues/333>.
2018-11-12 14:45:43 +01:00
6dc6ec05d5 Configure version at meson project level
Make meson aware of the project version, so that it does not print:

    Project version: undefined
2018-11-12 14:10:21 +01:00
b5e630eea3 Update links to v1.5-fixversion
I forgot to bump version _before_ the release, so I had to make a new
one which fixes the version string (for scrcpy --help).
2018-11-12 08:52:45 +01:00
a17f1116ce Bump version to 1.5
Signed-off-by: Romain Vimont <rom@rom1v.com>
2018-11-12 08:24:18 +01:00
e4cf152b26 Update links to v1.5 in README and BUILD 2018-11-11 23:35:47 +01:00
bd32016632 Improve features presentation in README 2018-11-11 21:40:05 +01:00
77b620e1d0 Merge branch 'record' into dev (#292)
Record screen to file
2018-11-11 21:39:38 +01:00
22ff03f2f7 Do not queue invalid PTS
Configuration packets produced by MediaCodec have no valid PTS, and do
not produce frame. Do not queue their (invalid) PTS not to break the
matching between frames and their PTS.
2018-11-11 21:37:31 +01:00
60afb46c8d Store queue of PTS for pending frames
Several frames may be read by read_packet() before they are consumed
(returned by av_read_frame()), so we need to store the PTS of frames in
order, so that the right PTS is assigned to the right frame.
2018-11-11 21:37:31 +01:00
345f8858d3 Send frame meta only if recording is enabled
The client needs the PTS for each frame only if recording is enabled.
Otherwise, the PTS are not necessary, and the protocol is more
straighforward.
2018-11-11 21:37:31 +01:00
22bf0c19d6 Rename --output-file to --record
To record the screen to a local file:

    scrcpy --record file.mp4
2018-11-11 21:37:31 +01:00
70579dc709 Wrap receiver state into separate struct
For readability, wrap the state of the receiver in a separate struct
receiver_state.
2018-11-11 21:37:31 +01:00
e562837c0b Avoid partial header reads
Use net_recv_all() to avoid partial reads for the "meta" header (this
would break the whole stream).
2018-11-11 21:37:31 +01:00
ebe998cf78 Move buffer reader functions to buffer_util.h 2018-11-11 21:37:31 +01:00
b98eb7d0fa Support AVStream.codec for old FFmpeg versions
AVStream.codec has been deprecated in favor of AVStream.codecpar.

Due to the FFmpeg/Libav split, this happened in two separate versions:
 - 57.33.100 for FFmpeg
 - 57.5.0 for Libav
2018-11-11 21:37:31 +01:00
e361b49b4a recorder: use av_oformat_next to support older FFmpeg
Signed-off-by: yuchenlin <npes87184@gmail.com>
2018-11-11 21:37:31 +01:00
d0e090e1f9 Reenable custom SDL signal handlers
This partially reverts commit f00c6c5b13.

On Ctrl+C, we need to execute cleanup code. For instance, if recording
is enabled, we need to write MP4 file trailer on exit.

Custom SDL signal handlers were disabled because it leaded to process
hanging on Ctrl+C during network calls on initialization, but now it
seems to work correctly, the network calls return immediately on signal.
2018-11-11 21:37:31 +01:00
475912a39c Do not transmit MediaCodec flags
Since PTS handling has been fixed, the recorder do not associate a PTS
to a wrong frame anymore, so PTS of "configuration packets" (which never
produce a frame), are never read by the recorder. Therefore, there is no
need to ignore them explicitly, so we can remove the MediaCodec flags
completely.
2018-11-11 21:37:31 +01:00
27e8a9a79d Assign PTS to the right frame
The PTS was read from the socket and set as the current one even before
the frame was consumed, so it could be assigned to the previous frame
"in advance".

Store the PTS for the current frame and the last PTS read from the
packet header of the next frame in separate fields.

As a side-effect, this fixes the warning on quit:

> Application provided invalid, non monotonically increasing dts to
> muxer in stream 0: 17164020 >= 17164020
2018-11-11 21:37:31 +01:00
61db575861 Decode and push frame before recording
Handle display before recording, to reduce latency.
2018-11-11 21:37:31 +01:00
2cd99e7205 Only set valid PTS/DTS
When the PTS is valid, set both PTS and DTS to avoid FFmpeg warnings.

Since configuration packets have no PTS, do not record these packets.
2018-11-11 21:37:26 +01:00
27686e9361 Add recorder
Implement recording in a separate "class".
2018-11-11 16:30:23 +01:00
d706c5df39 Enable video output file, with pts set by server 2018-11-11 16:30:23 +01:00
b5c64c0f5a Fix SDL 2.0.9 for Windows
Add missing version upgrade in cross_winXX.txt files.
2018-11-11 16:29:03 +01:00
a5787dccd6 Update SDL (2.0.9) for Windows
Include the last version of SDL in Windows releases.
2018-11-11 16:04:17 +01:00
cb3cf801c8 Extract bit operations to buffer_util.h
Move util functions to a reusable separate header.
2018-11-11 01:01:56 +01:00
b1d2c2c640 Explain how to install up-to-date meson
On Ubuntu 16.04, meson is 0.29, while scrcpy requires >= 0.37.

Explain how to install a newer version from pip3.
2018-11-11 00:08:41 +01:00
9160d465ec Add feature test macro to declare kill()
Avoid the following warning on some systems:

> warning: implicit declaration of function 'kill'
> [-Wimplicit-function-declaration]
2018-11-10 16:16:08 +01:00
5c739874a4 Fix memory leak on error
On decode error, unref the packet.
2018-11-09 16:10:44 +01:00
d061c30965 Replace Ctrl by Meta for volume shortcuts on MacOS
Ctrl+UP and Ctrl+DOWN are already used by the window manager on MacOS.

Use Cmd key instead (like on VLC).
2018-11-01 16:19:07 +01:00
5bf1261364 Refactor to support Meta in shortcuts
Move the Ctrl and Meta key down checks to each shortcut individually, so
that we can add a shortcut involving Meta.
2018-11-01 16:19:07 +01:00
facbbced9e Merge pull request #310 from npes87184/master
fix text memory leak
2018-10-27 14:49:00 +02:00
96056e3213 input_manager: fix potential memory leak on text
Fix potential memory leak when controller_push_event failed.

Signed-off-by: yuchenlin <npes87184@gmail.com>
2018-10-27 20:07:22 +08:00
0b92b93358 Capture Alt and Meta keys
Alt and Meta keys should not be forwarded to the device. For now, they
are not used for shortcuts, but they could be.
2018-10-24 19:08:36 +02:00
c20245630e Factorize Windows command building
Extract command line building to a separate method.
2018-10-21 18:57:06 +02:00
b882322f73 Work around Os.write() not updating position
ByteBuffer position is not updated as expected by Os.write() on old
Android versions. Count the remaining bytes manually.

Fixes <https://github.com/Genymobile/scrcpy/issues/291>.
2018-10-09 08:43:17 +02:00
8875955921 Support paths containing spaces on Windows
Quote the arguments of "adb push" to support paths which contain spaces
on Windows.

Fixes <https://github.com/Genymobile/scrcpy/issues/288>.
2018-10-04 21:01:23 +02:00
ff4430b2a3 Declare fun(void) functions with no parameters
This is not C++.
2018-10-04 17:04:20 +02:00
cea176c210 Update links to v1.4 in README and BUILD 2018-10-04 00:13:28 +02:00
f613752606 Update platform-tools (28.0.1) for Windows
Include the latest version of adb in Windows releases.
2018-10-03 23:18:37 +02:00
24d107d017 Bump version to 1.4 2018-10-03 23:03:27 +02:00
66d1f81f56 Merge branch 'master' into dev 2018-10-03 23:02:09 +02:00
411aa4fcfd Handle alpha and space chars as raw events
To handle special chars, text is handled as text input instead of key
events. However, this breaks the separation of DOWN and UP key events.

As a compromise, send letters and space as key events, to preserve
original DOWN/UP events, but send other text input events as text, to be
able to send "special" characters.

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

Suggested-by: pete1414
Suggested-by: King-Slide <kingslide@gmail.com>
2018-10-03 22:07:09 +02:00
78d5a4d8a1 Add link to Gentoo Ebuild in README 2018-09-19 22:09:52 +02:00
52e2c60190 Merge pull request #261 from npes87184/dev
prevent closing console right after process error in windows
2018-09-18 08:56:40 +02:00
140b1ef6a5 prevent closing console right after process error in windows
Signed-off-by: yuchenlin <npes87184@gmail.com>
2018-09-14 20:34:59 +08:00
eca99d5af7 Fix header guard name 2018-09-13 16:27:19 +02:00
6a1fb070f7 Merge branch 'npes87184:dev' into dev (#254)
Return specific error for missing adb binary on Windows
2018-09-09 15:28:56 +02:00
27bed948d4 Use specific error for missing binary on Windows
Signed-off-by: yuchenlin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2018-09-09 15:28:25 +02:00
66def38b73 Avoid additional buffer copy in userspace
Directly send the data from MediaCodec buffers to the LocalSocket,
without an intermediate copy in userspace.
2018-09-09 15:05:06 +02:00
a60aef5aaf Merge branch 'philippsandhaus:master' into dev (#252)
Added new command line parameter to start in fullscreen
2018-09-04 18:51:56 +02:00
28015c3ee4 Present fullscreen option in README 2018-09-04 18:51:35 +02:00
af9808cf02 Add option to start in fullscreen
Signed-off-by: Romain Vimont <rom@rom1v.com>
2018-09-04 18:51:32 +02:00
34550311be Merge branch 'npes87184:dev' into dev (#236)
check adb runnable before starting scrcpy
2018-09-04 18:43:17 +02:00
55d33ddd5f Do not handle system-specific values in command.c
The common command.c handled process errors from system-specific int
values (errno).

Rather, expose a new enum process_result to handle error cause in a
generic way.
2018-09-04 08:57:07 +02:00
6d2d803003 Notify adb missing
There are many user who encounters missing adb.
To stop things happens again, we check it and show
sexy response to user.

Signed-off-by: yuchenlin <npes87184@gmail.com>
2018-09-04 08:46:17 +02:00
fdbb725436 Add link to FLAG_SECURE in FAQ 2018-08-20 15:00:01 +02:00
ce6e5d1969 Explain how to install adb on Mac OS
The package scrcpy from Homebrew does not install adb.
2018-08-17 19:51:25 +02:00
963890e9c2 Separate build instructions from README
README included build instructions, which made it complicated to follow.
Move the build instructions to a separate file (BUILD.md).
2018-08-17 17:57:08 +02:00
3b5e54278e Update FFmpeg (4.0.2) for Windows
Include the last version of FFmpeg in Windows releases.
2018-08-15 19:30:02 +02:00
dd3ba685e4 Update platform-tools (28.0.0) for Windows
Include the last version of adb in Windows releases.

Fixes <https://github.com/Genymobile/scrcpy/issues/224>.
2018-08-15 19:30:02 +02:00
89e0203682 Add missing include for lock_util.h
lock_util.c did not include lock_util.h. This was catched by the gcc
option -Wmissing-prototypes.
2018-08-15 19:30:02 +02:00
536b31829a Separate multi-words filenames by '_'
Rename foobar.ext to foo_bar.ext.

<https://github.com/Genymobile/scrcpy/pull/226#discussion_r209454865>
2018-08-15 19:30:01 +02:00
f3f704d1ed Document "push file" feature
Document how to push a file to /sdcard/ in the shortcuts list.
2018-08-15 17:19:32 +02:00
6581f9feb9 Make request_queue functions static
These functions are local to file_handler.c.
2018-08-15 17:19:32 +02:00
359685b1db Simplify SDL_assert() calls
SDL_assert() already prevents "unused variable" warnings.
2018-08-15 17:19:32 +02:00
4527be4cde Add missing include config.h
When config.h is not included, BUILD_DEBUG is not set.
2018-08-15 17:19:32 +02:00
92d1aff85f Merge branch 'npes87184:push_file_to_sdcard' into dev (#226)
support drag & drop file to device /sdcard
2018-08-15 17:18:15 +02:00
66f45f9dae Support drag&drop a file to transfer it to device
Signed-off-by: npes87184 <npes87184@gmail.com>
2018-08-15 17:12:10 +02:00
aa97eed24b installer -> file_handler
Signed-off-by: npes87184 <npes87184@gmail.com>
2018-08-15 17:11:41 +02:00
2daeb1fd5f Reset current installer process
The current_process field was never reset after an installation is
complete. As a consequence, installer_stop() attempted to terminate it,
leading to a warning, at best.
2018-08-15 17:10:07 +02:00
cde0b3d248 Return non-zero value on connection loss
Make scrscpy to return 0 exit code only in case when the user closes its
app, otherwise, i.e. in case of connection loss, return 1.
2018-08-15 14:20:45 +02:00
6fa209fa82 Remove AINPUT_SOURCE_ANY value
In the Android input header file, an enum has a value taking more than
31 bits, leading to the following warning:

    ISO C restricts enumerator values to range of ‘int’

Since we don't use it, remove it.
2018-08-12 15:35:29 +02:00
b37c0f6cd6 Replace Uint32 by int to fix warnings in tinyxpm 2018-08-12 15:35:25 +02:00
c4a1fc746b Explicitly use ISO C11
ISO C99 doesn't support unnamed structs/unions.
2018-08-12 15:21:30 +02:00
a3ab92226d Destroy mutex if strdup failed
Signed-off-by: npes87184 <npes87184@gmail.com>
2018-08-12 14:57:36 +02:00
ec66b3be82 Merge pull request #225 from npes87184/master
remove redundant semicolon
2018-08-12 14:56:13 +02:00
f8ef4f1cf7 remove redundant semicolon
Signed-off-by: npes87184 <npes87184@gmail.com>
2018-08-11 21:16:36 +08:00
9e683b7dac Update links to v1.3 in README 2018-08-09 19:26:31 +02:00
7e42358a7b Bump version to 1.3 2018-08-09 19:14:17 +02:00
caa9e30004 Add crop feature
Add an option to crop the screen on the server. This allows to mirror
only part of the device screen.
2018-08-09 19:12:27 +02:00
e85010fbc2 Move annotation comment
This confused the Android Studio code formatter.
2018-08-09 18:23:38 +02:00
820cd2bb54 Extract video size computation
One method, one thing.
2018-08-09 18:22:52 +02:00
8793c104ee Increase "adb forward" connection attempts
5 seconds might not be sufficient:
<https://github.com/Genymobile/scrcpy/issues/213>

Increase to 10 seconds (it does not harm).
2018-08-09 18:22:42 +02:00
fca806e095 Do not call deprecated av_register_all()
av_register_all() is deprecated in FFmpeg since this commit:
<http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=0694d8702421e7aff1340038559c438b61bb30dd>

It is now useless to call it:
<https://ffmpeg.org/pipermail/ffmpeg-devel/2018-February/225051.html>

Fixes <https://github.com/Genymobile/scrcpy/issues/203>.
2018-08-09 18:18:22 +02:00
280891837e Simplify README for Windows users
So that users stop downloading platform-tools unnecessarily.
2018-07-16 17:13:36 +02:00
49b2e63d13 Forward repeated volume events
Send repeated events when holding volume up/down shortcuts.
2018-06-24 21:51:54 +02:00
c12c64ed41 Send separate DOWN/UP key events
Shortcuts generated instant DOWN/UP key events. Instead, generate DOWN
event on Ctrl+key down and UP event on Ctrl+key up.

Fixes <https://github.com/Genymobile/scrcpy/issues/166>.
2018-06-24 21:50:53 +02:00
2f66acd75d Improve English comment
Replace "implying" by "involving" (both "impliquant" in French).
2018-06-24 20:46:14 +02:00
1846d2f078 Prevent killing unexpected process
A missing initialization (fixed by the previous commit) leaded to kill
unexpected process.

In order to prevent consequences of similar errors in the future, never
call kill() with a non-positive PID.

See <https://github.com/Genymobile/scrcpy/issues/182>.
2018-06-22 19:56:58 +02:00
1a0139321b Fix missing installer initialization
The current_process field of struct installer was not initialized.
Since the installer instance is static, its default value was 0.

The call to installer_stop() then called kill(0, SIGTERM) (on Linux),
which sent SIGTERM to every process in the process group. In particular,
the scrcpy process was killed.

As a consequence, the last cleanup steps, like disabling "show touches",
were not executed.

Fixes <https://github.com/Genymobile/scrcpy/issues/183>.
2018-06-22 18:35:58 +02:00
8890750681 Merge pull request #169 from zopelee/master
Fix meson error: ‘for’ loop initial declarations are only allowed in …
2018-06-08 08:08:00 +02:00
aac9d5057f Fix meson error: ‘for’ loop initial declarations are only allowed in C99 mode. 2018-06-08 12:21:04 +08:00
f705a73149 Use a meson option to crossbuild for Windows
Meson decided to crossbuild for Windows as soon as
meson.is_cross_build() returned true. This made non-Windows crossbuilds
fail.

Instead, add an explicit option "crossbuild_windows".

Fixes <https://github.com/Genymobile/scrcpy/issues/165>.
2018-06-05 20:45:41 +02:00
27a9bd3424 Fix clean recipe in cross Makefile
Make the "clean" recipe also remove the noconsole build directories.
2018-06-05 20:42:35 +02:00
cf121a0f65 Upgrade gradle 2018-06-05 20:33:37 +02:00
c8a5f9dc63 Update links to v1.2 in README 2018-05-28 22:52:52 +02:00
8106bb8215 Bump version to 1.2 2018-05-28 22:32:14 +02:00
f1b3a40375 Rename SHA256SUM to SHA256SUMS
It contains the checksums for several files.
2018-05-28 22:32:14 +02:00
a63dd47f2d Make CreateProcess() flags depend on "noconsole"
On Windows, display the output of external commands (adb) when a console
is available.
2018-05-28 22:28:20 +02:00
6b4bbb1fb3 Update README and FAQ for the new Windows releases
Document how to generate a Windows release from Linux.

It solves the "no output" issue on Windows, so update the FAQ.
2018-05-28 20:45:54 +02:00
aedc2c2da9 Also build "noconsole" binary for Windows
On Windows, an application is either console or gui, it cannot be both.

Scrcpy should be both: it outputs important information to console, but
we still want to be able to ignore the console and launch it without a
visible cmd.exe window.

Therefore, build two binaries:
 - scrcpy.exe
 - scrcpy-noconsole.exe
2018-05-28 20:45:54 +02:00
106b87a4d2 Add cross-compilation scripts for Windows
Build the Windows binary from mingw on Linux, using the official
prebuilt binaries for ffmpeg, SDL2 and adb.

MSYS2 and all its packaged dll are not necessary anymore.
2018-05-28 20:45:54 +02:00
7d68316f45 Indicate that libs are included for Windows 2018-05-28 20:45:54 +02:00
e729b76176 Indicate that scrcpy also works over TCP/IP 2018-05-28 20:45:54 +02:00
6274cc3767 Document APK drag & drop
Add the drag & drop shortcut for installing an APK file to the README
and the help.
2018-05-28 20:45:54 +02:00
9c6f9b24f9 Quote apk path on Windows
Windows will parse the string, so the local name must be quoted.
2018-05-28 20:45:54 +02:00
e2a2973990 Drag and drop to install apk files from computer
<https://github.com/Genymobile/scrcpy/pull/133>
2018-05-28 20:45:54 +02:00
41419a2e77 Fix net_send_all() warning
If len is not positive, w could be returned uninitialized.
2018-05-28 20:21:56 +02:00
e3d0a59c80 Fix proc_show_touches warning
In practice, proc_show_touches may not be used uninitialized, since it
checks the flag options->show_touches, but the compiler can't know that,
so initialize it to avoid the warning.
2018-05-28 20:21:50 +02:00
bb3a7f05ac Release controller lock while processing events
Once the controller took a control event, release the mutex before
processing it, so that the main thread is not blocked to push a new
event.
2018-05-26 15:14:46 +02:00
ec02823045 Fix leak on server start error
Fail on SDL_strdup() failure, and free the duplicated serial on further
error.
2018-05-26 14:20:05 +02:00
435a7fe314 Add missing includes
Include string.h and stdio.h explicitly.
2018-05-13 15:33:13 +02:00
78da66f126 Merge branch 'master' into dev 2018-04-08 12:41:51 +02:00
9aa88b6fc3 Map numpad ENTER key
Forward numpad ENTER key to the device.

Fixes <https://github.com/Genymobile/scrcpy/issues/117>.
2018-04-08 12:40:05 +02:00
f8ad7df3be Add FAQ section about KWin crash
Link to the workaround to keep the compositor enabled while _scrcpy_ is
running.
2018-04-05 02:03:26 +02:00
0871bca9c7 Avoid pointer arithmetic on "void *"
Fix the following warning (with -Wpedantic enabled):

    pointer of type ‘void *’ used in arithmetic [-Wpointer-arith]
2018-04-04 10:50:12 +02:00
b2b5404883 Use const pointers when possible
Sending data only require to read the input buffer, so declare it const.
2018-04-04 10:50:07 +02:00
1bddb80b5f Change volume shortcuts
I could not make Ctrl+'+' and Ctrl+'-' work for every keyboard on every
platform.

Instead, use Ctrl+UP and Ctrl+DOWN (like in VLC) to change the volume.

Fixes <https://github.com/Genymobile/scrcpy/issues/103>.
2018-04-03 14:25:11 +02:00
6323f3974f Document 32 bits packages Windows in README
To build for Windows 32 bits, use the i686 packages instead.
2018-03-31 11:07:25 +02:00
16a3de1796 Make checkstyle happy
Reorder the imports to remove checkstyle warnings.
2018-03-28 22:05:34 +02:00
6161f7688c Install on macOS via Homebrew in README
The application is now packaged for Homebrew:
<https://github.com/Homebrew/homebrew-core/pull/25173>

Give instructions to install it from Homebrew for macOS (it's much
easier).

Thanks to @stek29 for the formula ;-)
2018-03-28 14:14:56 +02:00
2a02fb3611 Document how to make a portable build on Windows
On MSYS2, ./gradlew does not work as expected, so use its absolute path.
2018-03-28 11:08:01 +02:00
159 changed files with 12902 additions and 3739 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/

268
BUILD.md Normal file
View File

@ -0,0 +1,268 @@
# Build scrcpy
Here are the instructions to build _scrcpy_ (client and server).
You may want to build only the client: the server binary, which will be pushed
to the Android device, does not depend on your system and architecture. In that
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
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`adb`).
On Windows, download the [platform-tools][platform-tools-windows] and extract
the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
[adb]: https://developer.android.com/studio/command-line/adb.html
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
## System-specific steps
### Linux
Install the required packages from your package manager.
#### Debian/Ubuntu
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0-0 adb
# client build dependencies
sudo apt install gcc git pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
# server build dependencies
sudo apt install openjdk-8-jdk
```
On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install
it from `pip3`:
```bash
sudo apt install python3-pip
pip3 install meson
```
#### Fedora
```bash
# enable RPM fusion free
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies
sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
```
### Windows
#### Cross-compile from Linux
This is the preferred method (and the way the release is built).
From _Debian_, install _mingw_:
```bash
sudo apt install mingw-w64 mingw-w64-tools
```
You also need the JDK to build the server:
```bash
sudo apt install openjdk-8-jdk
```
Then generate the releases:
```bash
make -f Makefile.CrossWindows
```
It will generate win32 and win64 releases into `dist/`.
#### In MSYS2
From Windows, you need [MSYS2] to build the project. From an MSYS2 terminal,
install the required packages:
[MSYS2]: http://www.msys2.org/
```bash
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
# client build dependencies
pacman -S mingw-w64-x86_64-make \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-meson
```
For a 32 bits version, replace `x86_64` by `i686`:
```bash
# runtime dependencies
pacman -S mingw-w64-i686-SDL2 \
mingw-w64-i686-ffmpeg
# client build dependencies
pacman -S mingw-w64-i686-make \
mingw-w64-i686-gcc \
mingw-w64-i686-pkg-config \
mingw-w64-i686-meson
```
Java (>= 7) is not available in MSYS2, so if you plan to build the server,
install it manually and make it available from the `PATH`:
```bash
export PATH="$JAVA_HOME/bin:$PATH"
```
### Mac OS
Install the packages with [Homebrew]:
[Homebrew]: https://brew.sh/
```bash
# runtime dependencies
brew install sdl2 ffmpeg
# client build dependencies
brew install pkg-config meson
```
Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it avaliable from the `PATH`:
```bash
brew tap caskroom/versions
brew cask install java8
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
export PATH="$JAVA_HOME/bin:$PATH"
```
### Docker
See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
## Common steps
If you want to build the server, install the [Android SDK] (_Android Studio_),
and set `ANDROID_HOME` to its directory. For example:
[Android SDK]: https://developer.android.com/studio/index.html
```bash
export ANDROID_HOME=~/android/sdk
```
If you don't want to build the server, use the [prebuilt server].
Clone the project:
```bash
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
```
Then, build:
```bash
meson x --buildtype release --strip -Db_lto=true
ninja -Cx
```
_Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja
install` must be run as root)._
[ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a
### Run
To run without installing:
```bash
./run x [options]
```
### Install
After a successful build, you can install _scrcpy_ on the system:
```bash
sudo ninja -Cx install # without sudo on Windows
```
This installs two files:
- `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server`
Just remove them to "uninstall" the application.
You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.13`][direct-scrcpy-server]
_(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_
[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
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
@ -32,7 +32,7 @@ The server is a Java application (with a [`public static void main(String...
args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123
To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
@ -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
@ -65,17 +65,20 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view
[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view
### Threading
The server uses 2 threads:
The server uses 3 threads:
- the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control events_ (typically,
keyboard and mouse events) from the client.
- the **controller** thread, listening for _control messages_ (typically,
keyboard and mouse events) from the client;
- the **receiver** thread (managed by the controller), sending _device messges_
to the clients (currently, it is only used to send the device clipboard
content).
Since the video encoding is typically hardware, there would be no benefit in
encoding and streaming in two different threads.
@ -89,9 +92,9 @@ The video is encoded using the [`MediaCodec`] API. The codec takes its input
from a [surface] associated to the display, and writes the resulting H.264
stream to the provided output stream (the socket connected to the client).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69
On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced.
@ -105,30 +108,30 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection
_Control events_ are received from the client by the [`EventController`] (run in
a separate thread). There are 5 types of input events:
_Control messages_ are received from the client by the [`Controller`] (run in a
separate thread). There are several types of input events:
- keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly),
- mouse motion/click,
- mouse scroll,
- custom command (e.g. to switch the screen on).
- other commands (e.g. to switch the screen on or to copy the clipboard).
All of them may need to inject input events to the system. To do so, they use
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
Some of them need to inject input events to the system. To do so, they use the
_hidden_ method [`InputManager.injectInputEvent`] (exposed by our
[`InputManager` wrapper][inject-wrapper]).
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@ -145,15 +148,15 @@ The video stream is decoded by [libav] (FFmpeg).
### Initialization
On startup, in addition to _libav_ and _SDL_ initialization, the client must
push and start the server on the device, and open a socket so that they may
communicate.
push and start the server on the device, and open two sockets (one for the video
stream, one for control) so that they may communicate.
Note that the client-server roles are expressed at the application level:
- the server _serves_ video stream and handle requests from the client,
- the client _controls_ the device through the server.
However, the roles are inverted at the network level:
However, the roles are reversed at the network level:
- the client opens a server socket and listen on a port before starting the
server,
@ -162,6 +165,9 @@ However, the roles are inverted at the network level:
This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling.
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
reverse`. See commit [1038bad] and [issue #5].)_
Once the server is connected, it sends the device information (name and initial
screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available.
@ -169,25 +175,38 @@ the first frame is available.
To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]).
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
### Threading
The client uses 3 threads:
The client uses 4 threads:
- the **main** thread, executing the SDL event loop,
- the **decoder** thread, decoding video frames,
- the **controller** thread, sending _control events_ to the server.
- the **stream** thread, receiving the video and used for decoding and
recording,
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
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
print the framerate regularly in the console.
### Decoder
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264
stream from the socket, and notifies the main thread when a new frame is
available.
### Stream
There are two [frames] simultaneously in memory:
The video [stream] is received from the socket (connected to the server on the
device) in a separate thread.
If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_
to decode the H.264 stream from the socket, and notifies the main thread when a
new frame is available.
There are two [frames][video_buffer] simultaneously in memory:
- the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread.
@ -195,26 +214,39 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediatly starts
to decode a new frame while the main thread renders the last one.
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h
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
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h
```
+----------+ +----------+
---> | decoder | ---> | screen |
+---------+ / +----------+ +----------+
socket ---> | stream | ----
+---------+ \ +----------+
---> | recorder |
+----------+
```
### Controller
The [controller] is responsible to send _control events_ to the device. It runs
in a separate thread, to avoid I/O on the main thread.
The [controller] is responsible to send _control messages_ to the device. It
runs in a separate thread, to avoid I/O on the main thread.
On SDL event, received on the main thread, the [input manager][inputmanager]
creates appropriate [_control events_][controlevent]. It is responsible to
creates appropriate [_control messages_][controlmsg]. It is responsible to
convert SDL events to Android events (using [convert]). It pushes the _control
events_ to a blocking queue hold by the controller. On its own thread, the
controller takes events from the queue, that it serializes and sends to the
client.
messages_ to a queue hold by the controller. On its own thread, the controller
takes messages from the queue, that it serializes and sends to the client.
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h
[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h
### UI and event loop
@ -225,9 +257,9 @@ thread.
Events are handled in the [event loop], which either updates the [screen] or
delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201
[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h
## Hack
@ -236,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
```

194
FAQ.md
View File

@ -1,66 +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, I have no output in the console
When run in `cmd.exe`, the application does not print anything. Even `scrcpy
--help` have no output. We don't know why yet.
## `adb` issues
However, if you run the very same `scrcpy.exe` from
[MSYS2](https://www.msys2.org/) (`mingw64`), then it correctly prints output.
`scrcpy` execute `adb` commands to initialize the connection with the device. If
`adb` fails, then scrcpy will not work.
As a workaround, redirect outputs to files, so that you can read the files
afterwards:
In that case, it will print this error:
> 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
scrcpy >stdout 2>stderr
type stdout
type stderr
adb devices
```
_Note that all SDL logs are printed to stderr._
### `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.
### On Windows, when I start the application, nothing happens
### Device unauthorized
The previous problem does not help to get a clue about the cause.
Check [stackoverflow][device-unauthorized].
The most common is your device not being detected by `adb`, or is unauthorized.
Check everything is ok by calling:
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
adb devices
Windows may need some [drivers] to detect your device.
### 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
If you still encounter problems, please see [issue 9].
[issue 9]: https://github.com/Genymobile/scrcpy/issues/9
### 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
### I get a black screen for some applications like Silence
### Conflicts between adb versions
This is expected, they requested to protect the screen.
> adb server version (41) doesn't match this client (39); killing...
In [Silence], you can disable it in settings → Privacy → Screen security.
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.
[silence]: https://f-droid.org/en/packages/org.smssecure.smssecure/
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:
See [issue 36].
[issue 36]: https://github.com/Genymobile/scrcpy/issues/36
```bash
set ADB=/path/to/your/adb
scrcpy
```
### Mouse clicks do not work
### 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
@ -68,14 +112,88 @@ 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
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,6 +188,7 @@
identification within third-party archives.
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.

View File

@ -1,62 +0,0 @@
# This makefile provides recipes to build a "portable" version of scrcpy.
#
# 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).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
#
# "make release-portable" builds a zip containing the client and the server.
#
# This is a simple Makefile because Meson is not flexible enough to execute some
# arbitrary commands.
.PHONY: default clean build-portable release-portable dist-portable dist-portable-zip sums test check
GRADLE ?= ./gradlew
PORTABLE_BUILD_DIR := build-portable
DIST := dist
TARGET_DIR := scrcpy
VERSION := $(shell git describe --tags --always)
TARGET := $(TARGET_DIR)-$(VERSION).zip
default:
@echo 'You must specify a target. Try: make release-portable'
clean:
$(GRADLE) clean
rm -rf "$(PORTABLE_BUILD_DIR)" "$(DIST)"
build-portable:
[ -d "$(PORTABLE_BUILD_DIR)" ] || ( mkdir "$(PORTABLE_BUILD_DIR)" && \
meson "$(PORTABLE_BUILD_DIR)" \
--buildtype release --strip -Db_lto=true \
-Doverride_server_path=scrcpy-server.jar )
ninja -C "$(PORTABLE_BUILD_DIR)"
release-portable: clean dist-portable-zip sums
@echo "Release created in $(DIST)/."
dist-portable: build-portable
mkdir -p "$(DIST)/$(TARGET_DIR)"
cp "$(PORTABLE_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(TARGET_DIR)/"
cp "$(PORTABLE_BUILD_DIR)"/app/scrcpy "$(DIST)/$(TARGET_DIR)/"
dist-portable-zip: dist-portable
cd "$(DIST)"; \
zip -r "$(TARGET)" "$(TARGET_DIR)"
sums:
cd "$(DIST)"; \
sha256sum *.zip > SHA256SUM.txt
test: build-portable
$(GRADLE) test
ninja -C "$(PORTABLE_BUILD_DIR)" test
check: build-portable
$(GRADLE) check
ninja -C "$(PORTABLE_BUILD_DIR)" test

138
Makefile.CrossWindows Normal file
View File

@ -0,0 +1,138 @@
# This makefile provides recipes to build a "portable" version of scrcpy for
# Windows.
#
# 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).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
.PHONY: default clean \
build-server \
prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win32-noconsole \
build-win64 build-win64-noconsole \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
sums release
GRADLE ?= ./gradlew
SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole
WIN64_BUILD_DIR := build-win64
WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32
WIN64_TARGET_DIR := scrcpy-win64
VERSION := $(shell git describe --tags --always)
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
release: clean zip-win32 zip-win64 sums
@echo "Windows archives generated in $(DIST)/"
clean:
$(GRADLE) clean
rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \
"$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)"
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
-$(MAKE) -C prebuilt-deps prepare-win32
build-win32: prepare-deps-win32
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
meson "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32
[ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64:
-$(MAKE) -C prebuilt-deps prepare-win64
build-win64: prepare-deps-win64
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
meson "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dportable=true )
ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64
[ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(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.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.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 "$(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.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.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
zip -r "../$(WIN32_TARGET)" .
zip-win64: dist-win64
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \
zip -r "../$(WIN64_TARGET)" .
sums:
cd "$(DIST)"; \
sha256sum *.zip > SHA256SUMS.txt

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/

679
README.md
View File

@ -1,222 +1,122 @@
# scrcpy
# scrcpy (v1.13)
This application provides display and control of Android devices connected on
USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
and _MacOS_.
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
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
The Android part requires at least API 21 (Android 5.0).
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
On Windows, if you use the [prebuilt application](#windows), it is already
included. Otherwise, just download the [platform-tools][platform-tools-windows]
and extract the following files to a directory accessible from your `PATH`:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
The Android device requires at least API 21 (Android 5.0).
Make sure you [enabled adb debugging][enable-adb] on your device(s).
The client requires [FFmpeg] and [LibSDL2].
[adb]: https://developer.android.com/studio/command-line/adb.html
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
## Build and install
On some devices, you also need to enable [an additional option][control] to
control it using keyboard and mouse.
### System-specific steps
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
#### Linux
Install the required packages from your package manager.
## Get the app
##### Debian/Ubuntu
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0.0
### Linux
# client build dependencies
sudo apt install make gcc pkg-config meson \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04):
# server build dependencies
sudo apt install openjdk-8-jdk
```
apt install scrcpy
```
##### Fedora
A [Snap] package is available: [`scrcpy`][snap-link].
```bash
# enable RPM fusion free
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
[snap-link]: https://snapstats.org/snaps/scrcpy
# client build dependencies
sudo dnf install SDL2-devel ffms2-devel meson gcc make
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
# server build dependencies
sudo dnf install java
```
##### Arch Linux
Two [AUR] packages have been created by users:
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
[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
### Windows
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
- [`scrcpy-windows-with-deps-v1.1.zip`][direct-windows-with-deps].
_(SHA-256: 27eb36c15937601d1062c1dc0b45faae0e06fefea2019aadeb4fa7f76a07bb4c)_
- [`scrcpy-win64-v1.13.zip`][direct-win64]
_(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-windows-with-deps-v1.1.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip
_(It's just a portable version including _dll_ copied from MSYS2.)_
It is also available in [Chocolatey]:
Instead, you may want to build it manually. You need [MSYS2] to build the
project. From an MSYS2 terminal, install the required packages:
[MSYS2]: http://www.msys2.org/
[Chocolatey]: https://chocolatey.org/
```bash
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-ffmpeg
# client build dependencies
pacman -S mingw-w64-x86_64-make \
mingw-w64-x86_64-gcc \
mingw-w64-x86_64-pkg-config \
mingw-w64-x86_64-meson
choco install scrcpy
choco install adb # if you don't have it yet
```
Java (>= 7) is not available in MSYS2, so if you plan to build the server,
install it manually and make it available from the `PATH`:
And in [Scoop]:
```bash
export PATH="$JAVA_HOME/bin:$PATH"
scoop install scrcpy
scoop install adb # if you don't have it yet
```
#### Mac OS
[Scoop]: https://scoop.sh
Use [Homebrew] to install the packages:
You can also [build the app manually][BUILD].
### macOS
The application is available in [Homebrew]. Just install it:
[Homebrew]: https://brew.sh/
```bash
# runtime dependencies
brew install sdl2 ffmpeg
# client build dependencies
brew install pkg-config meson
brew install scrcpy
```
Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it avaliable from the `PATH`:
You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash
brew tap caskroom/versions
brew cask install java8
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
export PATH="$JAVA_HOME/bin:$PATH"
brew cask install android-platform-tools
```
#### Docker
You can also [build the app manually][BUILD].
See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
### Common steps
Install the [Android SDK] (_Android Studio_), and set `ANDROID_HOME` to
its directory. For example:
[Android SDK]: https://developer.android.com/studio/index.html
```bash
export ANDROID_HOME=~/android/sdk
```
Clone the project:
```bash
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
```
Then, build:
```bash
meson x --buildtype release --strip -Db_lto=true
cd x
ninja
```
You can test it from here:
```bash
ninja run
```
Or you can install it on the system:
```bash
sudo ninja install # without sudo on Windows
```
This installs two files:
- `/usr/local/bin/scrcpy`
- `/usr/local/share/scrcpy/scrcpy-server.jar`
Just remove them to "uninstall" the application.
#### Prebuilt server
Since the server binary, that will be pushed to the Android device, does not
depend on your system and architecture, you may want to use the prebuilt binary
instead:
- [`scrcpy-server-v1.1.jar`][direct-scrcpy-server].
_(SHA-256: 14826512bf38447ec94adf3b531676ce038d19e7e06757fb4e537882b17e77b3)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-server-v1.1.jar
In that case, the build does not require Java or the Android SDK.
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
```bash
meson x --buildtype release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server.jar
cd x
ninja
sudo ninja install
```
## Run
_At runtime, `adb` must be accessible from your `PATH`._
If everything is ok, just plug an Android device, and execute:
Plug an Android device, and execute:
```bash
scrcpy
@ -228,62 +128,456 @@ It accepts command-line arguments, listed by:
scrcpy --help
```
For example, to decrease video bitrate to 2Mbps (default is 8Mbps):
## Features
### Capture configuration
#### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to
increase performance.
To limit both the width and height to some value (e.g. 1024):
```bash
scrcpy -b 2M
scrcpy --max-size 1024
scrcpy -m 1024 # short version
```
To limit the video dimensions (e.g. if the device is 2540×1440, but the host
screen is smaller, or cannot decode such a high definition):
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
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
```bash
scrcpy -m 1024
scrcpy --bit-rate 2M
scrcpy -b 2M # short version
```
#### Limit frame rate
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.
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)
```
If `--max-size` is also specified, resizing is applied after cropping.
#### Lock video orientation
To lock the orientation of the mirroring:
```bash
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
```
This affects recording orientation.
### Recording
It is possible to record the screen while mirroring:
```bash
scrcpy --record file.mp4
scrcpy -r file.mkv
```
To disable mirroring while recording:
```bash
scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
# Ctrl+C does not terminate properly on Windows, so disconnect the device
```
"Skipped frames" are recorded, even if they are not displayed in real time (for
performance reasons). Frames are _timestamped_ on the device, so [packet delay
variation] does not impact the recorded file.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
### 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_:
```bash
scrcpy -s 0123456789abcdef
scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # short version
```
To show physical touches while scrcpy is running:
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
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:
```bash
scrcpy --fullscreen
scrcpy -f # short version
```
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
#### Rotation
The window may be rotated:
```bash
scrcpy --rotation 1
```
Possibles values are:
- `0`: no rotation
- `1`: 90 degrees counterclockwise
- `2`: 180 degrees
- `3`: 90 degrees clockwise
The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and
`Ctrl`+`→` _(right)_.
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.
### Other mirroring options
#### Read-only
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash
scrcpy --no-control
scrcpy -n
```
#### 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:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`).
It can be useful to also prevent the device to sleep:
```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.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
#### Show touches
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 --show-touches
scrcpy -t
```
To run without installing:
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
./run x [options]
scrcpy --prefer-text
```
(where `x` is your build directory).
(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/
```
### Audio forwarding
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
Also see [issue #14].
[USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts
| Action | Shortcut |
| -------------------------------------- |:---------------------------- |
| switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
| click on `APP_SWITCH` | `Ctrl`+`s` |
| click on `MENU` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_DOWN` | `Ctrl`+`-` |
| click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click²_ |
| paste computer clipboard to device | `Ctrl`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| 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_
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
| Click on `VOLUME_UP` | `Ctrl`+`` _(up)_ | `Cmd`+`↑` _(up)_
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
| 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`
| Paste computer clipboard to device | `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`
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
## Custom paths
To use a specific _adb_ binary, configure its path in the environment variable
`ADB`:
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server` file, configure its path in
`SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## Why _scrcpy_?
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
@ -294,6 +588,13 @@ A colleague challenged me to find a name as unpronounceable as [gnirehtet].
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
## How to build?
See [BUILD].
[BUILD]: BUILD.md
## Common issues
See the [FAQ](FAQ.md).
@ -309,6 +610,7 @@ Read the [developers page].
## Licence
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.
@ -324,5 +626,8 @@ Read the [developers page].
## Articles
- [Introducing scrcpy](https://blog.rom1v.com/2018/03/introducing-scrcpy/)
- [Scrcpy now works wirelessly](https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/)
- [Introducing scrcpy][article-intro]
- [Scrcpy now works wirelessly][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/

View File

@ -1,103 +1,192 @@
src = [
'src/main.c',
'src/cli.c',
'src/command.c',
'src/controlevent.c',
'src/control_msg.c',
'src/controller.c',
'src/convert.c',
'src/decoder.c',
'src/device.c',
'src/fpscounter.c',
'src/frames.c',
'src/inputmanager.c',
'src/lockutil.c',
'src/net.c',
'src/device_msg.c',
'src/event_converter.c',
'src/file_handler.c',
'src/fps_counter.c',
'src/input_manager.c',
'src/opengl.c',
'src/receiver.c',
'src/recorder.c',
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/strutil.c',
'src/tinyxpm.c',
'src/stream.c',
'src/tiny_xpm.c',
'src/video_buffer.c',
'src/util/net.c',
'src/util/str_util.c'
]
dependencies = [
dependency('libavformat'),
dependency('libavcodec'),
dependency('libavutil'),
dependency('sdl2'),
]
if not get_option('crossbuild_windows')
# native build
dependencies = [
dependency('libavformat'),
dependency('libavcodec'),
dependency('libavutil'),
dependency('sdl2'),
]
else
# cross-compile mingw32 build (from Linux to Windows)
cc = meson.get_compiler('c')
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin'
sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib'
sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include'
sdl2 = declare_dependency(
dependencies: [
cc.find_library('SDL2', dirs: sdl2_bin_dir),
cc.find_library('SDL2main', dirs: sdl2_lib_dir),
],
include_directories: include_directories(sdl2_include_dir)
)
prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared')
prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev')
ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin'
ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include'
ffmpeg = declare_dependency(
dependencies: [
cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir),
cc.find_library('avformat-58', dirs: ffmpeg_bin_dir),
cc.find_library('avutil-56', dirs: ffmpeg_bin_dir),
],
include_directories: include_directories(ffmpeg_include_dir)
)
dependencies = [
ffmpeg,
sdl2,
cc.find_library('mingw32')
]
endif
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', '1.1')
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
# the prefix used during configuration (meson --prefix=PREFIX)
conf.set_quoted('PREFIX', get_option('prefix'))
# the path of the server, which will be appended to the prefix
# ignored if OVERRIDE_SERVER_PATH if defined
# must be consistent with the install_dir in server/meson.build
conf.set_quoted('PREFIXED_SERVER_PATH', '/share/scrcpy/scrcpy-server.jar')
# build a "portable" version (with scrcpy-server accessible from the same
# directory as the executable)
conf.set('PORTABLE', get_option('portable'))
# the path of the server to be used "as is"
# this is useful for building a "portable" version (with the server in the same
# directory as the client)
override_server_path = get_option('override_server_path')
if override_server_path != ''
conf.set_quoted('OVERRIDE_SERVER_PATH', override_server_path)
else
# undefine it
conf.set('OVERRIDE_SERVER_PATH', false)
endif
# the default client TCP port for the "adb reverse" tunnel
# the default client TCP port 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
# whether the app should always display the most recent available frame, even
# if the previous one has not been displayed
# SKIP_FRAMES improves latency at the cost of framerate
conf.set('SKIP_FRAMES', get_option('skip_frames'))
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# 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')
executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true)
if get_option('windows_noconsole')
link_args = [ '-Wl,--subsystem,windows' ]
else
link_args = []
endif
executable('scrcpy', src,
dependencies: dependencies,
include_directories: src_dir,
install: true,
c_args: [],
link_args: link_args)
install_man('scrcpy.1')
### TESTS
tests = [
['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/controlevent.c']],
['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/controlevent.c']],
['test_strutil', ['tests/test_strutil.c', 'src/strutil.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

View File

@ -777,9 +777,6 @@ enum android_input_source {
AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK,
/** rotary encoder */
AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE,
/** any */
AINPUT_SOURCE_ANY = 0xffffff00,
};
/**

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

@ -1,15 +1,19 @@
#include "command.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "config.h"
#include "common.h"
#include "log.h"
#include "util/log.h"
#include "util/str_util.h"
static const char *adb_command;
static inline const char *get_adb_command() {
static inline const char *
get_adb_command(void) {
if (!adb_command) {
adb_command = getenv("ADB");
if (!adb_command)
@ -18,9 +22,91 @@ static inline const char *get_adb_command() {
return adb_command;
}
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len) {
// serialize argv to string "[arg1], [arg2], [arg3]"
static size_t
argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
size_t idx = 0;
bool first = true;
while (*argv) {
const char *arg = *argv;
size_t len = strlen(arg);
// count space for "[], ...\0"
if (idx + len + 8 >= bufsize) {
// not enough space, truncate
assert(idx < bufsize - 4);
memcpy(&buf[idx], "...", 3);
idx += 3;
break;
}
if (first) {
first = false;
} else {
buf[idx++] = ',';
buf[idx++] = ' ';
}
buf[idx++] = '[';
memcpy(&buf[idx], arg, len);
idx += len;
buf[idx++] = ']';
argv++;
}
assert(idx < bufsize);
buf[idx] = '\0';
return idx;
}
static void
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];
switch (err) {
case PROCESS_ERROR_GENERIC:
argv_to_string(argv, buf, sizeof(buf));
LOGE("Failed to execute: %s", buf);
break;
case PROCESS_ERROR_MISSING_BINARY:
argv_to_string(argv, buf, sizeof(buf));
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
break;
}
}
process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
const char *cmd[len + 4];
int i;
process_t process;
cmd[0] = get_adb_command();
if (serial) {
cmd[1] = "-s";
@ -32,10 +118,17 @@ process_t adb_execute(const char *serial, const char *const adb_cmd[], int len)
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
cmd[len + i] = NULL;
return cmd_execute(cmd[0], cmd);
enum process_result r = cmd_execute(cmd, &process);
if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, cmd);
return PROCESS_NONE;
}
return process;
}
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) {
process_t
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
@ -44,14 +137,17 @@ process_t adb_forward(const char *serial, uint16_t local_port, const char *devic
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_forward_remove(const char *serial, uint16_t local_port) {
process_t
adb_forward_remove(const char *serial, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port);
const char *const adb_cmd[] = {"forward", "--remove", local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
process_t
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
@ -60,27 +156,67 @@ process_t adb_reverse(const char *serial, const char *device_socket_name, uint16
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_reverse_remove(const char *serial, const char *device_socket_name) {
process_t
adb_reverse_remove(const char *serial, const char *device_socket_name) {
char remote[108 + 14 + 1]; // localabstract:NAME
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
const char *const adb_cmd[] = {"reverse", "--remove", remote};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_push(const char *serial, const char *local, const char *remote) {
process_t
adb_push(const char *serial, const char *local, const char *remote) {
#ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
local = strquote(local);
if (!local) {
return PROCESS_NONE;
}
remote = strquote(remote);
if (!remote) {
SDL_free((void *) local);
return PROCESS_NONE;
}
#endif
const char *const adb_cmd[] = {"push", local, remote};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
SDL_free((void *) remote);
SDL_free((void *) local);
#endif
return proc;
}
process_t adb_remove_path(const char *serial, const char *path) {
const char *const adb_cmd[] = {"shell", "rm", path};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
process_t
adb_install(const char *serial, const char *local) {
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
local = strquote(local);
if (!local) {
return PROCESS_NONE;
}
#endif
const char *const adb_cmd[] = {"install", "-r", local};
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
SDL_free((void *) local);
#endif
return proc;
}
SDL_bool process_check_success(process_t proc, const char *name) {
bool
process_check_success(process_t proc, const char *name) {
if (proc == PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name);
return SDL_FALSE;
return false;
}
exit_code_t exit_code;
if (!cmd_simple_wait(proc, &exit_code)) {
@ -89,7 +225,7 @@ SDL_bool process_check_success(process_t proc, const char *name) {
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
return SDL_FALSE;
return false;
}
return SDL_TRUE;
return true;
}

View File

@ -1,51 +1,97 @@
#ifndef COMMAND_H
#define COMMAND_H
#include <stdbool.h>
#include <inttypes.h>
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_platform.h>
// <https://stackoverflow.com/a/44383330/1987178>
#ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h>
# define PATH_SEPARATOR '\\'
# define PRIexitcode "lu"
// <https://stackoverflow.com/a/44383330/1987178>
# ifdef _WIN64
# define PRIsizet PRIu64
# else
# define PRIsizet PRIu32
# endif
#else
# define PRIsizet "zu"
# define PRIexitcode "d"
#endif
#ifdef __WINDOWS__
# include <winsock2.h> // not needed here, but must never be included AFTER windows.h
# include <windows.h>
# define PROCESS_NONE NULL
# define NO_EXIT_CODE -1u // max value as unsigned
typedef HANDLE process_t;
typedef DWORD exit_code_t;
#else
# include <sys/types.h>
# define PATH_SEPARATOR '/'
# 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
process_t cmd_execute(const char *path, const char *const argv[]);
SDL_bool cmd_terminate(process_t pid);
SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
#include "config.h"
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
process_t adb_forward_remove(const char *serial, uint16_t local_port);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t adb_push(const char *serial, const char *local, const char *remote);
process_t adb_remove_path(const char *serial, const char *path);
enum process_result {
PROCESS_SUCCESS,
PROCESS_ERROR_GENERIC,
PROCESS_ERROR_MISSING_BINARY,
};
#ifndef __WINDOWS__
bool
cmd_search(const char *file);
#endif
enum process_result
cmd_execute(const char *const argv[], process_t *process);
bool
cmd_terminate(process_t pid);
bool
cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
process_t
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name);
process_t
adb_forward_remove(const char *serial, uint16_t local_port);
process_t
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
process_t
adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t
adb_push(const char *serial, const char *local, const char *remote);
process_t
adb_install(const char *serial, const char *local);
// convenience function to wait for a successful process execution
// automatically log process errors with the provided process name
SDL_bool process_check_success(process_t process, const char *name);
bool
process_check_success(process_t proc, const char *name);
// return the absolute path of the executable (the scrcpy binary)
// may be NULL on error; to be freed by SDL_free
char *
get_executable_path(void);
// returns true if the file exists and is not a directory
bool
is_regular_file(const char *path);
#endif

View File

@ -1,27 +1,35 @@
#ifndef COMMON_H
#define COMMON_H
#include <SDL2/SDL_stdinc.h>
#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)
struct size {
Uint16 width;
Uint16 height;
uint16_t width;
uint16_t height;
};
struct point {
Uint16 x;
Uint16 y;
int32_t x;
int32_t y;
};
struct position {
// The video screen size may be different from the real device screen size,
// so store to which size the absolute position apply, to scale it accordingly.
// so store to which size the absolute position apply, to scale it
// accordingly.
struct size screen_size;
struct point point;
};
struct port_range {
uint16_t first;
uint16_t last;
};
#endif

51
app/src/compat.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef COMPAT_H
#define COMPAT_H
#include <libavformat/version.h>
#include <SDL2/SDL_version.h>
// In ffmpeg/doc/APIchanges:
// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h
// Add AVStream.codecpar, deprecate AVStream.codec.
#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \
|| (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0))
# define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
#endif
// In ffmpeg/doc/APIchanges:
// 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
// Deprecate use of av_register_input_format(), av_register_output_format(),
// av_register_all(), av_iformat_next(), av_oformat_next().
// Add av_demuxer_iterate(), and av_muxer_iterate().
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
# define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
#else
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#endif
// In ffmpeg/doc/APIchanges:
// 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h
// Add a new audio/video encoding and decoding API with decoupled input
// and output -- avcodec_send_packet(), avcodec_receive_frame(),
// avcodec_send_frame() and avcodec_receive_packet().
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
# define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
#endif
#if SDL_VERSION_ATLEAST(2, 0, 5)
// <https://wiki.libsdl.org/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH>
# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
// <https://wiki.libsdl.org/SDL_GetDisplayUsableBounds>
# define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS
// <https://wiki.libsdl.org/SDL_WindowFlags>
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
#endif
#if SDL_VERSION_ATLEAST(2, 0, 8)
// <https://hg.libsdl.org/SDL/rev/dfde5d3f9781>
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
#endif
#endif

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

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

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

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

View File

@ -1,110 +0,0 @@
#include "controlevent.h"
#include <SDL2/SDL_stdinc.h>
#include "lockutil.h"
#include "log.h"
static inline void write16(Uint8 *buf, Uint16 value) {
buf[0] = value >> 8;
buf[1] = value;
}
static inline void write32(Uint8 *buf, Uint32 value) {
buf[0] = value >> 24;
buf[1] = value >> 16;
buf[2] = value >> 8;
buf[3] = value;
}
static void write_position(Uint8 *buf, const struct position *position) {
write16(&buf[0], position->point.x);
write16(&buf[2], position->point.y);
write16(&buf[4], position->screen_size.width);
write16(&buf[6], position->screen_size.height);
}
int control_event_serialize(const struct control_event *event, unsigned char *buf) {
buf[0] = event->type;
switch (event->type) {
case CONTROL_EVENT_TYPE_KEYCODE:
buf[1] = event->keycode_event.action;
write32(&buf[2], event->keycode_event.keycode);
write32(&buf[6], event->keycode_event.metastate);
return 10;
case CONTROL_EVENT_TYPE_TEXT: {
// write length (1 byte) + date (non nul-terminated)
size_t len = strlen(event->text_event.text);
if (len > TEXT_MAX_LENGTH) {
// injecting a text takes time, so limit the text length
len = TEXT_MAX_LENGTH;
}
write16(&buf[1], (Uint16) len);
memcpy(&buf[3], event->text_event.text, len);
return 3 + len;
}
case CONTROL_EVENT_TYPE_MOUSE:
buf[1] = event->mouse_event.action;
write32(&buf[2], event->mouse_event.buttons);
write_position(&buf[6], &event->mouse_event.position);
return 14;
case CONTROL_EVENT_TYPE_SCROLL:
write_position(&buf[1], &event->scroll_event.position);
write32(&buf[9], (Uint32) event->scroll_event.hscroll);
write32(&buf[13], (Uint32) event->scroll_event.vscroll);
return 17;
case CONTROL_EVENT_TYPE_COMMAND:
buf[1] = event->command_event.action;
return 2;
default:
LOGW("Unknown event type: %u", (unsigned) event->type);
return 0;
}
}
void control_event_destroy(struct control_event *event) {
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
SDL_free(event->text_event.text);
}
}
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
return queue->head == queue->tail;
}
SDL_bool control_event_queue_is_full(const struct control_event_queue *queue) {
return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail;
}
SDL_bool control_event_queue_init(struct control_event_queue *queue) {
queue->head = 0;
queue->tail = 0;
// the current implementation may not fail
return SDL_TRUE;
}
void control_event_queue_destroy(struct control_event_queue *queue) {
int i = queue->tail;
while (i != queue->head) {
control_event_destroy(&queue->data[i]);
i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE;
}
}
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {
if (control_event_queue_is_full(queue)) {
return SDL_FALSE;
}
queue->data[queue->head] = *event;
queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE;
return SDL_TRUE;
}
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event) {
if (control_event_queue_is_empty(queue)) {
return SDL_FALSE;
}
*event = queue->data[queue->tail];
queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE;
return SDL_TRUE;
}

View File

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

View File

@ -1,102 +1,134 @@
#include "controller.h"
#include "lockutil.h"
#include "log.h"
#include <assert.h>
SDL_bool controller_init(struct controller *controller, socket_t video_socket) {
if (!control_event_queue_init(&controller->queue)) {
return SDL_FALSE;
#include "config.h"
#include "util/lock.h"
#include "util/log.h"
bool
controller_init(struct controller *controller, socket_t control_socket) {
cbuf_init(&controller->queue);
if (!receiver_init(&controller->receiver, control_socket)) {
return false;
}
if (!(controller->mutex = SDL_CreateMutex())) {
return SDL_FALSE;
receiver_destroy(&controller->receiver);
return false;
}
if (!(controller->event_cond = SDL_CreateCond())) {
if (!(controller->msg_cond = SDL_CreateCond())) {
receiver_destroy(&controller->receiver);
SDL_DestroyMutex(controller->mutex);
return SDL_FALSE;
return false;
}
controller->video_socket = video_socket;
controller->stopped = SDL_FALSE;
controller->control_socket = control_socket;
controller->stopped = false;
return SDL_TRUE;
return true;
}
void controller_destroy(struct controller *controller) {
SDL_DestroyCond(controller->event_cond);
void
controller_destroy(struct controller *controller) {
SDL_DestroyCond(controller->msg_cond);
SDL_DestroyMutex(controller->mutex);
control_event_queue_destroy(&controller->queue);
struct control_msg msg;
while (cbuf_take(&controller->queue, &msg)) {
control_msg_destroy(&msg);
}
receiver_destroy(&controller->receiver);
}
SDL_bool controller_push_event(struct controller *controller, const struct control_event *event) {
SDL_bool res;
bool
controller_push_msg(struct controller *controller,
const struct control_msg *msg) {
mutex_lock(controller->mutex);
SDL_bool was_empty = control_event_queue_is_empty(&controller->queue);
res = control_event_queue_push(&controller->queue, event);
bool was_empty = cbuf_is_empty(&controller->queue);
bool res = cbuf_push(&controller->queue, *msg);
if (was_empty) {
cond_signal(controller->event_cond);
cond_signal(controller->msg_cond);
}
mutex_unlock(controller->mutex);
return res;
}
static SDL_bool process_event(struct controller *controller, const struct control_event *event) {
unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE];
int length = control_event_serialize(event, serialized_event);
static bool
process_msg(struct controller *controller,
const struct control_msg *msg) {
unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int length = control_msg_serialize(msg, serialized_msg);
if (!length) {
return SDL_FALSE;
return false;
}
int w = net_send_all(controller->video_socket, serialized_event, length);
int w = net_send_all(controller->control_socket, serialized_msg, length);
return w == length;
}
static int run_controller(void *data) {
static int
run_controller(void *data) {
struct controller *controller = data;
mutex_lock(controller->mutex);
for (;;) {
while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
cond_wait(controller->event_cond, controller->mutex);
mutex_lock(controller->mutex);
while (!controller->stopped && cbuf_is_empty(&controller->queue)) {
cond_wait(controller->msg_cond, controller->mutex);
}
if (controller->stopped) {
// stop immediately, do not process further events
// stop immediately, do not process further msgs
mutex_unlock(controller->mutex);
break;
}
struct control_event event;
while (control_event_queue_take(&controller->queue, &event)) {
SDL_bool ok = process_event(controller, &event);
control_event_destroy(&event);
if (!ok) {
LOGD("Cannot write event to socket");
goto end;
}
struct control_msg msg;
bool non_empty = cbuf_take(&controller->queue, &msg);
assert(non_empty);
(void) non_empty;
mutex_unlock(controller->mutex);
bool ok = process_msg(controller, &msg);
control_msg_destroy(&msg);
if (!ok) {
LOGD("Could not write msg to socket");
break;
}
}
end:
mutex_unlock(controller->mutex);
return 0;
}
SDL_bool controller_start(struct controller *controller) {
bool
controller_start(struct controller *controller) {
LOGD("Starting controller thread");
controller->thread = SDL_CreateThread(run_controller, "controller", controller);
controller->thread = SDL_CreateThread(run_controller, "controller",
controller);
if (!controller->thread) {
LOGC("Could not start controller thread");
return SDL_FALSE;
return false;
}
return SDL_TRUE;
if (!receiver_start(&controller->receiver)) {
controller_stop(controller);
SDL_WaitThread(controller->thread, NULL);
return false;
}
return true;
}
void controller_stop(struct controller *controller) {
void
controller_stop(struct controller *controller) {
mutex_lock(controller->mutex);
controller->stopped = SDL_TRUE;
cond_signal(controller->event_cond);
controller->stopped = true;
cond_signal(controller->msg_cond);
mutex_unlock(controller->mutex);
}
void controller_join(struct controller *controller) {
void
controller_join(struct controller *controller) {
SDL_WaitThread(controller->thread, NULL);
receiver_join(&controller->receiver);
}

View File

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

View File

@ -1,182 +0,0 @@
#include "convert.h"
#define MAP(FROM, TO) case FROM: *to = TO; return SDL_TRUE
#define FAIL default: return SDL_FALSE
static SDL_bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP);
FAIL;
}
}
static enum android_metastate autocomplete_metastate(enum android_metastate metastate) {
// fill dependant flags
if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
metastate |= AMETA_SHIFT_ON;
}
if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) {
metastate |= AMETA_CTRL_ON;
}
if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) {
metastate |= AMETA_ALT_ON;
}
if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) {
metastate |= AMETA_META_ON;
}
return metastate;
}
static enum android_metastate convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) {
metastate |= AMETA_SHIFT_LEFT_ON;
}
if (mod & KMOD_RSHIFT) {
metastate |= AMETA_SHIFT_RIGHT_ON;
}
if (mod & KMOD_LCTRL) {
metastate |= AMETA_CTRL_LEFT_ON;
}
if (mod & KMOD_RCTRL) {
metastate |= AMETA_CTRL_RIGHT_ON;
}
if (mod & KMOD_LALT) {
metastate |= AMETA_ALT_LEFT_ON;
}
if (mod & KMOD_RALT) {
metastate |= AMETA_ALT_RIGHT_ON;
}
if (mod & KMOD_LGUI) { // Windows key
metastate |= AMETA_META_LEFT_ON;
}
if (mod & KMOD_RGUI) { // Windows key
metastate |= AMETA_META_RIGHT_ON;
}
if (mod & KMOD_NUM) {
metastate |= AMETA_NUM_LOCK_ON;
}
if (mod & KMOD_CAPS) {
metastate |= AMETA_CAPS_LOCK_ON;
}
if (mod & KMOD_MODE) { // Alt Gr
// no mapping?
}
// fill the dependent fields
return autocomplete_metastate(metastate);
}
static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to) {
switch (from) {
MAP(SDLK_RETURN, AKEYCODE_ENTER);
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB);
MAP(SDLK_HOME, AKEYCODE_HOME);
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
MAP(SDLK_END, AKEYCODE_MOVE_END);
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
FAIL;
}
}
static SDL_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 convert_mouse_buttons(Uint32 state) {
enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) {
buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
}
if (state & SDL_BUTTON_RMASK) {
buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
}
if (state & SDL_BUTTON_MMASK) {
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
}
if (state & SDL_BUTTON_X1) {
buttons |= AMOTION_EVENT_BUTTON_BACK;
}
if (state & SDL_BUTTON_X2) {
buttons |= AMOTION_EVENT_BUTTON_FORWARD;
}
return buttons;
}
SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_KEYCODE;
if (!convert_keycode_action(from->type, &to->keycode_event.action)) {
return SDL_FALSE;
}
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode)) {
return SDL_FALSE;
}
to->keycode_event.metastate = convert_meta_state(from->keysym.mod);
return SDL_TRUE;
}
SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
if (!convert_mouse_action(from->type, &to->mouse_event.action)) {
return SDL_FALSE;
}
to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
return SDL_TRUE;
}
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_MOUSE;
to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
to->mouse_event.buttons = convert_mouse_buttons(from->state);
to->mouse_event.position.screen_size = screen_size;
to->mouse_event.position.point.x = from->x;
to->mouse_event.position.point.y = from->y;
return SDL_TRUE;
}
SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_event *to) {
to->type = CONTROL_EVENT_TYPE_SCROLL;
to->scroll_event.position = position;
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
// SDL behavior seems inconsistent between horizontal and vertical scrolling
// so reverse the horizontal
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
to->scroll_event.hscroll = -mul * from->x;
to->scroll_event.vscroll = mul * from->y;
return SDL_TRUE;
}

View File

@ -1,35 +0,0 @@
#ifndef CONVERT_H
#define CONVERT_H
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_events.h>
#include "controlevent.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;
};
SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
struct control_event *to);
SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
struct size screen_size,
struct control_event *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
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
struct size screen_size,
struct control_event *to);
// on Android, a scroll event requires the current mouse position
SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
struct position position,
struct control_event *to);
#endif

View File

@ -1,28 +1,27 @@
#include "decoder.h"
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "config.h"
#include "compat.h"
#include "events.h"
#include "frames.h"
#include "lockutil.h"
#include "log.h"
#define BUFSIZE 0x10000
static int read_packet(void *opaque, uint8_t *buf, int buf_size) {
struct decoder *decoder = opaque;
return net_recv(decoder->video_socket, buf, buf_size);
}
#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 push_frame(struct decoder *decoder) {
SDL_bool previous_frame_consumed = frames_offer_decoded_frame(decoder->frames);
if (!previous_frame_consumed) {
static void
push_frame(struct decoder *decoder) {
bool previous_frame_skipped;
video_buffer_offer_decoded_frame(decoder->video_buffer,
&previous_frame_skipped);
if (previous_frame_skipped) {
// the previous EVENT_NEW_FRAME will consume this frame
return;
}
@ -32,143 +31,71 @@ static void push_frame(struct decoder *decoder) {
SDL_PushEvent(&new_frame_event);
}
static void notify_stopped(void) {
SDL_Event stop_event;
stop_event.type = EVENT_DECODER_STOPPED;
SDL_PushEvent(&stop_event);
void
decoder_init(struct decoder *decoder, struct video_buffer *vb) {
decoder->video_buffer = vb;
}
static int run_decoder(void *data) {
struct decoder *decoder = data;
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto run_end;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
bool
decoder_open(struct decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx = avcodec_alloc_context3(codec);
if (!decoder->codec_ctx) {
LOGC("Could not allocate decoder context");
goto run_end;
return false;
}
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Could not open H.264 codec");
goto run_finally_free_codec_ctx;
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
LOGE("Could not open codec");
avcodec_free_context(&decoder->codec_ctx);
return false;
}
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto run_finally_close_codec;
}
return true;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto run_finally_free_format_ctx;
}
void
decoder_close(struct decoder *decoder) {
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
}
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL);
if (!avio_ctx) {
LOGC("Could not allocate avio context");
// avformat_open_input takes ownership of 'buffer'
// so only free the buffer before avformat_open_input()
av_free(buffer);
goto run_finally_free_format_ctx;
}
format_ctx->pb = avio_ctx;
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
LOGE("Could not open video stream");
goto run_finally_free_avio_ctx;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (!av_read_frame(format_ctx, &packet)) {
bool
decoder_push(struct decoder *decoder, const AVPacket *packet) {
// the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0)
int ret;
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
goto run_quit;
}
ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame);
if (!ret) {
// a frame was received
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
av_packet_unref(&packet);
goto run_quit;
}
#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
int ret;
if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
return false;
}
ret = avcodec_receive_frame(decoder->codec_ctx,
decoder->video_buffer->decoding_frame);
if (!ret) {
// a frame was received
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
return false;
}
#else
while (packet.size > 0) {
int got_picture;
int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet);
if (len < 0) {
LOGE("Could not decode video packet: %d", len);
goto run_quit;
}
if (got_picture) {
push_frame(decoder);
}
packet.size -= len;
packet.data += len;
}
int got_picture;
int len = avcodec_decode_video2(decoder->codec_ctx,
decoder->video_buffer->decoding_frame,
&got_picture,
packet);
if (len < 0) {
LOGE("Could not decode video packet: %d", len);
return false;
}
if (got_picture) {
push_frame(decoder);
}
#endif
av_packet_unref(&packet);
if (avio_ctx->eof_reached) {
break;
}
}
LOGD("End of frames");
run_quit:
avformat_close_input(&format_ctx);
run_finally_free_avio_ctx:
av_freep(&avio_ctx);
run_finally_free_format_ctx:
avformat_free_context(format_ctx);
run_finally_close_codec:
avcodec_close(codec_ctx);
run_finally_free_codec_ctx:
avcodec_free_context(&codec_ctx);
notify_stopped();
run_end:
return 0;
return true;
}
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) {
decoder->frames = frames;
decoder->video_socket = video_socket;
}
SDL_bool decoder_start(struct decoder *decoder) {
LOGD("Starting decoder thread");
decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder);
if (!decoder->thread) {
LOGC("Could not start decoder thread");
return SDL_FALSE;
}
return SDL_TRUE;
}
void decoder_stop(struct decoder *decoder) {
frames_stop(decoder->frames);
}
void decoder_join(struct decoder *decoder) {
SDL_WaitThread(decoder->thread, NULL);
void
decoder_interrupt(struct decoder *decoder) {
video_buffer_interrupt(decoder->video_buffer);
}

View File

@ -1,23 +1,31 @@
#ifndef DECODER_H
#define DECODER_H
#include <SDL2/SDL_stdinc.h>
#include <SDL2/SDL_thread.h>
#include <stdbool.h>
#include <libavformat/avformat.h>
#include "net.h"
#include "config.h"
struct frames;
struct video_buffer;
struct decoder {
struct frames *frames;
socket_t video_socket;
SDL_Thread *thread;
SDL_mutex *mutex;
struct video_buffer *video_buffer;
AVCodecContext *codec_ctx;
};
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket);
SDL_bool decoder_start(struct decoder *decoder);
void decoder_stop(struct decoder *decoder);
void decoder_join(struct decoder *decoder);
void
decoder_init(struct decoder *decoder, struct video_buffer *vb);
bool
decoder_open(struct decoder *decoder, const AVCodec *codec);
void
decoder_close(struct decoder *decoder);
bool
decoder_push(struct decoder *decoder, const AVPacket *packet);
void
decoder_interrupt(struct decoder *decoder);
#endif

View File

@ -1,18 +1,24 @@
#include "device.h"
#include "log.h"
SDL_bool device_read_info(socket_t device_socket, char *device_name, struct size *size) {
#include "config.h"
#include "util/log.h"
bool
device_read_info(socket_t device_socket, char *device_name, struct size *size) {
unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4];
int r = net_recv_all(device_socket, buf, sizeof(buf));
if (r < DEVICE_NAME_FIELD_LENGTH + 4) {
LOGE("Could not retrieve device information");
return SDL_FALSE;
return false;
}
buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; // in case the client sends garbage
// strcpy is safe here, since name contains at least DEVICE_NAME_FIELD_LENGTH bytes
// and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
// in case the client sends garbage
buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
// strcpy is safe here, since name contains at least
// DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
strcpy(device_name, (char *) buf);
size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 1];
size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 3];
return SDL_TRUE;
size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8)
| buf[DEVICE_NAME_FIELD_LENGTH + 1];
size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8)
| buf[DEVICE_NAME_FIELD_LENGTH + 3];
return true;
}

View File

@ -1,14 +1,16 @@
#ifndef DEVICE_H
#define DEVICE_H
#include <SDL2/SDL_stdinc.h>
#include <stdbool.h>
#include "config.h"
#include "common.h"
#include "net.h"
#include "util/net.h"
#define DEVICE_NAME_FIELD_LENGTH 64
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
SDL_bool device_read_info(socket_t device_socket, char *name, struct size *frame_size);
bool
device_read_info(socket_t device_socket, char *device_name, struct size *size);
#endif

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

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

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

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

193
app/src/event_converter.c Normal file
View File

@ -0,0 +1,193 @@
#include "event_converter.h"
#include "config.h"
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP);
FAIL;
}
}
static enum android_metastate
autocomplete_metastate(enum android_metastate metastate) {
// fill dependant flags
if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
metastate |= AMETA_SHIFT_ON;
}
if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) {
metastate |= AMETA_CTRL_ON;
}
if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) {
metastate |= AMETA_ALT_ON;
}
if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) {
metastate |= AMETA_META_ON;
}
return metastate;
}
enum android_metastate
convert_meta_state(SDL_Keymod mod) {
enum android_metastate metastate = 0;
if (mod & KMOD_LSHIFT) {
metastate |= AMETA_SHIFT_LEFT_ON;
}
if (mod & KMOD_RSHIFT) {
metastate |= AMETA_SHIFT_RIGHT_ON;
}
if (mod & KMOD_LCTRL) {
metastate |= AMETA_CTRL_LEFT_ON;
}
if (mod & KMOD_RCTRL) {
metastate |= AMETA_CTRL_RIGHT_ON;
}
if (mod & KMOD_LALT) {
metastate |= AMETA_ALT_LEFT_ON;
}
if (mod & KMOD_RALT) {
metastate |= AMETA_ALT_RIGHT_ON;
}
if (mod & KMOD_LGUI) { // Windows key
metastate |= AMETA_META_LEFT_ON;
}
if (mod & KMOD_RGUI) { // Windows key
metastate |= AMETA_META_RIGHT_ON;
}
if (mod & KMOD_NUM) {
metastate |= AMETA_NUM_LOCK_ON;
}
if (mod & KMOD_CAPS) {
metastate |= AMETA_CAPS_LOCK_ON;
}
if (mod & KMOD_MODE) { // Alt Gr
// no mapping?
}
// fill the dependent fields
return autocomplete_metastate(metastate);
}
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);
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
MAP(SDLK_TAB, AKEYCODE_TAB);
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
MAP(SDLK_HOME, AKEYCODE_MOVE_HOME);
MAP(SDLK_END, AKEYCODE_MOVE_END);
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
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;
}
// if ALT and META are not pressed, also handle letters and space
switch (from) {
MAP(SDLK_a, AKEYCODE_A);
MAP(SDLK_b, AKEYCODE_B);
MAP(SDLK_c, AKEYCODE_C);
MAP(SDLK_d, AKEYCODE_D);
MAP(SDLK_e, AKEYCODE_E);
MAP(SDLK_f, AKEYCODE_F);
MAP(SDLK_g, AKEYCODE_G);
MAP(SDLK_h, AKEYCODE_H);
MAP(SDLK_i, AKEYCODE_I);
MAP(SDLK_j, AKEYCODE_J);
MAP(SDLK_k, AKEYCODE_K);
MAP(SDLK_l, AKEYCODE_L);
MAP(SDLK_m, AKEYCODE_M);
MAP(SDLK_n, AKEYCODE_N);
MAP(SDLK_o, AKEYCODE_O);
MAP(SDLK_p, AKEYCODE_P);
MAP(SDLK_q, AKEYCODE_Q);
MAP(SDLK_r, AKEYCODE_R);
MAP(SDLK_s, AKEYCODE_S);
MAP(SDLK_t, AKEYCODE_T);
MAP(SDLK_u, AKEYCODE_U);
MAP(SDLK_v, AKEYCODE_V);
MAP(SDLK_w, AKEYCODE_W);
MAP(SDLK_x, AKEYCODE_X);
MAP(SDLK_y, AKEYCODE_Y);
MAP(SDLK_z, AKEYCODE_Z);
MAP(SDLK_SPACE, AKEYCODE_SPACE);
FAIL;
}
}
enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) {
enum android_motionevent_buttons buttons = 0;
if (state & SDL_BUTTON_LMASK) {
buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
}
if (state & SDL_BUTTON_RMASK) {
buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
}
if (state & SDL_BUTTON_MMASK) {
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
}
if (state & SDL_BUTTON_X1MASK) {
buttons |= AMOTION_EVENT_BUTTON_BACK;
}
if (state & SDL_BUTTON_X2MASK) {
buttons |= AMOTION_EVENT_BUTTON_FORWARD;
}
return buttons;
}
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;
}
}
bool
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;
}
}

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,3 +1,3 @@
#define EVENT_NEW_SESSION SDL_USEREVENT
#define EVENT_NEW_FRAME (SDL_USEREVENT + 1)
#define EVENT_DECODER_STOPPED (SDL_USEREVENT + 2)
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2)

191
app/src/file_handler.c Normal file
View File

@ -0,0 +1,191 @@
#include "file_handler.h"
#include <assert.h>
#include <string.h>
#include "config.h"
#include "command.h"
#include "util/lock.h"
#include "util/log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/"
static void
file_handler_request_destroy(struct file_handler_request *req) {
SDL_free(req->file);
}
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target) {
cbuf_init(&file_handler->queue);
if (!(file_handler->mutex = SDL_CreateMutex())) {
return false;
}
if (!(file_handler->event_cond = SDL_CreateCond())) {
SDL_DestroyMutex(file_handler->mutex);
return false;
}
if (serial) {
file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) {
LOGW("Could not strdup serial");
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex);
return false;
}
} else {
file_handler->serial = NULL;
}
// lazy initialization
file_handler->initialized = false;
file_handler->stopped = false;
file_handler->current_process = PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
return true;
}
void
file_handler_destroy(struct file_handler *file_handler) {
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex);
SDL_free(file_handler->serial);
struct file_handler_request req;
while (cbuf_take(&file_handler->queue, &req)) {
file_handler_request_destroy(&req);
}
}
static process_t
install_apk(const char *serial, const char *file) {
return adb_install(serial, file);
}
static process_t
push_file(const char *serial, const char *file, const char *push_target) {
return adb_push(serial, file, push_target);
}
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, char *file) {
// start file_handler if it's used for the first time
if (!file_handler->initialized) {
if (!file_handler_start(file_handler)) {
return false;
}
file_handler->initialized = true;
}
LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push",
file);
struct file_handler_request req = {
.action = action,
.file = file,
};
mutex_lock(file_handler->mutex);
bool was_empty = cbuf_is_empty(&file_handler->queue);
bool res = cbuf_push(&file_handler->queue, req);
if (was_empty) {
cond_signal(file_handler->event_cond);
}
mutex_unlock(file_handler->mutex);
return res;
}
static int
run_file_handler(void *data) {
struct file_handler *file_handler = data;
for (;;) {
mutex_lock(file_handler->mutex);
file_handler->current_process = PROCESS_NONE;
while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) {
cond_wait(file_handler->event_cond, file_handler->mutex);
}
if (file_handler->stopped) {
// stop immediately, do not process further events
mutex_unlock(file_handler->mutex);
break;
}
struct file_handler_request req;
bool non_empty = cbuf_take(&file_handler->queue, &req);
assert(non_empty);
(void) non_empty;
process_t process;
if (req.action == ACTION_INSTALL_APK) {
LOGI("Installing %s...", req.file);
process = install_apk(file_handler->serial, req.file);
} else {
LOGI("Pushing %s...", req.file);
process = push_file(file_handler->serial, req.file,
file_handler->push_target);
}
file_handler->current_process = process;
mutex_unlock(file_handler->mutex);
if (req.action == ACTION_INSTALL_APK) {
if (process_check_success(process, "adb install")) {
LOGI("%s successfully installed", req.file);
} else {
LOGE("Failed to install %s", req.file);
}
} else {
if (process_check_success(process, "adb push")) {
LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target);
} else {
LOGE("Failed to push %s to %s", req.file,
file_handler->push_target);
}
}
file_handler_request_destroy(&req);
}
return 0;
}
bool
file_handler_start(struct file_handler *file_handler) {
LOGD("Starting file_handler thread");
file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler",
file_handler);
if (!file_handler->thread) {
LOGC("Could not start file_handler thread");
return false;
}
return true;
}
void
file_handler_stop(struct file_handler *file_handler) {
mutex_lock(file_handler->mutex);
file_handler->stopped = true;
cond_signal(file_handler->event_cond);
if (file_handler->current_process != PROCESS_NONE) {
if (!cmd_terminate(file_handler->current_process)) {
LOGW("Could not terminate install process");
}
cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE;
}
mutex_unlock(file_handler->mutex);
}
void
file_handler_join(struct file_handler *file_handler) {
SDL_WaitThread(file_handler->thread, NULL);
}

58
app/src/file_handler.h Normal file
View File

@ -0,0 +1,58 @@
#ifndef FILE_HANDLER_H
#define FILE_HANDLER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "command.h"
#include "util/cbuf.h"
typedef enum {
ACTION_INSTALL_APK,
ACTION_PUSH_FILE,
} file_handler_action_t;
struct file_handler_request {
file_handler_action_t action;
char *file;
};
struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler {
char *serial;
const char *push_target;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *event_cond;
bool stopped;
bool initialized;
process_t current_process;
struct file_handler_request_queue queue;
};
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target);
void
file_handler_destroy(struct file_handler *file_handler);
bool
file_handler_start(struct file_handler *file_handler);
void
file_handler_stop(struct file_handler *file_handler);
void
file_handler_join(struct file_handler *file_handler);
// take ownership of file, and will SDL_free() it
bool
file_handler_request(struct file_handler *file_handler,
file_handler_action_t action,
char *file);
#endif

180
app/src/fps_counter.c Normal file
View File

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

57
app/src/fps_counter.h Normal file
View File

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

View File

@ -1,62 +0,0 @@
#include "fpscounter.h"
#include <SDL2/SDL_timer.h>
#include "log.h"
void fps_counter_init(struct fps_counter *counter) {
counter->started = SDL_FALSE;
// no need to initialize the other fields, they are meaningful only when
// started is true
}
void fps_counter_start(struct fps_counter *counter) {
counter->started = SDL_TRUE;
counter->slice_start = SDL_GetTicks();
counter->nr_rendered = 0;
#ifdef SKIP_FRAMES
counter->nr_skipped = 0;
#endif
}
void fps_counter_stop(struct fps_counter *counter) {
counter->started = SDL_FALSE;
}
static void display_fps(struct fps_counter *counter) {
#ifdef SKIP_FRAMES
if (counter->nr_skipped) {
LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, counter->nr_skipped);
} else {
#endif
LOGI("%d fps", counter->nr_rendered);
#ifdef SKIP_FRAMES
}
#endif
}
static void check_expired(struct fps_counter *counter) {
Uint32 now = SDL_GetTicks();
if (now - counter->slice_start >= 1000) {
display_fps(counter);
// add a multiple of one second
Uint32 elapsed_slices = (now - counter->slice_start) / 1000;
counter->slice_start += 1000 * elapsed_slices;
counter->nr_rendered = 0;
#ifdef SKIP_FRAMES
counter->nr_skipped = 0;
#endif
}
}
void fps_counter_add_rendered_frame(struct fps_counter *counter) {
check_expired(counter);
++counter->nr_rendered;
}
#ifdef SKIP_FRAMES
void fps_counter_add_skipped_frame(struct fps_counter *counter) {
check_expired(counter);
++counter->nr_skipped;
}
#endif

View File

@ -1,26 +0,0 @@
#ifndef FPSCOUNTER_H
#define FPSCOUNTER_H
#include <SDL2/SDL_stdinc.h>
#include "config.h"
struct fps_counter {
SDL_bool started;
Uint32 slice_start; // initialized by SDL_GetTicks()
int nr_rendered;
#ifdef SKIP_FRAMES
int nr_skipped;
#endif
};
void fps_counter_init(struct fps_counter *counter);
void fps_counter_start(struct fps_counter *counter);
void fps_counter_stop(struct fps_counter *counter);
void fps_counter_add_rendered_frame(struct fps_counter *counter);
#ifdef SKIP_FRAMES
void fps_counter_add_skipped_frame(struct fps_counter *counter);
#endif
#endif

View File

@ -1,110 +0,0 @@
#include "frames.h"
#include <SDL2/SDL_assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "lockutil.h"
#include "log.h"
SDL_bool frames_init(struct frames *frames) {
if (!(frames->decoding_frame = av_frame_alloc())) {
goto error_0;
}
if (!(frames->rendering_frame = av_frame_alloc())) {
goto error_1;
}
if (!(frames->mutex = SDL_CreateMutex())) {
goto error_2;
}
#ifndef SKIP_FRAMES
if (!(frames->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(frames->mutex);
goto error_2;
}
frames->stopped = SDL_FALSE;
#endif
// there is initially no rendering frame, so consider it has already been
// consumed
frames->rendering_frame_consumed = SDL_TRUE;
fps_counter_init(&frames->fps_counter);
return SDL_TRUE;
error_2:
av_frame_free(&frames->rendering_frame);
error_1:
av_frame_free(&frames->decoding_frame);
error_0:
return SDL_FALSE;
}
void frames_destroy(struct frames *frames) {
#ifndef SKIP_FRAMES
SDL_DestroyCond(frames->rendering_frame_consumed_cond);
#endif
SDL_DestroyMutex(frames->mutex);
av_frame_free(&frames->rendering_frame);
av_frame_free(&frames->decoding_frame);
}
static void frames_swap(struct frames *frames) {
AVFrame *tmp = frames->decoding_frame;
frames->decoding_frame = frames->rendering_frame;
frames->rendering_frame = tmp;
}
SDL_bool frames_offer_decoded_frame(struct frames *frames) {
mutex_lock(frames->mutex);
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then the decoder must wait for the current
// frame to be consumed
while (!frames->rendering_frame_consumed && !frames->stopped) {
cond_wait(frames->rendering_frame_consumed_cond, frames->mutex);
}
#else
if (frames->fps_counter.started && !frames->rendering_frame_consumed) {
fps_counter_add_skipped_frame(&frames->fps_counter);
}
#endif
frames_swap(frames);
SDL_bool previous_frame_consumed = frames->rendering_frame_consumed;
frames->rendering_frame_consumed = SDL_FALSE;
mutex_unlock(frames->mutex);
return previous_frame_consumed;
}
const AVFrame *frames_consume_rendered_frame(struct frames *frames) {
SDL_assert(!frames->rendering_frame_consumed);
frames->rendering_frame_consumed = SDL_TRUE;
if (frames->fps_counter.started) {
fps_counter_add_rendered_frame(&frames->fps_counter);
}
#ifndef SKIP_FRAMES
// if SKIP_FRAMES is disabled, then notify the decoder the current frame is
// consumed, so that it may push a new one
cond_signal(frames->rendering_frame_consumed_cond);
#endif
return frames->rendering_frame;
}
void frames_stop(struct frames *frames) {
#ifdef SKIP_FRAMES
(void) frames; // unused
#else
mutex_lock(frames->mutex);
frames->stopped = SDL_TRUE;
mutex_unlock(frames->mutex);
// wake up blocking wait
cond_signal(frames->rendering_frame_consumed_cond);
#endif
}

View File

@ -1,42 +0,0 @@
#ifndef FRAMES_H
#define FRAMES_H
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_stdinc.h>
#include "config.h"
#include "fpscounter.h"
// forward declarations
typedef struct AVFrame AVFrame;
struct frames {
AVFrame *decoding_frame;
AVFrame *rendering_frame;
SDL_mutex *mutex;
#ifndef SKIP_FRAMES
SDL_bool stopped;
SDL_cond *rendering_frame_consumed_cond;
#endif
SDL_bool rendering_frame_consumed;
struct fps_counter fps_counter;
};
SDL_bool frames_init(struct frames *frames);
void frames_destroy(struct frames *frames);
// set the decoder frame as ready for rendering
// this function locks frames->mutex during its execution
// returns true if the previous frame had been consumed
SDL_bool frames_offer_decoded_frame(struct frames *frames);
// mark the rendering frame as consumed and return it
// MUST be called with frames->mutex locked!!!
// the caller is expected to render the returned frame to some texture before
// unlocking frames->mutex
const AVFrame *frames_consume_rendered_frame(struct frames *frames);
// wake up and avoid any blocking call
void frames_stop(struct frames *frames);
#endif

605
app/src/input_manager.c Normal file
View File

@ -0,0 +1,605 @@
#include "input_manager.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)
//
// See my question:
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
static void
convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
SDL_Rect viewport;
float scale_x, scale_y;
SDL_RenderGetViewport(renderer, &viewport);
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
*x = (int) (*x / scale_x) - viewport.x;
*y = (int) (*y / scale_y) - viewport.y;
}
static struct point
get_mouse_point(struct screen *screen) {
int x;
int y;
SDL_GetMouseState(&x, &y);
convert_to_renderer_coordinates(screen->renderer, &x, &y);
return (struct point) {
.x = x,
.y = y,
};
}
static const int ACTION_DOWN = 1;
static const int ACTION_UP = 1 << 1;
static void
send_keycode(struct controller *controller, enum android_keycode keycode,
int actions, const char *name) {
// send DOWN event
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
msg.inject_keycode.keycode = keycode;
msg.inject_keycode.metastate = 0;
if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject %s (DOWN)'", name);
return;
}
}
if (actions & ACTION_UP) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject %s (UP)'", name);
}
}
}
static inline void
action_home(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_HOME, actions, "HOME");
}
static inline void
action_back(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_BACK, actions, "BACK");
}
static inline void
action_app_switch(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH");
}
static inline void
action_power(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_POWER, actions, "POWER");
}
static inline void
action_volume_up(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP");
}
static inline void
action_volume_down(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN");
}
static inline void
action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
}
// turn the screen on if it was off, press BACK otherwise
static void
press_back_or_turn_screen_on(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'press back or turn screen on'");
}
}
static void
expand_notification_panel(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'expand notification panel'");
}
}
static void
collapse_notification_panel(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'collapse notification panel'");
}
}
static void
request_device_clipboard(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device clipboard");
}
}
static void
set_device_clipboard(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
// empty text
SDL_free(text);
return;
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
msg.set_clipboard.text = text;
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Could not request 'set device clipboard'");
}
}
static void
set_screen_power_mode(struct controller *controller,
enum screen_power_mode mode) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = mode;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
static void
switch_fps_counter_state(struct fps_counter *fps_counter) {
// the started state can only be written from the current thread, so there
// is no ToCToU issue
if (fps_counter_is_started(fps_counter)) {
fps_counter_stop(fps_counter);
LOGI("FPS counter stopped");
} else {
if (fps_counter_start(fps_counter)) {
LOGI("FPS counter started");
} else {
LOGE("FPS counter starting failed");
}
}
}
static void
clipboard_paste(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
// empty text
SDL_free(text);
return;
}
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = text;
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Could not request 'paste clipboard'");
}
}
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);
if (!msg.inject_text.text) {
LOGW("Could not strdup input text");
return;
}
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 *im,
const SDL_KeyboardEvent *event,
bool control) {
// control: indicates the state of the command-line option --no-control
// ctrl: the Ctrl key
bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
// use Cmd on macOS, Ctrl on other platforms
#ifdef __APPLE__
bool cmd = !ctrl && meta;
#else
if (meta) {
// no shortcuts involve Meta on platforms other than macOS, and it must
// not be forwarded to the device
return;
}
bool cmd = ctrl; // && !meta, already guaranteed
#endif
if (alt) {
// no shortcuts involve Alt, and it must not be forwarded to the device
return;
}
struct controller *controller = im->controller;
// capture all Ctrl events
if (ctrl || cmd) {
SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN;
int action = down ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) {
case SDLK_h:
// Ctrl+h on all platform, since Cmd+h is already captured by
// the system on macOS to hide the window
if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action);
}
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
if (control && cmd && !shift && !repeat) {
action_back(controller, action);
}
return;
case SDLK_s:
if (control && cmd && !shift && !repeat) {
action_app_switch(controller, action);
}
return;
case SDLK_m:
// Ctrl+m on all platform, since Cmd+m is already captured by
// the system on macOS to minimize the window
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action);
}
return;
case SDLK_p:
if (control && cmd && !shift && !repeat) {
action_power(controller, action);
}
return;
case SDLK_o:
if (control && cmd && !shift && down) {
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
}
return;
case SDLK_DOWN:
if (control && cmd && !shift) {
// forward repeated events
action_volume_down(controller, action);
}
return;
case SDLK_UP:
if (control && cmd && !shift) {
// forward repeated events
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);
}
return;
case SDLK_v:
if (control && cmd && !repeat && down) {
if (shift) {
// store the text in the device clipboard
set_device_clipboard(controller);
} else {
// inject the text as input events
clipboard_paste(controller);
}
}
return;
case SDLK_f:
if (!shift && cmd && !repeat && down) {
screen_switch_fullscreen(im->screen);
}
return;
case SDLK_x:
if (!shift && cmd && !repeat && down) {
screen_resize_to_fit(im->screen);
}
return;
case SDLK_g:
if (!shift && cmd && !repeat && down) {
screen_resize_to_pixel_perfect(im->screen);
}
return;
case SDLK_i:
if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter =
im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter);
}
return;
case SDLK_n:
if (control && cmd && !repeat && down) {
if (shift) {
collapse_notification_panel(controller);
} else {
expand_notification_panel(controller);
}
}
return;
case SDLK_r:
if (control && cmd && !shift && !repeat && down) {
rotate_device(controller);
}
return;
}
return;
}
if (!control) {
return;
}
struct control_msg 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 *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 (convert_mouse_motion(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'");
}
}
}
static bool
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_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(im->controller);
return;
}
if (control && event->button == SDL_BUTTON_MIDDLE) {
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(im, event->x, event->y);
if (outside) {
screen_resize_to_fit(im->screen);
return;
}
}
// otherwise, send the click event to the device
}
if (!control) {
return;
}
struct control_msg msg;
if (convert_mouse_button(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'");
}
}
}
static bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
struct control_msg *to) {
struct position position = {
.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 (convert_mouse_wheel(event, im->screen, &msg)) {
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'");
}
}
}

46
app/src/input_manager.h Normal file
View File

@ -0,0 +1,46 @@
#ifndef INPUTMANAGER_H
#define INPUTMANAGER_H
#include <stdbool.h>
#include "config.h"
#include "common.h"
#include "controller.h"
#include "fps_counter.h"
#include "video_buffer.h"
#include "screen.h"
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 *im,
const SDL_TextInputEvent *event);
void
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event,
bool control);
void
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event);
void
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 *im,
const SDL_MouseWheelEvent *event);
#endif

View File

@ -1,277 +0,0 @@
#include "inputmanager.h"
#include "convert.h"
#include "lockutil.h"
#include "log.h"
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer coordinates (as provided in SDL mouse events)
//
// See my question:
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
static void convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
SDL_Rect viewport;
float scale_x, scale_y;
SDL_RenderGetViewport(renderer, &viewport);
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
*x = (int) (*x / scale_x) - viewport.x;
*y = (int) (*y / scale_y) - viewport.y;
}
static struct point get_mouse_point(struct screen *screen) {
int x;
int y;
SDL_GetMouseState(&x, &y);
convert_to_renderer_coordinates(screen->renderer, &x, &y);
SDL_assert_release(x >= 0 && x < 0x10000 && y >= 0 && y < 0x10000);
return (struct point) {
.x = (Uint16) x,
.y = (Uint16) y,
};
}
static SDL_bool is_ctrl_down(void) {
const Uint8 *state = SDL_GetKeyboardState(NULL);
return state[SDL_SCANCODE_LCTRL] || state[SDL_SCANCODE_RCTRL];
}
static void send_keycode(struct controller *controller, enum android_keycode keycode, const char *name) {
// send DOWN event
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_KEYCODE;
control_event.keycode_event.action = AKEY_EVENT_ACTION_DOWN;
control_event.keycode_event.keycode = keycode;
control_event.keycode_event.metastate = 0;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (DOWN)", name);
return;
}
// send UP event
control_event.keycode_event.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot send %s (UP)", name);
}
}
static inline void action_home(struct controller *controller) {
send_keycode(controller, AKEYCODE_HOME, "HOME");
}
static inline void action_back(struct controller *controller) {
send_keycode(controller, AKEYCODE_BACK, "BACK");
}
static inline void action_app_switch(struct controller *controller) {
send_keycode(controller, AKEYCODE_APP_SWITCH, "APP_SWITCH");
}
static inline void action_power(struct controller *controller) {
send_keycode(controller, AKEYCODE_POWER, "POWER");
}
static inline void action_volume_up(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_UP, "VOLUME_UP");
}
static inline void action_volume_down(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN");
}
static inline void action_menu(struct controller *controller) {
send_keycode(controller, AKEYCODE_MENU, "MENU");
}
// turn the screen on if it was off, press BACK otherwise
static void press_back_or_turn_screen_on(struct controller *controller) {
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action = CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON;
if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on");
}
}
static void switch_fps_counter_state(struct frames *frames) {
mutex_lock(frames->mutex);
if (frames->fps_counter.started) {
LOGI("FPS counter stopped");
fps_counter_stop(&frames->fps_counter);
} else {
LOGI("FPS counter started");
fps_counter_start(&frames->fps_counter);
}
mutex_unlock(frames->mutex);
}
static void clipboard_paste(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Cannot get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
// empty text
SDL_free(text);
return;
}
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
control_event.text_event.text = text;
if (!controller_push_event(controller, &control_event)) {
SDL_free(text);
LOGW("Cannot send clipboard paste event");
}
}
void input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event) {
if (is_ctrl_down()) {
switch (event->text[0]) {
case '+':
action_volume_up(input_manager->controller);
break;
case '-':
action_volume_down(input_manager->controller);
break;
}
return;
}
struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_TEXT;
control_event.text_event.text = SDL_strdup(event->text);
if (!control_event.text_event.text) {
LOGW("Cannot strdup input text");
return;
}
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send text event");
}
}
void input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event) {
SDL_bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
// capture all Ctrl events
if (ctrl) {
SDL_bool repeat = event->repeat;
// only consider keydown events, and ignore repeated events
if (repeat || event->type != SDL_KEYDOWN) {
return;
}
SDL_bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
if (shift) {
// currently, there is no shortcut implying SHIFT
return;
}
SDL_Keycode keycode = event->keysym.sym;
switch (keycode) {
case SDLK_h:
action_home(input_manager->controller);
return;
case SDLK_b: // fall-through
case SDLK_BACKSPACE:
action_back(input_manager->controller);
return;
case SDLK_s:
action_app_switch(input_manager->controller);
return;
case SDLK_m:
action_menu(input_manager->controller);
return;
case SDLK_p:
action_power(input_manager->controller);
return;
case SDLK_v:
clipboard_paste(input_manager->controller);
return;
case SDLK_f:
screen_switch_fullscreen(input_manager->screen);
return;
case SDLK_x:
screen_resize_to_fit(input_manager->screen);
return;
case SDLK_g:
screen_resize_to_pixel_perfect(input_manager->screen);
return;
case SDLK_i:
switch_fps_counter_state(input_manager->frames);
return;
}
return;
}
struct control_event control_event;
if (input_key_from_sdl_to_android(event, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send control event");
}
}
}
void input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
// do not send motion events when no button is pressed
return;
}
struct control_event control_event;
if (mouse_motion_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse motion event");
}
}
}
void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event) {
if (event->type == SDL_MOUSEBUTTONDOWN) {
if (event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(input_manager->controller);
return;
}
if (event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller);
return;
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
SDL_bool outside_device_screen =
event->x < 0 || event->x >= input_manager->screen->frame_size.width ||
event->y < 0 || event->y >= input_manager->screen->frame_size.height;
if (outside_device_screen) {
screen_resize_to_fit(input_manager->screen);
return;
}
// otherwise, send the click event to the device
}
};
struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse button event");
}
}
}
void input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event) {
struct position position = {
.screen_size = input_manager->screen->frame_size,
.point = get_mouse_point(input_manager->screen),
};
struct control_event control_event;
if (mouse_wheel_from_sdl_to_android(event, position, &control_event)) {
if (!controller_push_event(input_manager->controller, &control_event)) {
LOGW("Cannot send mouse wheel event");
}
}
}

View File

@ -1,27 +0,0 @@
#ifndef INPUTMANAGER_H
#define INPUTMANAGER_H
#include "common.h"
#include "controller.h"
#include "fpscounter.h"
#include "frames.h"
#include "screen.h"
struct input_manager {
struct controller *controller;
struct frames *frames;
struct screen *screen;
};
void input_manager_process_text_input(struct input_manager *input_manager,
const SDL_TextInputEvent *event);
void input_manager_process_key(struct input_manager *input_manager,
const SDL_KeyboardEvent *event);
void input_manager_process_mouse_motion(struct input_manager *input_manager,
const SDL_MouseMotionEvent *event);
void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event);
void input_manager_process_mouse_wheel(struct input_manager *input_manager,
const SDL_MouseWheelEvent *event);
#endif

View File

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

View File

@ -1,13 +0,0 @@
#ifndef LOCKUTIL_H
#define LOCKUTIL_H
// forward declarations
typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
void mutex_lock(SDL_mutex *mutex);
void mutex_unlock(SDL_mutex *mutex);
void cond_wait(SDL_cond *cond, SDL_mutex *mutex);
void cond_signal(SDL_cond *cond);
#endif

View File

@ -1,268 +1,59 @@
#include "scrcpy.h"
#include <getopt.h>
#include <stdbool.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
#include "config.h"
#include "log.h"
#include "cli.h"
#include "compat.h"
#include "util/log.h"
struct args {
const char *serial;
SDL_bool help;
SDL_bool version;
SDL_bool show_touches;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
};
static void usage(const char *arg0) {
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"
" -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"
" -p, --port port\n"
" Set the TCP port the client listens on.\n"
" Default is %d.\n"
"\n"
" -s, --serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
" -t, --show-touches\n"
" Enable \"show touches\" on start, disable on quit.\n"
" It only shows physical touches (not clicks from scrcpy).\n"
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
"Shortcuts:\n"
"\n"
" Ctrl+f\n"
" switch fullscreen mode\n"
"\n"
" Ctrl+g\n"
" resize window to 1:1 (pixel-perfect)\n"
"\n"
" Ctrl+x\n"
" Double-click on black borders\n"
" resize window to remove black borders\n"
"\n"
" Ctrl+h\n"
" Home\n"
" Middle-click\n"
" click on HOME\n"
"\n"
" Ctrl+b\n"
" Ctrl+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n"
"\n"
" Ctrl+s\n"
" click on APP_SWITCH\n"
"\n"
" Ctrl+m\n"
" click on MENU\n"
"\n"
" Ctrl+'+'\n"
" click on VOLUME_UP\n"
"\n"
" Ctrl+'-'\n"
" click on VOLUME_DOWN\n"
"\n"
" Ctrl+p\n"
" click on POWER (turn screen on/off)\n"
"\n"
" Right-click (when screen is off)\n"
" turn screen on\n"
"\n"
" Ctrl+v\n"
" paste computer clipboard to device\n"
"\n"
" Ctrl+i\n"
" enable/disable FPS counter (print frames/second in logs)\n"
"\n",
arg0,
DEFAULT_BIT_RATE,
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT);
}
static void print_version(void) {
fprintf(stderr, "scrcpy v%s\n\n", SCRCPY_VERSION);
static void
print_version(void) {
fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION);
fprintf(stderr, "dependencies:\n");
fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL);
fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, LIBAVCODEC_VERSION_MICRO);
fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO);
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO);
fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION,
SDL_PATCHLEVEL);
fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR,
LIBAVCODEC_VERSION_MINOR,
LIBAVCODEC_VERSION_MICRO);
fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR,
LIBAVFORMAT_VERSION_MINOR,
LIBAVFORMAT_VERSION_MICRO);
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
LIBAVUTIL_VERSION_MINOR,
LIBAVUTIL_VERSION_MICRO);
}
static SDL_bool parse_bit_rate(char *optarg, Uint32 *bit_rate) {
char *endptr;
if (*optarg == '\0') {
LOGE("Bit-rate parameter is empty");
return SDL_FALSE;
}
long value = strtol(optarg, &endptr, 0);
int mul = 1;
if (*endptr != '\0') {
if (optarg == endptr) {
LOGE("Invalid bit-rate: %s", optarg);
return SDL_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 SDL_FALSE;
}
}
if (value < 0 || ((Uint32) -1) / mul < value) {
LOGE("Bitrate must be positive and less than 2^32: %s", optarg);
return SDL_FALSE;
}
*bit_rate = (Uint32) value * mul;
return SDL_TRUE;
}
static SDL_bool parse_max_size(char *optarg, Uint16 *max_size) {
char *endptr;
if (*optarg == '\0') {
LOGE("Max size parameter is empty");
return SDL_FALSE;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid max size: %s", optarg);
return SDL_FALSE;
}
if (value & ~0xffff) {
LOGE("Max size must be between 0 and 65535: %ld", value);
return SDL_FALSE;
}
*max_size = (Uint16) value;
return SDL_TRUE;
}
static SDL_bool parse_port(char *optarg, Uint16 *port) {
char *endptr;
if (*optarg == '\0') {
LOGE("Invalid port parameter is empty");
return SDL_FALSE;
}
long value = strtol(optarg, &endptr, 0);
if (*endptr != '\0') {
LOGE("Invalid port: %s", optarg);
return SDL_FALSE;
}
if (value & ~0xffff) {
LOGE("Port out of range: %ld", value);
return SDL_FALSE;
}
*port = (Uint16) value;
return SDL_TRUE;
}
static SDL_bool parse_args(struct args *args, int argc, char *argv[]) {
static const struct option long_options[] = {
{"bit-rate", required_argument, NULL, 'b'},
{"help", no_argument, NULL, 'h'},
{"max-size", required_argument, NULL, 'm'},
{"port", required_argument, NULL, 'p'},
{"serial", required_argument, NULL, 's'},
{"show-touches", no_argument, NULL, 't'},
{"version", no_argument, NULL, 'v'},
{NULL, 0, NULL, 0 },
};
int c;
while ((c = getopt_long(argc, argv, "b:hm:p:s:tv", long_options, NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &args->bit_rate)) {
return SDL_FALSE;
}
break;
case 'h':
args->help = SDL_TRUE;
break;
case 'm':
if (!parse_max_size(optarg, &args->max_size)) {
return SDL_FALSE;
}
break;
case 'p':
if (!parse_port(optarg, &args->port)) {
return SDL_FALSE;
}
break;
case 's':
args->serial = optarg;
break;
case 't':
args->show_touches = SDL_TRUE;
break;
case 'v':
args->version = SDL_TRUE;
break;
default:
// getopt prints the error message on stderr
return SDL_FALSE;
}
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return SDL_FALSE;
}
return SDL_TRUE;
}
int main(int argc, char *argv[]) {
int
main(int argc, char *argv[]) {
#ifdef __WINDOWS__
// disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL);
setbuf(stderr, NULL);
#endif
struct args args = {
.serial = NULL,
.help = SDL_FALSE,
.version = SDL_FALSE,
.show_touches = SDL_FALSE,
.port = DEFAULT_LOCAL_PORT,
.max_size = DEFAULT_MAX_SIZE,
.bit_rate = DEFAULT_BIT_RATE,
#ifndef NDEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT,
.help = false,
.version = 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;
}
@ -271,26 +62,25 @@ int main(int argc, char *argv[]) {
return 0;
}
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all();
#endif
if (avformat_network_init()) {
return 1;
}
#ifdef BUILD_DEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_options options = {
.serial = args.serial,
.port = args.port,
.max_size = args.max_size,
.bit_rate = args.bit_rate,
.show_touches = args.show_touches,
};
int res = scrcpy(&options) ? 0 : 1;
int res = scrcpy(&args.opts) ? 0 : 1;
avformat_network_deinit(); // ignore failure
#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE)
if (res != 0) {
fprintf(stderr, "Press any key to continue...\n");
getchar();
}
#endif
return res;
}

View File

@ -1,35 +0,0 @@
#ifndef NET_H
#define NET_H
#include <SDL2/SDL_platform.h>
#include <SDL2/SDL_stdinc.h>
#ifdef __WINDOWS__
# include <winsock2.h>
#define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH
typedef SOCKET socket_t;
#else
# include <sys/socket.h>
# define INVALID_SOCKET -1
typedef int socket_t;
#endif
SDL_bool net_init(void);
void net_cleanup(void);
socket_t net_connect(Uint32 addr, Uint16 port);
socket_t net_listen(Uint32 addr, Uint16 port, int backlog);
socket_t net_accept(socket_t server_socket);
// the _all versions wait/retry until len bytes have been written/read
ssize_t net_recv(socket_t socket, void *buf, size_t len);
ssize_t net_recv_all(socket_t socket, void *buf, size_t len);
ssize_t net_send(socket_t socket, void *buf, size_t len);
ssize_t net_send_all(socket_t socket, void *buf, size_t len);
// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
SDL_bool net_shutdown(socket_t socket, int how);
SDL_bool net_close(socket_t socket);
#endif

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

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

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

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

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

378
app/src/recorder.c Normal file
View File

@ -0,0 +1,378 @@
#include "recorder.h"
#include <assert.h>
#include <libavutil/time.h>
#include "config.h"
#include "compat.h"
#include "util/lock.h"
#include "util/log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat *
find_muxer(const char *name) {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
void *opaque = NULL;
#endif
const AVOutputFormat *oformat = NULL;
do {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
oformat = av_muxer_iterate(&opaque);
#else
oformat = av_oformat_next(oformat);
#endif
// until null or with name "mp4"
} while (oformat && strcmp(oformat->name, name));
return oformat;
}
static struct record_packet *
record_packet_new(const AVPacket *packet) {
struct record_packet *rec = SDL_malloc(sizeof(*rec));
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;
}
return rec;
}
static void
record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet);
SDL_free(rec);
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
while (!queue_is_empty(queue)) {
struct record_packet *rec;
queue_take(queue, next, &rec);
record_packet_delete(rec);
}
}
bool
recorder_init(struct recorder *recorder,
const char *filename,
enum recorder_format format,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
return false;
}
recorder->mutex = SDL_CreateMutex();
if (!recorder->mutex) {
LOGC("Could not create mutex");
SDL_free(recorder->filename);
return false;
}
recorder->queue_cond = SDL_CreateCond();
if (!recorder->queue_cond) {
LOGC("Could not create cond");
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
return false;
}
queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
recorder->previous = NULL;
return true;
}
void
recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
}
static const char *
recorder_get_format_name(enum recorder_format format) {
switch (format) {
case RECORDER_FORMAT_MP4: return "mp4";
case RECORDER_FORMAT_MKV: return "matroska";
default: return NULL;
}
}
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
const char *format_name = recorder_get_format_name(recorder->format);
assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
return false;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOGE("Could not allocate output context");
return false;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
// still expects a pointer-to-non-const (it has not be updated accordingly)
// <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);
return false;
}
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = recorder->declared_frame_size.width;
ostream->codecpar->height = recorder->declared_frame_size.height;
#else
ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codec->codec_id = input_codec->id;
ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
ostream->codec->width = recorder->declared_frame_size.width;
ostream->codec->height = recorder->declared_frame_size.height;
#endif
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
avformat_free_context(recorder->ctx);
return false;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
}
void
recorder_close(struct recorder *recorder) {
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);
avformat_free_context(recorder->ctx);
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
}
static bool
recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
return false;
}
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
#else
ostream->codec->extradata = extradata;
ostream->codec->extradata_size = packet->size;
#endif
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
return false;
}
return true;
}
static void
recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
bool
recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = recorder_write_header(recorder, packet);
if (!ok) {
return false;
}
recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
}
static int
run_recorder(void *data) {
struct recorder *recorder = data;
for (;;) {
mutex_lock(recorder->mutex);
while (!recorder->stopped && queue_is_empty(&recorder->queue)) {
cond_wait(recorder->queue_cond, recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && 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;
}
struct record_packet *rec;
queue_take(&recorder->queue, next, &rec);
mutex_unlock(recorder->mutex);
// 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");
mutex_lock(recorder->mutex);
recorder->failed = true;
// discard pending packets
recorder_queue_clear(&recorder->queue);
mutex_unlock(recorder->mutex);
break;
}
}
LOGD("Recorder thread ended");
return 0;
}
bool
recorder_start(struct recorder *recorder) {
LOGD("Starting recorder thread");
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder);
if (!recorder->thread) {
LOGC("Could not start recorder thread");
return false;
}
return true;
}
void
recorder_stop(struct recorder *recorder) {
mutex_lock(recorder->mutex);
recorder->stopped = true;
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
}
void
recorder_join(struct recorder *recorder) {
SDL_WaitThread(recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex);
assert(!recorder->stopped);
if (recorder->failed) {
// reject any new packet (this will stop the stream)
return false;
}
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
return false;
}
queue_push(&recorder->queue, next, rec);
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
return true;
}

72
app/src/recorder.h Normal file
View File

@ -0,0 +1,72 @@
#ifndef RECORDER_H
#define RECORDER_H
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "common.h"
#include "util/queue.h"
enum recorder_format {
RECORDER_FORMAT_AUTO,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV,
};
struct record_packet {
AVPacket packet;
struct record_packet *next;
};
struct recorder_queue QUEUE(struct record_packet);
struct recorder {
char *filename;
enum recorder_format format;
AVFormatContext *ctx;
struct size declared_frame_size;
bool header_written;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *queue_cond;
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
// 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
recorder_init(struct recorder *recorder, const char *filename,
enum recorder_format format, struct size declared_frame_size);
void
recorder_destroy(struct recorder *recorder);
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec);
void
recorder_close(struct recorder *recorder);
bool
recorder_start(struct recorder *recorder);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
#endif

View File

@ -7,34 +7,94 @@
#include <sys/time.h>
#include <SDL2/SDL.h>
#include "config.h"
#include "command.h"
#include "common.h"
#include "compat.h"
#include "controller.h"
#include "decoder.h"
#include "device.h"
#include "events.h"
#include "frames.h"
#include "fpscounter.h"
#include "inputmanager.h"
#include "log.h"
#include "lockutil.h"
#include "net.h"
#include "file_handler.h"
#include "fps_counter.h"
#include "input_manager.h"
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "tinyxpm.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;
static struct frames frames;
static struct fps_counter fps_counter;
static struct video_buffer video_buffer;
static struct stream stream;
static struct decoder decoder;
static struct recorder recorder;
static struct controller controller;
static struct file_handler file_handler;
static struct input_manager input_manager = {
.controller = &controller,
.frames = &frames,
.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, 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());
return false;
}
atexit(SDL_Quit);
if (!display) {
return true;
}
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
// Handle a click to gain focus as any other click
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
LOGW("Could not enable mouse focus clickthrough");
}
#endif
#ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
// Disable compositor bypassing on X11
if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) {
LOGW("Could not disable X11 compositor bypass");
}
#endif
// Do not minimize on focus loss
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
LOGW("Could not disable minimize on focus loss");
}
// Do not disable the screensaver when scrcpy is running
SDL_EnableScreenSaver();
return true;
}
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@ -45,186 +105,356 @@ static struct input_manager input_manager = {
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static int event_watcher(void *data, SDL_Event *event) {
if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) {
// called from another thread, not very safe, but it's a workaround!
screen_render(&screen);
static int
event_watcher(void *data, SDL_Event *event) {
(void) data;
if (event->type == SDL_WINDOWEVENT
&& 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;
}
#endif
static void event_loop(void) {
static bool
is_apk(const char *file) {
const char *ext = strrchr(file, '.');
return ext && !strcmp(ext, ".apk");
}
enum event_result {
EVENT_RESULT_CONTINUE,
EVENT_RESULT_STOPPED_BY_USER,
EVENT_RESULT_STOPPED_BY_EOS,
};
static enum event_result
handle_event(SDL_Event *event, bool control) {
switch (event->type) {
case EVENT_STREAM_STOPPED:
LOGD("Video stream stopped");
return EVENT_RESULT_STOPPED_BY_EOS;
case SDL_QUIT:
LOGD("User requested to quit");
return EVENT_RESULT_STOPPED_BY_USER;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = true;
// this is the very first frame, show the window
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &video_buffer)) {
return EVENT_RESULT_CONTINUE;
}
break;
case SDL_WINDOWEVENT:
screen_handle_window_event(&screen, &event->window);
break;
case SDL_TEXTINPUT:
if (!control) {
break;
}
input_manager_process_text_input(&input_manager, &event->text);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
// some key events do not interact with the device, so process the
// event even if control is disabled
input_manager_process_key(&input_manager, &event->key, control);
break;
case SDL_MOUSEMOTION:
if (!control) {
break;
}
input_manager_process_mouse_motion(&input_manager, &event->motion);
break;
case SDL_MOUSEWHEEL:
if (!control) {
break;
}
input_manager_process_mouse_wheel(&input_manager, &event->wheel);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process
// the event even if control is disabled
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;
}
file_handler_action_t action;
if (is_apk(event->drop.file)) {
action = ACTION_INSTALL_APK;
} else {
action = ACTION_PUSH_FILE;
}
file_handler_request(&file_handler, action, event->drop.file);
break;
}
}
return EVENT_RESULT_CONTINUE;
}
static bool
event_loop(bool display, bool control) {
(void) display;
#ifdef CONTINUOUS_RESIZING_WORKAROUND
SDL_AddEventWatch(event_watcher, NULL);
if (display) {
SDL_AddEventWatch(event_watcher, NULL);
}
#endif
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case EVENT_DECODER_STOPPED:
LOGD("Video decoder stopped");
return;
case SDL_QUIT:
LOGD("User requested to quit");
return;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = SDL_TRUE;
// this is the very first frame, show the window
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &frames)) {
return;
}
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(&screen);
break;
}
break;
case SDL_TEXTINPUT:
input_manager_process_text_input(&input_manager, &event.text);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
input_manager_process_key(&input_manager, &event.key);
break;
case SDL_MOUSEMOTION:
input_manager_process_mouse_motion(&input_manager, &event.motion);
break;
case SDL_MOUSEWHEEL:
input_manager_process_mouse_wheel(&input_manager, &event.wheel);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
input_manager_process_mouse_button(&input_manager, &event.button);
enum event_result result = handle_event(&event, control);
switch (result) {
case EVENT_RESULT_STOPPED_BY_USER:
return true;
case EVENT_RESULT_STOPPED_BY_EOS:
LOGW("Device disconnected");
return false;
case EVENT_RESULT_CONTINUE:
break;
}
}
return false;
}
static process_t set_show_touches_enabled(const char *serial, SDL_bool enabled) {
const char *value = enabled ? "1" : "0";
const char *const adb_cmd[] = {
"shell", "settings", "put", "system", "show_touches", value
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
return SDL_LOG_PRIORITY_CRITICAL;
case AV_LOG_ERROR:
return SDL_LOG_PRIORITY_ERROR;
case AV_LOG_WARNING:
return SDL_LOG_PRIORITY_WARN;
case AV_LOG_INFO:
return SDL_LOG_PRIORITY_INFO;
}
// do not forward others, which are too verbose
return 0;
}
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;
}
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
if (!local_fmt) {
LOGC("Could not allocate string");
return;
}
// strcpy is safe here, the destination is large enough
strcpy(local_fmt, "[FFmpeg] ");
strcpy(local_fmt + 9, fmt);
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
SDL_free(local_fmt);
}
bool
scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename;
struct server_params params = {
.crop = options->crop,
.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,
};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
static void wait_show_touches(process_t process) {
// reap the process, ignore the result
process_check_success(process, "show_touches");
}
SDL_bool scrcpy(const struct scrcpy_options *options) {
if (!server_start(&server, options->serial, options->port,
options->max_size, options->bit_rate)) {
return SDL_FALSE;
if (!server_start(&server, options->serial, &params)) {
return false;
}
process_t proc_show_touches;
SDL_bool show_touches_waited;
if (options->show_touches) {
LOGI("Enable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_TRUE);
show_touches_waited = SDL_FALSE;
bool ret = false;
bool fps_counter_initialized = false;
bool video_buffer_initialized = false;
bool file_handler_initialized = false;
bool recorder_initialized = false;
bool stream_started = false;
bool controller_initialized = false;
bool controller_started = false;
if (!sdl_init_and_configure(options->display, options->render_driver)) {
goto end;
}
SDL_bool ret = SDL_TRUE;
if (!SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1")) {
LOGW("Cannot request to keep default signal handlers");
}
if (!sdl_init_and_configure()) {
ret = SDL_FALSE;
goto finally_destroy_server;
}
socket_t device_socket = server_connect_to(&server);
if (device_socket == INVALID_SOCKET) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
if (!server_connect_to(&server)) {
goto end;
}
char device_name[DEVICE_NAME_FIELD_LENGTH];
struct size frame_size;
// screenrecord does not send frames when the screen content does not change
// therefore, we transmit the screen size before the video stream, to be able
// to init the window immediately
if (!device_read_info(device_socket, device_name, &frame_size)) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
// screenrecord does not send frames when the screen content does not
// change therefore, we transmit the screen size before the video stream,
// to be able to init the window immediately
if (!device_read_info(server.video_socket, device_name, &frame_size)) {
goto end;
}
if (!frames_init(&frames)) {
server_stop(&server);
ret = SDL_FALSE;
goto finally_destroy_server;
struct decoder *dec = NULL;
if (options->display) {
if (!fps_counter_init(&fps_counter)) {
goto end;
}
fps_counter_initialized = true;
if (!video_buffer_init(&video_buffer, &fps_counter,
options->render_expired_frames)) {
goto end;
}
video_buffer_initialized = true;
if (options->control) {
if (!file_handler_init(&file_handler, server.serial,
options->push_target)) {
goto end;
}
file_handler_initialized = true;
}
decoder_init(&decoder, &video_buffer);
dec = &decoder;
}
decoder_init(&decoder, &frames, device_socket);
struct recorder *rec = NULL;
if (record) {
if (!recorder_init(&recorder,
options->record_filename,
options->record_format,
frame_size)) {
goto end;
}
rec = &recorder;
recorder_initialized = true;
}
av_log_set_callback(av_log_callback);
stream_init(&stream, server.video_socket, dec, rec);
// now we consumed the header values, the socket receives the video stream
// start the decoder
if (!decoder_start(&decoder)) {
ret = SDL_FALSE;
server_stop(&server);
goto finally_destroy_frames;
// start the stream
if (!stream_start(&stream)) {
goto end;
}
stream_started = true;
if (options->display) {
if (options->control) {
if (!controller_init(&controller, server.control_socket)) {
goto end;
}
controller_initialized = true;
if (!controller_start(&controller)) {
goto end;
}
controller_started = true;
}
const char *window_title =
options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size,
options->always_on_top, options->window_x,
options->window_y, options->window_width,
options->window_height,
options->window_borderless,
options->rotation, options-> mipmaps)) {
goto end;
}
if (options->turn_screen_off) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
if (!controller_push_msg(&controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
}
if (!controller_init(&controller, device_socket)) {
ret = SDL_FALSE;
goto finally_stop_decoder;
}
input_manager.prefer_text = options->prefer_text;
if (!controller_start(&controller)) {
ret = SDL_FALSE;
goto finally_destroy_controller;
}
if (!screen_init_rendering(&screen, device_name, frame_size)) {
ret = SDL_FALSE;
goto finally_stop_and_join_controller;
}
if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = SDL_TRUE;
}
event_loop();
ret = event_loop(options->display, options->control);
LOGD("quit...");
screen_destroy(&screen);
finally_stop_and_join_controller:
controller_stop(&controller);
controller_join(&controller);
finally_destroy_controller:
controller_destroy(&controller);
finally_stop_decoder:
decoder_stop(&decoder);
// stop the server before decoder_join() to wake up the decoder
end:
// stop stream and controller so that they don't continue once their socket
// is shutdown
if (stream_started) {
stream_stop(&stream);
}
if (controller_started) {
controller_stop(&controller);
}
if (file_handler_initialized) {
file_handler_stop(&file_handler);
}
if (fps_counter_initialized) {
fps_counter_interrupt(&fps_counter);
}
// shutdown the sockets and kill the server
server_stop(&server);
decoder_join(&decoder);
finally_destroy_frames:
frames_destroy(&frames);
finally_destroy_server:
if (options->show_touches) {
if (!show_touches_waited) {
// wait the process which enabled "show touches"
wait_show_touches(proc_show_touches);
}
LOGI("Disable show_touches");
proc_show_touches = set_show_touches_enabled(options->serial, SDL_FALSE);
wait_show_touches(proc_show_touches);
// now that the sockets are shutdown, the stream and controller are
// interrupted, we can join them
if (stream_started) {
stream_join(&stream);
}
if (controller_started) {
controller_join(&controller);
}
if (controller_initialized) {
controller_destroy(&controller);
}
if (recorder_initialized) {
recorder_destroy(&recorder);
}
if (file_handler_initialized) {
file_handler_join(&file_handler);
file_handler_destroy(&file_handler);
}
if (video_buffer_initialized) {
video_buffer_destroy(&video_buffer);
}
if (fps_counter_initialized) {
fps_counter_join(&fps_counter);
fps_counter_destroy(&fps_counter);
}
server_destroy(&server);

View File

@ -1,16 +1,84 @@
#ifndef SCRCPY_H
#define SCRCPY_H
#include <SDL2/SDL_stdinc.h>
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
#include "common.h"
#include "input_manager.h"
#include "recorder.h"
struct scrcpy_options {
const char *serial;
Uint16 port;
Uint16 max_size;
Uint32 bit_rate;
SDL_bool show_touches;
const char *crop;
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;
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;
bool control;
bool display;
bool turn_screen_off;
bool render_expired_frames;
bool prefer_text;
bool window_borderless;
bool mipmaps;
bool stay_awake;
};
SDL_bool scrcpy(const struct scrcpy_options *options);
#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);
#endif

View File

@ -1,40 +1,36 @@
#include "screen.h"
#include <SDL2/SDL.h>
#include <assert.h>
#include <string.h>
#include <SDL2/SDL.h>
#include "config.h"
#include "common.h"
#include "compat.h"
#include "icon.xpm"
#include "lockutil.h"
#include "log.h"
#include "tinyxpm.h"
#include "tiny_xpm.h"
#include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h"
#define DISPLAY_MARGINS 96
SDL_bool sdl_init_and_configure(void) {
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGC("Could not initialize SDL: %s", SDL_GetError());
return SDL_FALSE;
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;
}
atexit(SDL_Quit);
// Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
LOGW("Could not enable bilinear filtering");
}
#if SDL_VERSION_ATLEAST(2, 0, 5)
// Handle a click to gain focus as any other click
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
LOGW("Could not enable mouse focus clickthrough");
}
#endif
return SDL_TRUE;
return rotated_size;
}
// get the window size in a struct size
static struct size get_native_window_size(SDL_Window *window) {
static struct size
get_window_size(SDL_Window *window) {
int width;
int height;
SDL_GetWindowSize(window, &width, &height);
@ -46,60 +42,70 @@ static struct size 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) {
static struct size
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
static void set_window_size(struct screen *screen, struct size new_size) {
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)
static SDL_bool get_preferred_display_bounds(struct size *bounds) {
static bool
get_preferred_display_bounds(struct size *bounds) {
SDL_Rect rect;
#if SDL_VERSION_ATLEAST(2, 0, 5)
#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r))
#else
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r))
#endif
if (GET_DISPLAY_BOUNDS(0, &rect)) {
LOGW("Could not get display usable bounds: %s", SDL_GetError());
return SDL_FALSE;
return false;
}
bounds->width = MAX(0, rect.w - DISPLAY_MARGINS);
bounds->height = MAX(0, rect.h - DISPLAY_MARGINS);
return SDL_TRUE;
return true;
}
// return the optimal size of the window, with the following constraints:
// - it attempts to keep at least one dimension of the current_size (i.e. it crops the black borders)
// - it attempts to keep at least one dimension of the current_size (i.e. it
// crops the black borders)
// - 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) {
static struct size
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;
}
struct size display_size;
// 32 bits because we need to multiply two 16 bits values
Uint32 w;
Uint32 h;
uint32_t w;
uint32_t h;
if (!get_preferred_display_bounds(&display_size)) {
// cannot get display bounds, do not constraint the size
// could not get display bounds, do not constraint the size
w = current_size.width;
h = current_size.height;
} else {
@ -107,93 +113,213 @@ static struct size get_optimal_size(struct size current_size, struct size frame_
h = MIN(current_size.height, display_size.height);
}
SDL_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;
// remove black borders on left and right (or none at all if it already
// fits)
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);
static inline struct 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
static inline struct size get_initial_optimal_size(struct size frame_size) {
return get_optimal_size(frame_size, frame_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 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 screen_init(struct screen *screen) {
void
screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER;
}
static inline SDL_Texture *create_texture(SDL_Renderer *renderer, struct size frame_size) {
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
frame_size.width, frame_size.height);
static inline SDL_Texture *
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;
}
SDL_bool screen_init_rendering(struct screen *screen, const char *device_name, struct size frame_size) {
bool
screen_init_rendering(struct screen *screen, const char *window_title,
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);
Uint32 window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
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;
#endif
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height, window_flags);
if (!screen->window) {
LOGC("Could not create window: %s", SDL_GetError());
return SDL_FALSE;
if (always_on_top) {
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else
LOGW("The 'always on top' flag is not available "
"(compile with SDL >= 2.0.5 to enable it)");
#endif
}
if (window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED);
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) {
LOGC("Could not create window: %s", SDL_GetError());
return false;
}
screen->renderer = SDL_CreateRenderer(screen->window, -1,
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
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 SDL_FALSE;
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) {
LOGE("Could not load icon: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
SDL_FreeSurface(icon);
} else {
LOGW("Could not load icon");
}
SDL_SetWindowIcon(screen->window, icon);
SDL_FreeSurface(icon);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height);
screen->texture = create_texture(screen->renderer, frame_size);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
frame_size.height);
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen);
return SDL_FALSE;
return false;
}
return SDL_TRUE;
screen->windowed_window_size = window_size;
return true;
}
void screen_show_window(struct screen *screen) {
void
screen_show_window(struct screen *screen) {
SDL_ShowWindow(screen->window);
}
void screen_destroy(struct screen *screen) {
void
screen_destroy(struct screen *screen) {
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
@ -205,100 +331,262 @@ void 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 SDL_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)) {
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) {
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 SDL_FALSE;
return false;
}
// 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) current_size.width * new_frame_size.width / screen->frame_size.width,
(Uint32) 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;
LOGD("New texture: %" PRIu16 "x%" PRIu16,
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 SDL_FALSE;
return false;
}
}
return SDL_TRUE;
return true;
}
// write the frame into the texture
static void update_texture(struct screen *screen, const AVFrame *frame) {
static void
update_texture(struct screen *screen, const AVFrame *frame) {
SDL_UpdateYUVTexture(screen->texture, NULL,
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);
}
}
SDL_bool screen_update_frame(struct screen *screen, struct frames *frames) {
mutex_lock(frames->mutex);
const AVFrame *frame = frames_consume_rendered_frame(frames);
bool
screen_update_frame(struct screen *screen, struct video_buffer *vb) {
mutex_lock(vb->mutex);
const AVFrame *frame = video_buffer_consume_rendered_frame(vb);
struct size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) {
mutex_unlock(frames->mutex);
return SDL_FALSE;
mutex_unlock(vb->mutex);
return false;
}
update_texture(screen, frame);
mutex_unlock(frames->mutex);
mutex_unlock(vb->mutex);
screen_render(screen);
return SDL_TRUE;
return true;
}
void screen_render(struct screen *screen) {
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 new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
void
screen_switch_fullscreen(struct screen *screen) {
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());
return;
}
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);
}
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");
void
screen_resize_to_fit(struct screen *screen) {
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) {
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;
}
}
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");
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

@ -1,69 +1,122 @@
#ifndef SCREEN_H
#define SCREEN_H
#include <stdbool.h>
#include <SDL2/SDL.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "common.h"
#include "frames.h"
#include "opengl.h"
#define WINDOW_POSITION_UNDEFINED (-0x8000)
struct video_buffer;
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;
SDL_bool has_frame;
SDL_bool fullscreen;
// 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 = SDL_FALSE, \
.fullscreen = SDL_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, \
}
// init SDL and set appropriate hints
SDL_bool sdl_init_and_configure(void);
// initialize default values
void screen_init(struct screen *screen);
void
screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden)
SDL_bool screen_init_rendering(struct screen *screen,
const char *device_name,
struct size frame_size);
// 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,
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 screen_show_window(struct screen *screen);
void
screen_show_window(struct screen *screen);
// destroy window, renderer and texture (if any)
void screen_destroy(struct screen *screen);
void
screen_destroy(struct screen *screen);
// resize if necessary and write the rendered frame into the texture
SDL_bool screen_update_frame(struct screen *screen, struct frames *frames);
bool
screen_update_frame(struct screen *screen, struct video_buffer *vb);
// render the texture to the renderer
void screen_render(struct screen *screen);
void
screen_render(struct screen *screen);
// switch the fullscreen mode
void screen_switch_fullscreen(struct screen *screen);
void
screen_switch_fullscreen(struct screen *screen);
// resize window to optimal size (remove black borders)
void screen_resize_to_fit(struct screen *screen);
void
screen_resize_to_fit(struct screen *screen);
// resize window to 1:1 (pixel-perfect)
void screen_resize_to_pixel_perfect(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,106 +1,293 @@
#include "server.h"
#include <assert.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <SDL2/SDL_assert.h>
#include <libgen.h>
#include <stdio.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_timer.h>
#include <SDL2/SDL_platform.h>
#include "config.h"
#include "log.h"
#include "net.h"
#include "command.h"
#include "util/log.h"
#include "util/net.h"
#include "util/str_util.h"
#define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server"
#ifdef OVERRIDE_SERVER_PATH
# define DEFAULT_SERVER_PATH OVERRIDE_SERVER_PATH
#else
# define DEFAULT_SERVER_PATH PREFIX PREFIXED_SERVER_PATH
#endif
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static const char *get_server_path(void) {
const char *server_path = getenv("SCRCPY_SERVER_PATH");
if (!server_path) {
server_path = DEFAULT_SERVER_PATH;
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) {
// if the envvar is set, use it
#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 server_path;
#else
// 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, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return SERVER_FILENAME;
}
char *dir = dirname(executable_path);
size_t dirlen = strlen(dir);
// sizeof(SERVER_FILENAME) gives statically the size including the null byte
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
char *server_path = SDL_malloc(len);
if (!server_path) {
LOGE("Could not alloc server path string, "
"using " SERVER_FILENAME " from current directory");
SDL_free(executable_path);
return SERVER_FILENAME;
}
memcpy(server_path, dir, dirlen);
server_path[dirlen] = PATH_SEPARATOR;
memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME));
// the final null byte has been copied with SERVER_FILENAME
SDL_free(executable_path);
LOGD("Using server (portable): %s", server_path);
return server_path;
#endif
}
static SDL_bool push_server(const char *serial) {
process_t process = adb_push(serial, get_server_path(), DEVICE_SERVER_PATH);
static bool
push_server(const char *serial) {
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");
}
static SDL_bool remove_server(const char *serial) {
process_t process = adb_remove_path(serial, DEVICE_SERVER_PATH);
return process_check_success(process, "adb shell rm");
}
static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
static bool
enable_tunnel_reverse(const char *serial, uint16_t local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse");
}
static SDL_bool disable_tunnel_reverse(const char *serial) {
static bool
disable_tunnel_reverse(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove");
}
static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) {
static bool
enable_tunnel_forward(const char *serial, uint16_t local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward");
}
static SDL_bool disable_tunnel_forward(const char *serial, Uint16 local_port) {
static bool
disable_tunnel_forward(const char *serial, uint16_t local_port) {
process_t process = adb_forward_remove(serial, local_port);
return process_check_success(process, "adb forward --remove");
}
static SDL_bool enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return SDL_TRUE;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = SDL_TRUE;
return enable_tunnel_forward(server->serial, server->local_port);
}
static SDL_bool disable_tunnel(struct server *server) {
static bool
disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
return disable_tunnel_forward(server->serial, server->local_port);
}
return disable_tunnel_reverse(server->serial);
}
static process_t execute_server(const char *serial,
Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) {
char max_size_string[6];
char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size);
sprintf(bit_rate_string, "%"PRIu32, bit_rate);
const char *const cmd[] = {
"shell",
"CLASSPATH=/data/local/tmp/scrcpy-server.jar",
"app_process",
"/", // unused
"com.genymobile.scrcpy.Server",
max_size_string,
bit_rate_string,
tunnel_forward ? "true" : "false",
};
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
static socket_t
listen_on_port(uint16_t port) {
#define IPV4_LOCALHOST 0x7F000001
static socket_t listen_on_port(Uint16 port) {
return net_listen(IPV4_LOCALHOST, port, 1);
}
static socket_t connect_and_read_byte(Uint16 port) {
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=" 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]));
}
static socket_t
connect_and_read_byte(uint16_t port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
if (socket == INVALID_SOCKET) {
return INVALID_SOCKET;
@ -109,14 +296,16 @@ static socket_t connect_and_read_byte(Uint16 port) {
char byte;
// the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection
if (net_recv_all(socket, &byte, 1) != 1) {
if (net_recv(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
net_close(socket);
return INVALID_SOCKET;
}
return socket;
}
static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) {
static socket_t
connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
socket_t socket = connect_and_read_byte(port);
@ -131,125 +320,165 @@ static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) {
return INVALID_SOCKET;
}
static void close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
LOGW("Cannot close socket");
return;
static void
close_socket(socket_t socket) {
assert(socket != INVALID_SOCKET);
net_shutdown(socket, SHUT_RDWR);
if (!net_close(socket)) {
LOGW("Could not close socket");
}
*socket = INVALID_SOCKET;
}
void server_init(struct server *server) {
void
server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER;
}
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate) {
server->local_port = local_port;
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->port_range = params->port_range;
if (serial) {
server->serial = SDL_strdup(serial);
if (!server->serial) {
return false;
}
}
if (!push_server(serial)) {
return SDL_FALSE;
goto error1;
}
server->server_copied_to_device = SDL_TRUE;
if (!enable_tunnel(server)) {
return SDL_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(local_port);
if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(server);
return SDL_FALSE;
}
if (!enable_tunnel_any_port(server, params->port_range)) {
goto error1;
}
// server will connect to our server socket
server->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward);
server->process = execute_server(server, params);
if (server->process == PROCESS_NONE) {
if (!server->tunnel_forward) {
close_socket(&server->server_socket);
}
disable_tunnel(server);
return SDL_FALSE;
goto error2;
}
server->tunnel_enabled = SDL_TRUE;
// 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;
}
return SDL_TRUE;
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;
}
socket_t server_connect_to(struct server *server) {
bool
server_connect_to(struct server *server) {
if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
Uint32 attempts = 50;
Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay);
}
server->video_socket = net_accept(server->server_socket);
if (server->video_socket == INVALID_SOCKET) {
return false;
}
if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
server->control_socket = net_accept(server->server_socket);
if (server->control_socket == INVALID_SOCKET) {
// the video_socket will be cleaned up on destroy
return false;
}
if (!server->tunnel_forward) {
// 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
server->video_socket =
connect_to_server(server->local_port, attempts, delay);
if (server->video_socket == INVALID_SOCKET) {
return false;
}
// the server is started, we can clean up the jar from the temporary folder
remove_server(server->serial); // ignore failure
server->server_copied_to_device = SDL_FALSE;
// we know that the device is listening, we don't need several attempts
server->control_socket =
net_connect(IPV4_LOCALHOST, server->local_port);
if (server->control_socket == INVALID_SOCKET) {
return false;
}
}
// we don't need the adb tunnel anymore
disable_tunnel(server); // ignore failure
server->tunnel_enabled = SDL_FALSE;
server->tunnel_enabled = false;
return server->device_socket;
return true;
}
void server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
LOGW("Cannot terminate server");
void
server_stop(struct server *server) {
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);
}
if (server->control_socket != INVALID_SOCKET) {
close_socket(server->control_socket);
}
cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated");
assert(server->process != PROCESS_NONE);
cmd_terminate(server->process);
if (server->tunnel_enabled) {
// ignore failure
disable_tunnel(server);
}
if (server->server_copied_to_device) {
remove_server(server->serial); // ignore failure
}
SDL_WaitThread(server->wait_server_thread, NULL);
}
void server_destroy(struct server *server) {
if (server->server_socket != INVALID_SOCKET) {
close_socket(&server->server_socket);
}
if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket);
}
SDL_free((void *) server->serial);
void
server_destroy(struct server *server) {
SDL_free(server->serial);
}

View File

@ -1,45 +1,80 @@
#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 {
const char *serial;
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 device_socket;
Uint16 local_port;
SDL_bool tunnel_enabled;
SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse"
SDL_bool server_copied_to_device;
socket_t video_socket;
socket_t control_socket;
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, \
.device_socket = INVALID_SOCKET, \
.local_port = 0, \
.tunnel_enabled = SDL_FALSE, \
.tunnel_forward = SDL_FALSE, \
.server_copied_to_device = SDL_FALSE, \
#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, \
.port_range = { \
.first = 0, \
.last = 0, \
}, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
}
struct server_params {
const char *crop;
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
void server_init(struct server *server);
void
server_init(struct server *server);
// push, enable tunnel et start the server
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate);
bool
server_start(struct server *server, const char *serial,
const struct server_params *params);
// block until the communication with the server is established
socket_t server_connect_to(struct server *server);
bool
server_connect_to(struct server *server);
// disconnect and kill the server process
void server_stop(struct server *server);
void
server_stop(struct server *server);
// close and release sockets
void server_destroy(struct server *server);
void
server_destroy(struct server *server);
#endif

302
app/src/stream.c Normal file
View File

@ -0,0 +1,302 @@
#include "stream.h"
#include <assert.h>
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "config.h"
#include "compat.h"
#include "decoder.h"
#include "events.h"
#include "recorder.h"
#include "util/buffer_util.h"
#include "util/log.h"
#define BUFSIZE 0x10000
#define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1)
static bool
stream_recv_packet(struct stream *stream, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
//
// The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
// <-------------> <-----> <-----------------------------...
// PTS packet raw packet
// size
//
// It is followed by <packet_size> bytes containing the packet/frame.
uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r < HEADER_SIZE) {
return false;
}
uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]);
assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0);
assert(len);
if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet");
return false;
}
r = net_recv_all(stream->socket, packet->data, len);
if (r < 0 || ((uint32_t) r) < len) {
av_packet_unref(packet);
return false;
}
packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE;
return true;
}
static void
notify_stopped(void) {
SDL_Event stop_event;
stop_event.type = EVENT_STREAM_STOPPED;
SDL_PushEvent(&stop_event);
}
static bool
process_config_packet(struct stream *stream, AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
LOGE("Could not send config packet to recorder");
return false;
}
return true;
}
static bool
process_frame(struct stream *stream, AVPacket *packet) {
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
return false;
}
if (stream->recorder) {
packet->dts = packet->pts;
if (!recorder_push(stream->recorder, packet)) {
LOGE("Could not send packet to recorder");
return false;
}
}
return true;
}
static bool
stream_parse(struct stream *stream, AVPacket *packet) {
uint8_t *in_data = packet->data;
int in_len = packet->size;
uint8_t *out_data = NULL;
int out_len = 0;
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
&out_data, &out_len, in_data, in_len,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
assert(r == in_len);
(void) r;
assert(out_len == in_len);
if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;
}
bool ok = process_frame(stream, packet);
if (!ok) {
LOGE("Could not process frame");
return false;
}
return true;
}
static bool
stream_push_packet(struct stream *stream, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (stream->has_pending || is_config) {
size_t offset;
if (stream->has_pending) {
offset = stream->pending.size;
if (av_grow_packet(&stream->pending, packet->size)) {
LOGE("Could not grow packet");
return false;
}
} else {
offset = 0;
if (av_new_packet(&stream->pending, packet->size)) {
LOGE("Could not create packet");
return false;
}
stream->has_pending = true;
}
memcpy(stream->pending.data + offset, packet->data, packet->size);
if (!is_config) {
// prepare the concat packet to send to the decoder
stream->pending.pts = packet->pts;
stream->pending.dts = packet->dts;
stream->pending.flags = packet->flags;
packet = &stream->pending;
}
}
if (is_config) {
// config packet
bool ok = process_config_packet(stream, packet);
if (!ok) {
return false;
}
} else {
// data packet
bool ok = stream_parse(stream, packet);
if (stream->has_pending) {
// the pending packet must be discarded (consumed or error)
stream->has_pending = false;
av_packet_unref(&stream->pending);
}
if (!ok) {
return false;
}
}
return true;
}
static int
run_stream(void *data) {
struct stream *stream = data;
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto end;
}
stream->codec_ctx = avcodec_alloc_context3(codec);
if (!stream->codec_ctx) {
LOGC("Could not allocate codec context");
goto end;
}
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder");
goto finally_free_codec_ctx;
}
if (stream->recorder) {
if (!recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_decoder;
}
if (!recorder_start(stream->recorder)) {
LOGE("Could not start recorder");
goto finally_close_recorder;
}
}
stream->parser = av_parser_init(AV_CODEC_ID_H264);
if (!stream->parser) {
LOGE("Could not initialize parser");
goto finally_stop_and_join_recorder;
}
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
for (;;) {
AVPacket packet;
bool ok = stream_recv_packet(stream, &packet);
if (!ok) {
// end of stream
break;
}
ok = stream_push_packet(stream, &packet);
av_packet_unref(&packet);
if (!ok) {
// cannot process packet (error already logged)
break;
}
}
LOGD("End of frames");
if (stream->has_pending) {
av_packet_unref(&stream->pending);
}
av_parser_close(stream->parser);
finally_stop_and_join_recorder:
if (stream->recorder) {
recorder_stop(stream->recorder);
LOGI("Finishing recording...");
recorder_join(stream->recorder);
}
finally_close_recorder:
if (stream->recorder) {
recorder_close(stream->recorder);
}
finally_close_decoder:
if (stream->decoder) {
decoder_close(stream->decoder);
}
finally_free_codec_ctx:
avcodec_free_context(&stream->codec_ctx);
end:
notify_stopped();
return 0;
}
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder) {
stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
stream->has_pending = false;
}
bool
stream_start(struct stream *stream) {
LOGD("Starting stream thread");
stream->thread = SDL_CreateThread(run_stream, "stream", stream);
if (!stream->thread) {
LOGC("Could not start stream thread");
return false;
}
return true;
}
void
stream_stop(struct stream *stream) {
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}
}
void
stream_join(struct stream *stream) {
SDL_WaitThread(stream->thread, NULL);
}

42
app/src/stream.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef STREAM_H
#define STREAM_H
#include <stdbool.h>
#include <stdint.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "config.h"
#include "util/net.h"
struct video_buffer;
struct stream {
socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread;
struct decoder *decoder;
struct recorder *recorder;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
bool has_pending;
AVPacket pending;
};
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder);
bool
stream_start(struct stream *stream);
void
stream_stop(struct stream *stream);
void
stream_join(struct stream *stream);
#endif

View File

@ -1,33 +0,0 @@
#include "strutil.h"
size_t xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
for (i = 0; i < n - 1 && src[i] != '\0'; ++i)
dest[i] = src[i];
if (n)
dest[i] = '\0';
return src[i] == '\0' ? i : n;
}
size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
const char *const *remaining = tokens;
const char *token = *remaining++;
size_t i = 0;
while (token) {
if (i) {
dst[i++] = sep;
if (i == n)
goto truncated;
}
size_t w = xstrncpy(dst + i, token, n - i);
if (w >= n - i)
goto truncated;
i += w;
token = *remaining++;
}
return i;
truncated:
dst[n - 1] = '\0';
return n;
}

View File

@ -1,19 +0,0 @@
#ifndef STRUTIL_H
#define STRUTIL_H
#include <stddef.h>
// like strncpy, except:
// - it copies at most n-1 chars
// - the dest string is nul-terminated
// - it does not write useless bytes if strlen(src) < n
// - it returns the number of chars actually written (max n-1) if src has
// been copied completely, or n if src has been truncated
size_t xstrncpy(char *dest, const char *src, size_t n);
// join tokens by sep into dst
// returns the number of chars actually written (max n-1) if no trucation
// occurred, or n if truncated
size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
#endif

View File

@ -1,33 +1,142 @@
// for portability
#define _POSIX_SOURCE // for kill()
#define _BSD_SOURCE // for readlink()
// modern glibc will complain without this
#define _DEFAULT_SOURCE
#include "command.h"
#include "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>
pid_t cmd_execute(const char *path, const char *const argv[]) {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
#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;
}
}
if (pid == 0) {
execvp(path, (char *const *)argv);
perror("exec");
_exit(1);
}
return pid;
free(path);
return ret;
}
SDL_bool cmd_terminate(pid_t pid) {
enum process_result
cmd_execute(const char *const argv[], pid_t *pid) {
int fd[2];
if (pipe(fd) == -1) {
perror("pipe");
return PROCESS_ERROR_GENERIC;
}
enum process_result ret = PROCESS_SUCCESS;
*pid = fork();
if (*pid == -1) {
perror("fork");
ret = PROCESS_ERROR_GENERIC;
goto end;
}
if (*pid > 0) {
// parent close write side
close(fd[1]);
fd[1] = -1;
// wait for EOF or receive errno from child
if (read(fd[0], &ret, sizeof(ret)) == -1) {
perror("read");
ret = PROCESS_ERROR_GENERIC;
goto end;
}
} else if (*pid == 0) {
// child close read side
close(fd[0]);
if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
execvp(argv[0], (char *const *)argv);
if (errno == ENOENT) {
ret = PROCESS_ERROR_MISSING_BINARY;
} else {
ret = PROCESS_ERROR_GENERIC;
}
perror("exec");
} else {
perror("fcntl");
ret = PROCESS_ERROR_GENERIC;
}
// send ret to the parent
if (write(fd[1], &ret, sizeof(ret)) == -1) {
perror("write");
}
// close write side before exiting
close(fd[1]);
_exit(1);
}
end:
if (fd[0] != -1) {
close(fd[0]);
}
if (fd[1] != -1) {
close(fd[1]);
}
return ret;
}
bool
cmd_terminate(pid_t pid) {
if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n",
(int) pid);
abort();
}
return kill(pid, SIGTERM) != -1;
}
SDL_bool cmd_simple_wait(pid_t pid, int *exit_code) {
bool
cmd_simple_wait(pid_t pid, int *exit_code) {
int status;
int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
// cannot wait, or exited unexpectedly, probably by a signal
// could not wait, or exited unexpectedly, probably by a signal
code = -1;
} else {
code = WEXITSTATUS(status);
@ -37,3 +146,34 @@ SDL_bool cmd_simple_wait(pid_t pid, int *exit_code) {
}
return !code;
}
char *
get_executable_path(void) {
// <https://stackoverflow.com/a/1024937/1987178>
#ifdef __linux__
char buf[PATH_MAX + 1]; // +1 for the null byte
ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX);
if (len == -1) {
perror("readlink");
return NULL;
}
buf[len] = '\0';
return SDL_strdup(buf);
#else
// in practice, we only need this feature for portable builds, only used on
// Windows, so we don't care implementing it for every platform
// (it's useful to have a working version on Linux for debugging though)
return NULL;
#endif
}
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,16 +0,0 @@
#include "net.h"
# include <unistd.h>
SDL_bool net_init(void) {
// do nothing
return SDL_TRUE;
}
void net_cleanup(void) {
// do nothing
}
SDL_bool net_close(socket_t socket) {
return !close(socket);
}

View File

@ -1,40 +1,75 @@
#include "command.h"
#include "log.h"
#include "strutil.h"
#include <sys/stat.h>
HANDLE cmd_execute(const char *path, const char *const argv[]) {
STARTUPINFO si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
#include "config.h"
#include "util/log.h"
#include "util/str_util.h"
static int
build_cmd(char *cmd, size_t len, const char *const argv[]) {
// Windows command-line parsing is WTF:
// <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS>
// only make it work for this very specific program
// (don't handle escaping nor quotes)
char cmd[256];
size_t ret = xstrjoin(cmd, argv, ' ', sizeof(cmd));
if (ret >= sizeof(cmd)) {
LOGE("Command too long (%" PRIsizet " chars)", sizeof(cmd) - 1);
return NULL;
size_t ret = xstrjoin(cmd, argv, ' ', len);
if (ret >= len) {
LOGE("Command too long (%" PRIsizet " chars)", len - 1);
return -1;
}
if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
return NULL;
}
return pi.hProcess;
return 0;
}
SDL_bool cmd_terminate(HANDLE handle) {
enum process_result
cmd_execute(const char *const argv[], HANDLE *handle) {
STARTUPINFOW si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
char cmd[256];
if (build_cmd(cmd, sizeof(cmd), argv)) {
*handle = NULL;
return PROCESS_ERROR_GENERIC;
}
wchar_t *wide = utf8_to_wide_char(cmd);
if (!wide) {
LOGC("Could not allocate wide char string");
return PROCESS_ERROR_GENERIC;
}
#ifdef WINDOWS_NOCONSOLE
int flags = CREATE_NO_WINDOW;
#else
int flags = 0;
#endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) {
SDL_free(wide);
*handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY;
}
return PROCESS_ERROR_GENERIC;
}
SDL_free(wide);
*handle = pi.hProcess;
return PROCESS_SUCCESS;
}
bool
cmd_terminate(HANDLE handle) {
return TerminateProcess(handle, 1) && CloseHandle(handle);
}
SDL_bool cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
bool
cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) {
// cannot wait or retrieve the exit code
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) {
// could not wait or retrieve the exit code
code = -1; // max value, it's unsigned
}
if (exit_code) {
@ -42,3 +77,37 @@ SDL_bool cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
}
return !code;
}
char *
get_executable_path(void) {
HMODULE hModule = GetModuleHandleW(NULL);
if (!hModule) {
return NULL;
}
WCHAR buf[MAX_PATH + 1]; // +1 for the null byte
int len = GetModuleFileNameW(hModule, buf, MAX_PATH);
if (!len) {
return NULL;
}
buf[len] = '\0';
return utf8_from_wide_char(buf);
}
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,21 +0,0 @@
#include "net.h"
#include "log.h"
SDL_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 SDL_FALSE;
}
return SDL_TRUE;
}
void net_cleanup(void) {
WSACleanup();
}
SDL_bool net_close(socket_t socket) {
return !closesocket(socket);
}

View File

@ -1,25 +1,30 @@
#include "tinyxpm.h"
#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;
Uint32 color;
uint32_t color;
};
static SDL_bool find_color(struct index *index, int len, char c, Uint32 *color) {
static bool
find_color(struct index *index, int len, char c, uint32_t *color) {
// there are typically very few color, so it's ok to iterate over the array
for (int i = 0; i < len; ++i) {
if (index[i].c == c) {
*color = index[i].color;
return SDL_TRUE;
return true;
}
}
*color = 0;
return SDL_FALSE;
return false;
}
// We encounter some problems with SDL2_image on MSYS2 (Windows),
@ -30,8 +35,9 @@ static SDL_bool find_color(struct index *index, int len, char c, Uint32 *color)
//
// Parameter is not "const char *" because XPM formats are generally stored in a
// (non-const) "char *"
SDL_Surface *read_xpm(char *xpm[]) {
#if SDL_ASSERT_LEVEL >= 2
SDL_Surface *
read_xpm(char *xpm[]) {
#ifndef NDEBUG
// patch the XPM to change the icon color in debug mode
xpm[2] = ". c #CC00CC";
#endif
@ -40,36 +46,38 @@ SDL_Surface *read_xpm(char *xpm[]) {
// *** No error handling, assume the XPM source is valid ***
// (it's in our source repo)
// Assertions are only checked in debug
Uint32 width = strtol(xpm[0], &endptr, 10);
Uint32 height = strtol(endptr + 1, &endptr, 10);
Uint32 colors = strtol(endptr + 1, &endptr, 10);
Uint32 chars = strtol(endptr + 1, &endptr, 10);
int width = strtol(xpm[0], &endptr, 10);
int height = strtol(endptr + 1, &endptr, 10);
int colors = strtol(endptr + 1, &endptr, 10);
int chars = strtol(endptr + 1, &endptr, 10);
// sanity checks
SDL_assert(width < 256);
SDL_assert(height < 256);
SDL_assert(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;
}
}
// parse image
Uint32 *pixels = SDL_malloc(4 * width * height);
uint32_t *pixels = SDL_malloc(4 * width * height);
if (!pixels) {
LOGE("Could not allocate icon memory");
return NULL;
@ -78,29 +86,34 @@ SDL_Surface *read_xpm(char *xpm[]) {
const char *line = xpm[1 + colors + y];
for (int x = 0; x < width; ++x) {
char c = line[x];
Uint32 color;
SDL_bool color_found = find_color(index, colors, c, &color);
SDL_assert(color_found);
uint32_t color;
bool color_found = find_color(index, colors, c, &color);
assert(color_found);
(void) color_found;
pixels[y * width + x] = color;
}
}
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
Uint32 amask = 0x000000ff;
Uint32 rmask = 0x0000ff00;
Uint32 gmask = 0x00ff0000;
Uint32 bmask = 0xff000000;
uint32_t amask = 0x000000ff;
uint32_t rmask = 0x0000ff00;
uint32_t gmask = 0x00ff0000;
uint32_t bmask = 0xff000000;
#else // little endian, like x86
Uint32 amask = 0xff000000;
Uint32 rmask = 0x00ff0000;
Uint32 gmask = 0x0000ff00;
Uint32 bmask = 0x000000ff;
uint32_t amask = 0xff000000;
uint32_t rmask = 0x00ff0000;
uint32_t gmask = 0x0000ff00;
uint32_t bmask = 0x000000ff;
#endif
SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels,
width, height,
32, 4 * width,
rmask, gmask, bmask, amask);
if (!surface) {
LOGE("Could not create icon surface");
return NULL;
}
// make the surface own the raw pixels
surface->flags &= ~SDL_PREALLOC;
return surface;

View File

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

View File

@ -0,0 +1,46 @@
#ifndef BUFFER_UTIL_H
#define BUFFER_UTIL_H
#include <stdbool.h>
#include <stdint.h>
#include "config.h"
static inline void
buffer_write16be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
buf[1] = value;
}
static inline void
buffer_write32be(uint8_t *buf, uint32_t value) {
buf[0] = value >> 24;
buf[1] = value >> 16;
buf[2] = value >> 8;
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];
}
static inline uint32_t
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) {
uint32_t msb = buffer_read32be(buf);
uint32_t lsb = buffer_read32be(&buf[4]);
return ((uint64_t) msb << 32) | lsb;
}
#endif

52
app/src/util/cbuf.h Normal file
View File

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

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

@ -3,6 +3,7 @@
#include <SDL2/SDL_log.h>
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)

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__
@ -18,7 +20,8 @@
typedef struct in_addr IN_ADDR;
#endif
socket_t net_connect(Uint32 addr, Uint16 port) {
socket_t
net_connect(uint32_t addr, uint16_t port) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
@ -32,13 +35,15 @@ socket_t net_connect(Uint32 addr, Uint16 port) {
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("connect");
net_close(sock);
return INVALID_SOCKET;
}
return sock;
}
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
socket_t
net_listen(uint32_t addr, uint16_t port, int backlog) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
@ -46,7 +51,8 @@ socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
}
int reuse = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) {
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse,
sizeof(reuse)) == -1) {
perror("setsockopt(SO_REUSEADDR)");
}
@ -57,48 +63,85 @@ socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("bind");
net_close(sock);
return INVALID_SOCKET;
}
if (listen(sock, backlog) == SOCKET_ERROR) {
perror("listen");
net_close(sock);
return INVALID_SOCKET;
}
return sock;
}
socket_t net_accept(socket_t server_socket) {
socket_t
net_accept(socket_t server_socket) {
SOCKADDR_IN csin;
socklen_t sinsize = sizeof(csin);
return accept(server_socket, (SOCKADDR *) &csin, &sinsize);
}
ssize_t net_recv(socket_t socket, void *buf, size_t len) {
ssize_t
net_recv(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, 0);
}
ssize_t net_recv_all(socket_t socket, void *buf, size_t len) {
ssize_t
net_recv_all(socket_t socket, void *buf, size_t len) {
return recv(socket, buf, len, MSG_WAITALL);
}
ssize_t net_send(socket_t socket, void *buf, size_t len) {
ssize_t
net_send(socket_t socket, const void *buf, size_t len) {
return send(socket, buf, len, 0);
}
ssize_t net_send_all(socket_t socket, void *buf, size_t len) {
ssize_t w;
ssize_t
net_send_all(socket_t socket, const void *buf, size_t len) {
ssize_t w = 0;
while (len > 0) {
w = send(socket, buf, len, 0);
if (w == -1) {
return -1;
}
len -= w;
buf += w;
buf = (char *) buf + w;
}
return w;
}
SDL_bool net_shutdown(socket_t socket, int how) {
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
}

57
app/src/util/net.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef NET_H
#define NET_H
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_platform.h>
#ifdef __WINDOWS__
# include <winsock2.h>
#define SHUT_RD SD_RECEIVE
#define SHUT_WR SD_SEND
#define SHUT_RDWR SD_BOTH
typedef SOCKET socket_t;
#else
# include <sys/socket.h>
# define INVALID_SOCKET -1
typedef int socket_t;
#endif
#include "config.h"
bool
net_init(void);
void
net_cleanup(void);
socket_t
net_connect(uint32_t addr, uint16_t port);
socket_t
net_listen(uint32_t addr, uint16_t port, int backlog);
socket_t
net_accept(socket_t server_socket);
// the _all versions wait/retry until len bytes have been written/read
ssize_t
net_recv(socket_t socket, void *buf, size_t len);
ssize_t
net_recv_all(socket_t socket, void *buf, size_t len);
ssize_t
net_send(socket_t socket, const void *buf, size_t len);
ssize_t
net_send_all(socket_t socket, const void *buf, size_t len);
// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
bool
net_shutdown(socket_t socket, int how);
bool
net_close(socket_t socket);
#endif

77
app/src/util/queue.h Normal file
View File

@ -0,0 +1,77 @@
// generic intrusive FIFO queue
#ifndef QUEUE_H
#define QUEUE_H
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include "config.h"
// To define a queue type of "struct foo":
// struct queue_foo QUEUE(struct foo);
#define QUEUE(TYPE) { \
TYPE *first; \
TYPE *last; \
}
#define queue_init(PQ) \
(void) ((PQ)->first = (PQ)->last = NULL)
#define queue_is_empty(PQ) \
!(PQ)->first
// NEXTFIELD is the field in the ITEM type used for intrusive linked-list
//
// For example:
// struct foo {
// int value;
// struct foo *next;
// };
//
// // define the type "struct my_queue"
// struct my_queue QUEUE(struct foo);
//
// struct my_queue queue;
// queue_init(&queue);
//
// struct foo v1 = { .value = 42 };
// struct foo v2 = { .value = 27 };
//
// queue_push(&queue, next, v1);
// queue_push(&queue, next, v2);
//
// struct foo *foo;
// queue_take(&queue, next, &foo);
// assert(foo->value == 42);
// queue_take(&queue, next, &foo);
// assert(foo->value == 27);
// assert(queue_is_empty(&queue));
//
// push a new item into the queue
#define queue_push(PQ, NEXTFIELD, ITEM) \
(void) ({ \
(ITEM)->NEXTFIELD = NULL; \
if (queue_is_empty(PQ)) { \
(PQ)->first = (PQ)->last = (ITEM); \
} else { \
(PQ)->last->NEXTFIELD = (ITEM); \
(PQ)->last = (ITEM); \
} \
})
// take the next item and remove it from the queue (the queue must not be empty)
// the result is stored in *(PITEM)
// (without typeof(), we could not store a local variable having the correct
// type so that we can "return" it)
#define queue_take(PQ, NEXTFIELD, PITEM) \
(void) ({ \
assert(!queue_is_empty(PQ)); \
*(PITEM) = (PQ)->first; \
(PQ)->first = (PQ)->first->NEXTFIELD; \
})
// no need to update (PQ)->last if the queue is left empty:
// (PQ)->last is undefined if !(PQ)->first anyway
#endif

197
app/src/util/str_util.c Normal file
View File

@ -0,0 +1,197 @@
#include "str_util.h"
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
# include <windows.h>
# include <tchar.h>
#endif
#include <SDL2/SDL_stdinc.h>
#include "config.h"
size_t
xstrncpy(char *dest, const char *src, size_t n) {
size_t i;
for (i = 0; i < n - 1 && src[i] != '\0'; ++i)
dest[i] = src[i];
if (n)
dest[i] = '\0';
return src[i] == '\0' ? i : n;
}
size_t
xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
const char *const *remaining = tokens;
const char *token = *remaining++;
size_t i = 0;
while (token) {
if (i) {
dst[i++] = sep;
if (i == n)
goto truncated;
}
size_t w = xstrncpy(dst + i, token, n - i);
if (w >= n - i)
goto truncated;
i += w;
token = *remaining++;
}
return i;
truncated:
dst[n - 1] = '\0';
return n;
}
char *
strquote(const char *src) {
size_t len = strlen(src);
char *quoted = SDL_malloc(len + 3);
if (!quoted) {
return NULL;
}
memcpy(&quoted[1], src, len);
quoted[0] = '"';
quoted[len + 1] = '"';
quoted[len + 2] = '\0';
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);
if (len <= max_len) {
return len;
}
len = max_len;
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
// the next byte is not the start of a new UTF-8 codepoint
// so if we would cut there, the character would be truncated
len--;
}
return len;
}
#ifdef _WIN32
wchar_t *
utf8_to_wide_char(const char *utf8) {
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
if (!len) {
return NULL;
}
wchar_t *wide = SDL_malloc(len * sizeof(wchar_t));
if (!wide) {
return NULL;
}
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, len);
return wide;
}
char *
utf8_from_wide_char(const wchar_t *ws) {
int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
if (!len) {
return NULL;
}
char *utf8 = SDL_malloc(len);
if (!utf8) {
return NULL;
}
WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL);
return utf8;
}
#endif

60
app/src/util/str_util.h Normal file
View File

@ -0,0 +1,60 @@
#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
// - it does not write useless bytes if strlen(src) < n
// - it returns the number of chars actually written (max n-1) if src has
// been copied completely, or n if src has been truncated
size_t
xstrncpy(char *dest, const char *src, size_t n);
// join tokens by sep into dst
// returns the number of chars actually written (max n-1) if no trucation
// occurred, or n if truncated
size_t
xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
// quote a string
// returns the new allocated string, to be freed by the caller
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);
#ifdef _WIN32
// convert a UTF-8 string to a wchar_t string
// returns the new allocated string, to be freed by the caller
wchar_t *
utf8_to_wide_char(const char *utf8);
char *
utf8_from_wide_char(const wchar_t *s);
#endif
#endif

113
app/src/video_buffer.c Normal file
View File

@ -0,0 +1,113 @@
#include "video_buffer.h"
#include <assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#include "config.h"
#include "util/lock.h"
#include "util/log.h"
bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
bool render_expired_frames) {
vb->fps_counter = fps_counter;
if (!(vb->decoding_frame = av_frame_alloc())) {
goto error_0;
}
if (!(vb->rendering_frame = av_frame_alloc())) {
goto error_1;
}
if (!(vb->mutex = SDL_CreateMutex())) {
goto error_2;
}
vb->render_expired_frames = render_expired_frames;
if (render_expired_frames) {
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(vb->mutex);
goto error_2;
}
// interrupted is not used if expired frames are not rendered
// since offering a frame will never block
vb->interrupted = false;
}
// there is initially no rendering frame, so consider it has already been
// consumed
vb->rendering_frame_consumed = true;
return true;
error_2:
av_frame_free(&vb->rendering_frame);
error_1:
av_frame_free(&vb->decoding_frame);
error_0:
return false;
}
void
video_buffer_destroy(struct video_buffer *vb) {
if (vb->render_expired_frames) {
SDL_DestroyCond(vb->rendering_frame_consumed_cond);
}
SDL_DestroyMutex(vb->mutex);
av_frame_free(&vb->rendering_frame);
av_frame_free(&vb->decoding_frame);
}
static void
video_buffer_swap_frames(struct video_buffer *vb) {
AVFrame *tmp = vb->decoding_frame;
vb->decoding_frame = vb->rendering_frame;
vb->rendering_frame = tmp;
}
void
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped) {
mutex_lock(vb->mutex);
if (vb->render_expired_frames) {
// wait for the current (expired) frame to be consumed
while (!vb->rendering_frame_consumed && !vb->interrupted) {
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex);
}
} else if (!vb->rendering_frame_consumed) {
fps_counter_add_skipped_frame(vb->fps_counter);
}
video_buffer_swap_frames(vb);
*previous_frame_skipped = !vb->rendering_frame_consumed;
vb->rendering_frame_consumed = false;
mutex_unlock(vb->mutex);
}
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) {
assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true;
fps_counter_add_rendered_frame(vb->fps_counter);
if (vb->render_expired_frames) {
// unblock video_buffer_offer_decoded_frame()
cond_signal(vb->rendering_frame_consumed_cond);
}
return vb->rendering_frame;
}
void
video_buffer_interrupt(struct video_buffer *vb) {
if (vb->render_expired_frames) {
mutex_lock(vb->mutex);
vb->interrupted = true;
mutex_unlock(vb->mutex);
// wake up blocking wait
cond_signal(vb->rendering_frame_consumed_cond);
}
}

49
app/src/video_buffer.h Normal file
View File

@ -0,0 +1,49 @@
#ifndef VIDEO_BUFFER_H
#define VIDEO_BUFFER_H
#include <stdbool.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "fps_counter.h"
// forward declarations
typedef struct AVFrame AVFrame;
struct video_buffer {
AVFrame *decoding_frame;
AVFrame *rendering_frame;
SDL_mutex *mutex;
bool render_expired_frames;
bool interrupted;
SDL_cond *rendering_frame_consumed_cond;
bool rendering_frame_consumed;
struct fps_counter *fps_counter;
};
bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
bool render_expired_frames);
void
video_buffer_destroy(struct video_buffer *vb);
// set the decoded frame as ready for rendering
// this function locks frames->mutex during its execution
// the output flag is set to report whether the previous frame has been skipped
void
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped);
// mark the rendering frame as consumed and return it
// MUST be called with frames->mutex locked!!!
// the caller is expected to render the returned frame to some texture before
// unlocking frames->mutex
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb);
// wake up and avoid any blocking call
void
video_buffer_interrupt(struct video_buffer *vb);
#endif

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;
}

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

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

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

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

View File

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

View File

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

View File

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

38
app/tests/test_queue.c Normal file
View File

@ -0,0 +1,38 @@
#include <assert.h>
#include "util/queue.h"
struct foo {
int value;
struct foo *next;
};
static void test_queue(void) {
struct my_queue QUEUE(struct foo) queue;
queue_init(&queue);
assert(queue_is_empty(&queue));
struct foo v1 = { .value = 42 };
struct foo v2 = { .value = 27 };
queue_push(&queue, next, &v1);
queue_push(&queue, next, &v2);
struct foo *foo;
assert(!queue_is_empty(&queue));
queue_take(&queue, next, &foo);
assert(foo->value == 42);
assert(!queue_is_empty(&queue));
queue_take(&queue, next, &foo);
assert(foo->value == 27);
assert(queue_is_empty(&queue));
}
int main(void) {
test_queue();
return 0;
}

View File

@ -1,9 +1,12 @@
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <SDL2/SDL.h>
#include "strutil.h"
#include "util/str_util.h"
static void test_xstrncpy_simple() {
static void test_xstrncpy_simple(void) {
char s[] = "xxxxxxxxxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s));
@ -20,7 +23,7 @@ static void test_xstrncpy_simple() {
assert(!strcmp("abcdef", s));
}
static void test_xstrncpy_just_fit() {
static void test_xstrncpy_just_fit(void) {
char s[] = "xxxxxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s));
@ -34,7 +37,7 @@ static void test_xstrncpy_just_fit() {
assert(!strcmp("abcdef", s));
}
static void test_xstrncpy_truncated() {
static void test_xstrncpy_truncated(void) {
char s[] = "xxx";
size_t w = xstrncpy(s, "abcdef", sizeof(s));
@ -48,7 +51,7 @@ static void test_xstrncpy_truncated() {
assert(!strncmp("abcdef", s, 3));
}
static void test_xstrjoin_simple() {
static void test_xstrjoin_simple(void) {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxxxxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@ -66,7 +69,7 @@ static void test_xstrjoin_simple() {
assert(!strcmp("abc de fghi", s));
}
static void test_xstrjoin_just_fit() {
static void test_xstrjoin_just_fit(void) {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@ -81,7 +84,7 @@ static void test_xstrjoin_just_fit() {
assert(!strcmp("abc de fghi", s));
}
static void test_xstrjoin_truncated_in_token() {
static void test_xstrjoin_truncated_in_token(void) {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@ -96,7 +99,7 @@ static void test_xstrjoin_truncated_in_token() {
assert(!strcmp("abc d", s));
}
static void test_xstrjoin_truncated_before_sep() {
static void test_xstrjoin_truncated_before_sep(void) {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@ -111,7 +114,7 @@ static void test_xstrjoin_truncated_before_sep() {
assert(!strcmp("abc de", s));
}
static void test_xstrjoin_truncated_after_sep() {
static void test_xstrjoin_truncated_after_sep(void) {
const char *const tokens[] = { "abc", "de", "fghi", NULL };
char s[] = "xxxxxxx";
size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
@ -126,7 +129,164 @@ static void test_xstrjoin_truncated_after_sep() {
assert(!strcmp("abc de ", s));
}
int main() {
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
size_t count;
count = utf8_truncation_index(s, 1);
assert(count == 1);
count = utf8_truncation_index(s, 2);
assert(count == 1); // É is 2 bytes-wide
count = utf8_truncation_index(s, 3);
assert(count == 3);
count = utf8_truncation_index(s, 4);
assert(count == 4);
count = utf8_truncation_index(s, 5);
assert(count == 4); // Ô is 2 bytes-wide
count = utf8_truncation_index(s, 6);
assert(count == 6);
count = utf8_truncation_index(s, 7);
assert(count == 7);
count = utf8_truncation_index(s, 8);
assert(count == 7); // no more chars
}
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();
test_xstrncpy_truncated();
@ -135,5 +295,10 @@ int main() {
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.0.1'
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" />

20
cross_win32.txt Normal file
View File

@ -0,0 +1,20 @@
# apt install mingw-w64 mingw-w64-tools
[binaries]
name = 'mingw'
c = '/usr/bin/i686-w64-mingw32-gcc'
cpp = '/usr/bin/i686-w64-mingw32-g++'
ar = '/usr/bin/i686-w64-mingw32-ar'
strip = '/usr/bin/i686-w64-mingw32-strip'
pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config'
[host_machine]
system = 'windows'
cpu_family = 'x86'
cpu = 'i686'
endian = 'little'
[properties]
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'

20
cross_win64.txt Normal file
View File

@ -0,0 +1,20 @@
# apt install mingw-w64 mingw-w64-tools
[binaries]
name = 'mingw'
c = '/usr/bin/x86_64-w64-mingw32-gcc'
cpp = '/usr/bin/x86_64-w64-mingw32-g++'
ar = '/usr/bin/x86_64-w64-mingw32-ar'
strip = '/usr/bin/x86_64-w64-mingw32-strip'
pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config'
[host_machine]
system = 'windows'
cpu_family = 'x86'
cpu = 'x86_64'
endian = 'little'
[properties]
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'

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