Compare commits

...

102 Commits

Author SHA1 Message Date
8fb01922a3 fixrepeat 2020-08-07 09:21:13 +02:00
521f2fe994 Update links to v1.15 in README and BUILD 2020-08-06 21:56:06 +02:00
edc4f7675f Bump version to 1.15 2020-08-06 21:00:48 +02:00
712f1fa6b2 Upgrade FFmpeg (4.3.1) for Windows
Include the latest version of FFmpeg in Windows releases.
2020-08-06 21:00:48 +02:00
1ba06037f8 Upgrade platform-tools (30.0.4) for Windows
Include the latest version of adb in Windows releases.
2020-08-06 21:00:48 +02:00
da63e3774b Merge branch 'master' into dev 2020-08-06 21:00:15 +02:00
cf9d44979c Keep the screen off on powering on
PR #1577 <https://github.com/Genymobile/scrcpy/pull/1577>
Fixes #1573 <https://github.com/Genymobile/scrcpy/issues/1573>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-08-03 19:51:58 +02:00
84f1d9e375 Add --no-key-repeat cli option
Add an option to avoid forwarding repeated key events.

PR #1623 <https://github.com/Genymobile/scrcpy/pull/1623>
Refs #1013 <https://github.com/Genymobile/scrcpy/issues/1013>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-08-03 19:51:55 +02:00
65d06a3663 Pass full options struct to static functions
This avoids to pass specific options values individually. Since these
function are static (internal to the file), this is not a problem to
make them depend on scrcpy_options.

Refs #1623 <https://github.com/Genymobile/scrcpy/pull/1623>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-08-02 18:20:14 +02:00
74079ea5e4 Copy the options used in input manager init
This avoids to pass additional options to some input manager functions.

Refs #1623 <https://github.com/Genymobile/scrcpy/pull/1623>
2020-08-02 18:19:41 +02:00
0870d8620f Mention that MENU unlocks screen
Pressing MENU while in lock screen unlocks (it still asks for the schema
or code if enabled).
2020-08-01 17:10:09 +02:00
d49cffb938 Use <kbd> HTML tag for keys
It's prettyier in a browser.
2020-08-01 17:04:16 +02:00
dfb7324d7b Mention in README that Ctrl is forwarded 2020-08-01 16:51:38 +02:00
d8b3ba170c Update copy-paste section in README
Update documentation regarding the recent copy-paste changes.
2020-08-01 16:51:37 +02:00
7ad47dfaab Swap paste shortcuts
For consistency with MOD+c and MOD+x, use MOD+v to inject PASTE.

Use Mod+Shift+v to inject clipboard content as a sequence of text
events.
2020-08-01 16:47:44 +02:00
56a115b5c5 Add shortcuts for COPY and CUT
Send COPY and CUT on MOD+c and MOD+x (only supported for Android >= 7).

The shortcuts Ctrl+c and Ctrl+x should generally also work (even before
Android 7), but the active Android app may use them for other actions
instead.
2020-08-01 16:47:43 +02:00
8f64a5984b Change "resize to fit" shortcut to MOD+w
For convenience, MOD+x will be used for injecting the CUT keycode.
2020-08-01 16:31:31 +02:00
bccd12bf5c Remove "get clipboard" call
Now that every device clipboard change is automatically synchronized to
the computer, the "get clipboard" request (bound to MOD+c) is useless.
2020-08-01 16:31:31 +02:00
20d3925099 Set computer clipboard only if necessary
Do not explicitly set the clipboard text if it already contains the
expected content.

Even if copy-paste loops are avoided by the previous commit, this avoids
to trigger a clipboard change on the computer-side.

Refs #1580 <https://github.com/Genymobile/scrcpy/issues/1580>
2020-08-01 16:31:31 +02:00
1223a72eb8 Set device clipboard only if necessary
Do not explicitly set the clipboard text if it already contains the
expected content. This avoids possible copy-paste loops between the
computer and the device.
2020-08-01 16:31:31 +02:00
7683be8159 Synchronize clipboard on Ctrl+v
Pressing Ctrl+v on the device will typically paste the clipboard
content.

Before sending the key event, synchronize the computer clipboard to the
device clipboard to allow seamless copy-paste.
2020-08-01 16:31:31 +02:00
d4ca85d6a8 Forward Shift to the device
This allows to select text using Shift+(arrow keys).

Fixes #942 <https://github.com/Genymobile/scrcpy/issues/942>
2020-08-01 16:31:31 +02:00
e6e528f228 Forward Ctrl to the device
Now that the scrcpy shortcut modifier is Alt by default (and can be
configured), forward Ctrl to the device.

This allows to trigger Android shortcuts.

Fixes #555 <https://github.com/Genymobile/scrcpy/issues/555>
2020-08-01 16:31:31 +02:00
a5f8b577c5 Ignore text events for shortcuts
Pressing Alt+c generates a text event containing "c", so "c" was sent to
the device when --prefer-text was enabled.

Ignore text events when the mod state matches a shortcut modifier.
2020-08-01 16:31:31 +02:00
e4bb7c1d1f Accept Super as shortcut modifier
In addition to (left) Alt, also accept (left) Super. This is especially
convenient for macOS users (Super is the Cmd key).
2020-08-01 16:31:31 +02:00
1b76d9fd78 Customize shortcut modifier
Add --shortcut-mod, and use Alt as default modifier.

This paves the way to forward the Ctrl key to the device.
2020-08-01 16:31:27 +02:00
63cb93d7d7 Use Ctrl for all shortcuts
Remove the Cmd modifier on macOS, which was possible only for some
shortcuts but not all.

This paves the way to make the shortcut modifier customizable.
2020-08-01 16:31:08 +02:00
9d9dd1f143 Make expression order consistent
The condition "cmd" was always before "shift" in all expressions except
4.
2020-07-17 00:00:42 +02:00
eabaf6f7bd Simplify PASTE option for "set clipboard"
When the client requests to set the clipboard, it may request to press
the PASTE key in addition. To be a bit generic, it was stored as a flag
in ControlMessage.java.

But flags suggest that it represents a bitwise union. Use a simple
boolean instead.
2020-07-17 00:00:42 +02:00
199c74f62f Declare main() with argc/argv params in tests
Declaring the main method as "int main(void)" causes issues with SDL.

Fixes #1209 <https://github.com/Genymobile/scrcpy/issues/1209>
2020-07-15 12:17:04 +02:00
322f1512ea Inject WAKEUP instead of POWER
To power the device on, inject KEYCODE_WAKEUP to avoid a possible
race condition (the device might become off between the test
isScreenOn() and the POWER keycode injection).
2020-07-15 12:03:44 +02:00
30714aba34 Restore power mode to normal on cleanup
This avoids to let the device screen turned off (as enabled by Ctrl+o or
--turn-screen-off).

PR #1576 <https://github.com/Genymobile/scrcpy/pull/1576>
Fixes #1572 <https://github.com/Genymobile/scrcpy/issues/1572>
2020-07-09 22:33:11 +02:00
a973757fd1 Warn on ignored touch event
In theory, this was expected to only happen when a touch event is sent
just before the device is rotated, but some devices do not respect the
encoding size, causing an unexpected mismatch.

Refs #1518 <https://github.com/Genymobile/scrcpy/issues/1518>
2020-07-09 22:32:14 +02:00
deea29f52a Send touch event without pressure on button up
Refs #1518 <https://github.com/Genymobile/scrcpy/issues/1518>
2020-07-09 22:31:47 +02:00
5086e7b744 Update BUILD.md
Update the macOS section.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-07-07 20:50:09 +02:00
5fa46ad0c7 Fix constants name in comment 2020-07-07 15:47:25 +02:00
334964c380 Make setScreenPowerMode() method static
Its implementation does not use the instance at all.
2020-07-07 15:34:34 +02:00
f7d4b6d0db Do not crash on missing clipboard manager
Some devices have no clipboard manager.

In that case, do not try to enable clipboard synchronization to avoid
a crash.

Fixes #1440 <https://github.com/Genymobile/scrcpy/issues/1440>
Fixes #1556 <https://github.com/Genymobile/scrcpy/issues/1556>
2020-07-03 08:53:51 +02:00
e99b896ae2 README: Add Fedora install instructions
PR #1549 <https://github.com/Genymobile/scrcpy/pull/1549>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-06-27 15:51:44 +02:00
e8a565f9ea Fix touch events HiDPI-scaling
Touch events were HiDPI-scaled twice:
 - once because the position (provided as floats between 0 and 1) were
   converted in pixels using the drawable size (not the window size)
 - once due to screen_convert_to_frame_coords()

One possible fix could be to compute the position in pixels from the
window size instead, but this would unnecessarily round the event
position to the nearest window coordinates (instead of drawable
coordinates).

Instead, expose two separate functions to convert to frame coordinates
from either window or drawable coordinates.

Fixes #1536 <https://github.com/Genymobile/scrcpy/issues/1536>
Refs #15 <https://github.com/Genymobile/scrcpy/issues/15>
Refs e40532a376
2020-06-27 15:45:28 +02:00
8c27f59aa5 Improve linguistic
PR #1543 <https://github.com/Genymobile/scrcpy/pull/1543>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-06-25 22:08:02 +02:00
3c1ed5d86c Handle repeating keycodes
Initialize "repeat" count on key events.

PR #1519 <https://github.com/Genymobile/scrcpy/pull/1519>
Refs #1013 <https://github.com/Genymobile/scrcpy/pull/1013>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-06-19 22:30:17 +02:00
0ba74fbd9a Make scrcpy.h independant of other headers
The header scrcpy.h is intended to be the "public" API. It should not
depend on other internal headers.

Therefore, declare all required structs in this header and adapt
internal code.
2020-06-19 22:30:02 +02:00
29e5af76d4 Remove fprintf() call in tests
It should never have been committed.
2020-06-19 21:54:46 +02:00
dc7b60e619 Add option for disabling screensaver
PR #1502 <https://github.com/Genymobile/scrcpy/pull/1502>
Fixes #1370 <https://github.com/Genymobile/scrcpy/issues/1370>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-06-18 21:35:28 +02:00
42641d2737 Fix typo in README.md
PR #1523 <https://github.com/Genymobile/scrcpy/pull/1523>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-06-18 21:13:35 +02:00
1e4ee547b5 Make message buffer static
Now that the message max size is 256k, do not put the buffer on the
stack.
2020-06-11 23:11:20 +02:00
488d22d4e2 Increase clipboard size from 4k to 256k
Beyond 256k, SDL_GetClipboardText() returns an empty string on my
computer.

Fixes #1117 <https://github.com/Genymobile/scrcpy/issues/1117>
2020-06-11 23:11:20 +02:00
00d292b2f5 Fix receiver on partial device messages
If a single device message was read in several chunks from the control
socket, the communication was broken.
2020-06-11 23:11:20 +02:00
245999aec4 Serialize text size on 4 bytes
This will allow to send text having a size greater than 65535 bytes.
2020-06-11 23:11:17 +02:00
d91c5dcfd5 Rename MSG_SERIALIZED_MAX_SIZE to MSG_MAX_SIZE
For simplicity and consistency with the server part.
2020-06-11 23:08:04 +02:00
d202d7b205 Add unit test for big clipboard device message
Test clipboard synchronization from the device to the computer.
2020-06-11 23:06:02 +02:00
08c0c64af6 Rename test names from "event" to "msg"
The meson test names had not been changed when "event" had been renamed
to "message".

Ref: 28980bbc90
2020-06-11 23:06:02 +02:00
a00a8763d6 Avoid additional copy on Java text parsing
Directly pass the buffer range to the String constructor.
2020-06-11 23:06:02 +02:00
8f314c74b0 Reorganize message size constants
Make the max clipboard text length depend on the max message size.
2020-06-11 22:58:54 +02:00
3c0fc8f54f Mention sndcpy 2020-06-09 22:09:23 +02:00
1b73eff3c9 Add missing file in build_without_gradle.sh
Fixes #1481 <https://github.com/Genymobile/scrcpy/issues/1481>
PR #1482 <https://github.com/Genymobile/scrcpy/pull/1482>

Signed-off-by: Louis Leseur <louis.leseur@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-06-07 22:00:35 +02:00
6e1069a822 Configure log level for application only
Do not expose internal SDL logs to users.

Fixes #1441 <https://github.com/Genymobile/scrcpy/issues/1441>
2020-06-02 18:27:23 +02:00
c4323df976 Fix incorrect log return value
The function must return a SDL_LogPriority, but returned an enum
sc_log_level.

(It was harmless because this specific return should never happen, as
asserted.)
2020-06-02 18:27:14 +02:00
8ff07e0c88 Git-ignore release directories 2020-06-02 18:25:22 +02:00
e4efd75766 Avoid repetition for some shortcuts
Keeping the key pressed generate "repeat" events. It does not make sense
to repeat the event for rotation or turn screen off.
2020-05-29 22:02:41 +02:00
0e4a6f462b Mention stay awake limitation
The "stay awake" feature only works when the device is plugged in.

Refs #1445 <https://github.com/Genymobile/scrcpy/issues/1445>
2020-05-28 23:07:28 +02:00
8b73c90427 Mention how to turn the screen on in README
Now that Ctrl+Shift+o has been reactivated, mention it in the "turn
screen off" section.
2020-05-27 19:32:02 +02:00
ef91ab2841 Update links to v1.14 in README and BUILD 2020-05-27 19:31:12 +02:00
44fa4a090e Bump version to 1.14 2020-05-27 18:26:46 +02:00
dcde578a50 Reactivate "turn device screen on" feature
This reverts commit 8c8649cfcd.

I cannot reproduce the issue with Ctrl+Shift+o on any device, so in
practice it works, it's too bad to remove the feature for a random bug
on some Android versions on some devices.
2020-05-27 18:26:46 +02:00
93a5c5149d Push clipboard text only if not null
In practice, it does not change anything (it just avoids a spurious
wake-up), but semantically, it makes no sense to call
pushClipboardText() with a null value.
2020-05-27 18:26:39 +02:00
2ca8318b9d Improve manpage formatting
Add a new line to avoid unwanted text justification
2020-05-27 12:39:42 +02:00
d499ee53c9 Initialize a default log level
Clean up has been broken by 3df63c579d.

The verbosity was correctly initialized for the Server process, but not
for the CleanUp process.

To avoid the problem, initialize a default log level.
2020-05-27 12:05:29 +02:00
8f619f337b Upgrade platform-tools (30.0.0) for Windows
Include the latest version of adb in Windows releases.
2020-05-26 19:21:05 +02:00
fc1dec0270 Paste on "set clipboard" if possible
Ctrl+Shift+v synchronizes the computer clipboard to the Android device
clipboard. This feature had been added to provide a way to copy UTF-8
text from the computer to the device.

To make such a paste more straightforward, if the device runs at least
Android 7, also send a PASTE keycode to paste immediately.

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

Fixes #786 <https://github.com/Genymobile/scrcpy/issues/786>
2020-05-25 20:59:21 +02:00
274b591d18 Fix union typo
The "set clipboard" event used the wrong union type to store its text.

In practice, it worked because both are at the same offset.
2020-05-25 18:41:05 +02:00
4bbabfb4ef Move injection methods to Device
Only the main injection method was exposed on Device, the convenience
methods were implemented in Controller.

For consistency, move them all to the Device class.
2020-05-25 03:28:33 +02:00
ffc57512b3 Avoid clipboard synchronization loop
The Android device listens for clipboard changes to synchronize with the
computer clipboard.

However, if the change comes from scrcpy (for example via Ctrl+Shift+v),
do not notify the change.
2020-05-25 03:28:33 +02:00
c7a33fac36 Log actions on the caller side
Some actions are exposed by the Device class, but logging success should
be done by the caller.
2020-05-25 03:28:33 +02:00
81573d81a0 Pass a Locale to toUpperCase()
Make lint happy.
2020-05-25 03:28:15 +02:00
5c2cf88a1d Rename THRESHOLD to threshold
Since the field is not final anymore, lint expects the name not to be
capitalized.
2020-05-25 03:22:07 +02:00
8f46e18426 Add --force-adb-forward
Add a command-line option to force "adb forward", without attempting
"adb reverse" first.

This is especially useful for using SSH tunnels without enabling remote
port forwarding.
2020-05-24 23:28:31 +02:00
ee3882f8be Fix typo in manpage 2020-05-24 23:14:30 +02:00
a3ef461d73 Add cli option to set the verbosity level
The verbosity was set either to info (in release mode) or debug (in
debug mode).

Add a command-line argument to change it, so that users can enable debug
logs using the release:

    scrcpy -Vdebug
2020-05-24 22:01:51 +02:00
3df63c579d Configure server verbosity from the client
Send the requested log level from the client.

This paves the way to configure it via a command-line argument.
2020-05-24 21:14:25 +02:00
56bff2f718 Avoid compiler warning
The field lock_video_orientation may only take values between -1 and 3
(included). But the compiler may trigger a warning on the sprintf()
call, because its type could represent values which could overflow the
string (like "-128"):

> warning: ‘%i’ directive writing between 1 and 4 bytes into a region of
> size 3 [-Wformat-overflow=]

Increase the buffer size to remove the warning.
2020-05-24 21:11:21 +02:00
080a4ee365 Add --codec-options
Add a command-line parameter to pass custom options to the device
encoder (as a comma-separated list of "key[:type]=value").

The list of possible codec options is available in the Android
documentation:
<https://d.android.com/reference/android/media/MediaFormat>

PR #1325 <https://github.com/Genymobile/scrcpy/pull/1325>
Refs #1226 <https://github.com/Genymobile/scrcpy/pull/1226>

Co-authored-by: Romain Vimont <rom@rom1v.com>
2020-05-24 14:54:34 +02:00
c7155a1954 Add unit test for big "set clipboard" event
Add a unit test to avoid regressions.

Refs #1425 <https://github.com/Genymobile/scrcpy/issues/1425>
2020-05-24 14:33:43 +02:00
517dbd9c85 Increase buffer size to fix "set clipboard" event
The buffer size must be greater than any event message.

Clipboard events may take up to 4096 bytes, so increase the buffer size.

Fixes #1425 <https://github.com/Genymobile/scrcpy/issues/1425>
2020-05-24 14:33:23 +02:00
acc4ef31df Synchronize device clipboard to computer
Automatically synchronize the device clipboard to the computer any time
it changes.

This allows seamless copy-paste from Android to the computer.

Fixes #1056 <https://github.com/Genymobile/scrcpy/issues/1056#issuecomment-631363684>
PR #1423 <https://github.com/Genymobile/scrcpy/pull/1423>
2020-05-24 14:31:49 +02:00
73e722784d Remove useless exception declaration
The interface declares it can throw a RemoteException, but the
implementation never throws such exception.
2020-05-23 18:21:54 +02:00
e1cd75792c Simplify rotation watcher call
Remove unnecessary private method (which was wrongly public).
2020-05-23 14:39:09 +02:00
ac4c8b4a3f Increase LOD bias for mipmapping
Mipmapping caused too much blurring.

Using a LOD bias of -1 instead of -0.5 seems a better compromise to
avoid low-quality downscaling while keeping sharp edges in any case.

Refs <https://github.com/Genymobile/scrcpy/issues/1394>
Refs <https://github.com/Genymobile/scrcpy/issues/40#issuecomment-591917787>
2020-05-23 14:32:16 +02:00
fae3f9eeab Remove warning when renderer is not OpenGL
Trilinear filtering can currently only be enabled for OpenGL renderers.

Do not print a warning if the renderer is not OpenGL, as it can confuses
users, while nothing is wrong.
2020-05-23 14:23:30 +02:00
f5aeecbc62 Reset window size on initialization
On macOS with renderer "metal", HiDPI scaling may be incorrect on
initialization when several displays are connected.

Resetting the window size fixes the problem.

Refs #15 <https://github.com/Genymobile/scrcpy/issues/15>
2020-05-23 14:19:09 +02:00
e40532a376 Manually position and scale the content
Position and scale the content "manually" instead of relying on the
renderer "logical size".

This avoids possible rounding differences between the computed window
size and the content size, causing one row or column of black pixels on
the bottom or on the right.

This also avoids HiDPI scale issues, by computing the scaling manually.

This will also enable to draw items at their expected size on the screen
(unscaled).

Fixes #15 <https://github.com/Genymobile/scrcpy/issues/15>
2020-05-23 14:19:09 +02:00
d860ad48e6 Extract optimal window size detection
Extract the computation to detect whether the current size of the window
is already optimal.

This will allow to reuse it for rendering.
2020-05-23 14:19:09 +02:00
ec047b501e Disable "resize to fit" in maximized state
In maximized state (but not fullscreen), it was possible to resize to
fit the device screen (with Ctrl+x or double-clicking on black borders).

This caused problems on macOS with the "expand to fullscreen" feature,
which behaves like a fullscreen mode but is seen as maximized by SDL.
In that state, resizing to fit causes unexpected results.

To keep the behavior consistent on all platforms, just disable "resize
to fit" when the window is maximized.
2020-05-23 14:19:09 +02:00
4c2e10fd74 Workaround maximized+fullscreen on Windows
On Windows, in maximized+fullscreen state, disabling fullscreen mode
unexpectedly triggers the "restored" then "maximized" events, leaving
the window in a weird state (maximized according to the events, but not
maximized visually).

Moreover, apply_pending_resize() asserts that fullscreen is disabled.

To avoid the issue, if fullscreen is set, just ignore the "restored"
event.
2020-05-23 14:19:09 +02:00
6b1da2fcff Simplify size changes in fullscreen or maximized
If the content size changes (due to rotation for example) while the
window is maximized or fullscreen, the resize must be applied once
fullscreen and maximized are disabled.

The previous strategy consisted in storing the windowed size, computing
the target size on rotation, and applying it on window restoration. But
tracking the windowed size (while ignoring the non-windowed size) was
tricky, due to unspecified order of SDL events (e.g. size changes can be
notified before "maximized" events), race conditions when reading window
flags, different behaviors on different platforms...

To simplify the whole resize management, store the old content size (the
frame size, possibly rotated) when it changes while the window is
maximized or fullscreen, so that the new optimal size can be computed on
window restoration.
2020-05-23 14:19:09 +02:00
2608b1dc62 Factorize window resize
When the content size changes, either on frame size or client rotation
changes, the window must be resized. Factorize for both cases.
2020-05-23 14:19:09 +02:00
31fa115655 Merge branch 'master' into dev 2020-05-23 14:18:47 +02:00
82446b6b0d Reference README.md from localized versions
Mention that the README.md is the only one being up-to-date.
2020-05-10 11:47:56 +02:00
24c8039244 Add Brazilian Portuguese translation
Signed-off-by: Romain Vimont <rom@rom1v.com>
2020-05-10 11:46:24 +02:00
1a9429f3c7 Improve bug report template
Insert a code block to (hopefully) increase the chances that users
format their terminal output.
2020-05-06 22:26:43 +02:00
0b4e484da0 Add a note about multi-display limitation
Secondary display mirroring is read-only before Android 10.
2020-05-06 22:21:33 +02:00
59 changed files with 2457 additions and 654 deletions

View File

@ -21,5 +21,9 @@ assignees: ''
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
On errors, please provide the output of the console (and `adb logcat` if relevant). On errors, please provide the output of the console (and `adb logcat` if relevant).
Format them between code blocks (delimited by ```).
```
Please paste terminal output in a code block.
```
Please do not post screenshots of your terminal, just post the content as text instead. Please do not post screenshots of your terminal, just post the content as text instead.

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
build/ build/
/dist/ /dist/
/build-*/
/build_*/
/release-*/
.idea/ .idea/
.gradle/ .gradle/
/x/ /x/

View File

@ -176,8 +176,8 @@ Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it avaliable from the `PATH`: make it avaliable from the `PATH`:
```bash ```bash
brew tap caskroom/versions brew tap homebrew/cask-versions
brew cask install java8 brew cask install adoptopenjdk/openjdk/adoptopenjdk8
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
@ -190,12 +190,17 @@ See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
## Common steps ## Common steps
If you want to build the server, install the [Android SDK] (_Android Studio_), If you want to build the server, install the [Android SDK] (_Android Studio_),
and set `ANDROID_HOME` to its directory. For example: and set `ANDROID_SDK_ROOT` to its directory. For example:
[Android SDK]: https://developer.android.com/studio/index.html [Android SDK]: https://developer.android.com/studio/index.html
```bash ```bash
export ANDROID_HOME=~/android/sdk # Linux
export ANDROID_SDK_ROOT=~/Android/Sdk
# Mac
export ANDROID_SDK_ROOT=~/Library/Android/sdk
# Windows
set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk
``` ```
If you don't want to build the server, use the [prebuilt server]. If you don't want to build the server, use the [prebuilt server].
@ -249,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.13`][direct-scrcpy-server] - [`scrcpy-server-v1.15`][direct-scrcpy-server]
_(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_ _(SHA-256: e160c46784f30566eae621cad88bfb34e124f945cb8274b8dfc615b613b86dd8)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-server-v1.15
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@ -100,11 +100,11 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-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.3.1-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.3.1-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.3.1-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/ffmpeg-4.3.1-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/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@ -115,11 +115,11 @@ dist-win64: build-server build-win64 build-win64-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-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.3.1-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.3.1-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.3.1-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/ffmpeg-4.3.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"

View File

@ -1,3 +1,5 @@
_Only the original [README](README.md) is guaranteed to be up-to-date._
# scrcpy (v1.11) # scrcpy (v1.11)
This document will be updated frequently along with the original Readme file This document will be updated frequently along with the original Readme file

199
README.md
View File

@ -1,4 +1,4 @@
# scrcpy (v1.13) # scrcpy (v1.15)
This application provides display and control of Android devices connected on This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access. USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
@ -49,6 +49,11 @@ A [Snap] package is available: [`scrcpy`][snap-link].
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) [snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
For Fedora, a [COPR] package is available: [`scrcpy`][copr-link].
[COPR]: https://fedoraproject.org/wiki/Category:Copr
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
@ -69,10 +74,10 @@ hard).
For Windows, for simplicity, a prebuilt archive with all the dependencies For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available: (including `adb`) is available:
- [`scrcpy-win64-v1.13.zip`][direct-win64] - [`scrcpy-win64-v1.15.zip`][direct-win64]
_(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_ _(SHA-256: dd514bb591e63ef4cd52a53c30f1153a28f59722d64690eb07bd017849edcba2)_
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-win64-v1.15.zip
It is also available in [Chocolatey]: It is also available in [Chocolatey]:
@ -289,6 +294,22 @@ From another terminal:
scrcpy scrcpy
``` ```
To avoid enabling remote port forwarding, you could force a forward connection
instead (notice the `-L` instead of `-R`):
```bash
adb kill-server # kill the local adb server on 5037
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal:
```bash
scrcpy --force-adb-forward
```
Like for wireless connections, it may be useful to reduce quality: Like for wireless connections, it may be useful to reduce quality:
``` ```
@ -338,7 +359,7 @@ scrcpy --fullscreen
scrcpy -f # short version scrcpy -f # short version
``` ```
Fullscreen can then be toggled dynamically with `Ctrl`+`f`. Fullscreen can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>.
#### Rotation #### Rotation
@ -354,18 +375,19 @@ Possibles values are:
- `2`: 180 degrees - `2`: 180 degrees
- `3`: 90 degrees clockwise - `3`: 90 degrees clockwise
The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and The rotation can also be changed dynamically with <kbd>MOD</kbd>+<kbd></kbd>
`Ctrl`+`→` _(right)_. _(left)_ and <kbd>MOD</kbd>+<kbd></kbd> _(right)_.
Note that _scrcpy_ manages 3 different rotations: Note that _scrcpy_ manages 3 different rotations:
- `Ctrl`+`r` requests the device to switch between portrait and landscape (the - <kbd>MOD</kbd>+<kbd>r</kbd> requests the device to switch between portrait and
current running app may refuse, if it does support the requested landscape (the current running app may refuse, if it does support the
orientation). requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation - `--lock-video-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the of the video sent from the device to the computer). This affects the
recording. recording.
- `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This - `--rotation` (or <kbd>MOD</kbd>+<kbd></kbd>/<kbd>MOD</kbd>+<kbd></kbd>)
affects only the display, not the recording. rotates only the window content. This affects only the display, not the
recording.
### Other mirroring options ### Other mirroring options
@ -395,9 +417,13 @@ The list of display ids can be retrieved by:
adb shell dumpsys display # search "mDisplayId=" in the output adb shell dumpsys display # search "mDisplayId=" in the output
``` ```
The secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored in read-only).
#### Stay awake #### Stay awake
To prevent the device to sleep after some delay: To prevent the device to sleep after some delay when the device is plugged in:
```bash ```bash
scrcpy --stay-awake scrcpy --stay-awake
@ -417,11 +443,16 @@ scrcpy --turn-screen-off
scrcpy -S scrcpy -S
``` ```
Or by pressing `Ctrl`+`o` at any time. Or by pressing <kbd>MOD</kbd>+<kbd>o</kbd> at any time.
To turn it back on, press `POWER` (or `Ctrl`+`p`). To turn it back on, press <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
It can be useful to also prevent the device to sleep: On Android, the `POWER` button always turns the screen on. For convenience, if
`POWER` is sent via scrcpy (via right-click or <kbd>Ctrl</kbd>+<kbd>p</kbd>), it
will force to turn the screen off after a small delay (on a best effort basis).
The physical `POWER` button will still cause the screen to be turned on.
It can also be useful to prevent the device from sleeping:
```bash ```bash
scrcpy --turn-screen-off --stay-awake scrcpy --turn-screen-off --stay-awake
@ -459,24 +490,60 @@ scrcpy -t
Note that it only shows _physical_ touches (with the finger on the device). Note that it only shows _physical_ touches (with the finger on the device).
#### Disable screensaver
By default, scrcpy does not prevent the screensaver to run on the computer.
To disable it:
```bash
scrcpy --disable-screensaver
```
### Input control ### Input control
#### Rotate device screen #### Rotate device screen
Press `Ctrl`+`r` to switch between portrait and landscape modes. Press <kbd>MOD</kbd>+<kbd>r</kbd> to switch between portrait and landscape
modes.
Note that it rotates only if the application in foreground supports the Note that it rotates only if the application in foreground supports the
requested orientation. requested orientation.
#### Copy-paste #### Copy-paste
It is possible to synchronize clipboards between the computer and the device, in Any time the Android clipboard changes, it is automatically synchronized to the
both directions: computer clipboard.
- `Ctrl`+`c` copies the device clipboard to the computer clipboard; Any <kbd>Ctrl</kbd> shortcut is forwarded to the device. In particular:
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; - <kbd>Ctrl</kbd>+<kbd>c</kbd> typically copies
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but - <kbd>Ctrl</kbd>+<kbd>x</kbd> typically cuts
breaks non-ASCII characters). - <kbd>Ctrl</kbd>+<kbd>v</kbd> typically pastes (after computer-to-device
clipboard synchronization)
This typically works as you expect.
The actual behavior depends on the active application though. For example,
_Termux_ sends SIGINT on <kbd>Ctrl</kbd>+<kbd>c</kbd> instead, and _K-9 Mail_
composes a new message.
To copy, cut and paste in such cases (but only supported on Android >= 7):
- <kbd>MOD</kbd>+<kbd>c</kbd> injects `COPY`
- <kbd>MOD</kbd>+<kbd>x</kbd> injects `CUT`
- <kbd>MOD</kbd>+<kbd>v</kbd> injects `PASTE` (after computer-to-device
clipboard synchronization)
In addition, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> allows to inject the
computer clipboard text as a sequence of key events. This is useful when the
component does not accept text pasting (for example in _Termux_), but it can
break non-ASCII content.
**WARNING:** Pasting the computer clipboard to the device (either via
<kbd>Ctrl</kbd>+<kbd>v</kbd> or <kbd>MOD</kbd>+<kbd>v</kbd>) copies the content
into the device clipboard. As a consequence, any Android application could read
its content. You should avoid to paste sensitive content (like passwords) that
way.
#### Text injection preference #### Text injection preference
@ -500,6 +567,18 @@ scrcpy --prefer-text
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
#### Key repeat
By default, holding a key down generates repeated key events. This can cause
performance problems in some games, where these events are useless anyway.
To avoid forwarding repeated key events:
```bash
scrcpy --no-key-repeat
```
### File drop ### File drop
#### Install APK #### Install APK
@ -526,42 +605,66 @@ scrcpy --push-target /sdcard/foo/bar/
### Audio forwarding ### Audio forwarding
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). Audio is not forwarded by _scrcpy_. Use [sndcpy].
Also see [issue #14]. Also see [issue #14].
[USBaudio]: https://github.com/rom1v/usbaudio [sndcpy]: https://github.com/rom1v/sndcpy
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 [issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts ## Shortcuts
| Action | Shortcut | Shortcut (macOS) In the following list, <kbd>MOD</kbd> is the shortcut modifier. By default, it's
| -------------------------------------- |:----------------------------- |:----------------------------- (left) <kbd>Alt</kbd> or (left) <kbd>Super</kbd>.
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_ It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`,
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_ `lalt`, `ralt`, `lsuper` and `rsuper`. For example:
| 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¹_ ```bash
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ # use RCtrl for shortcuts
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ scrcpy --shortcut-mod=rctrl
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` # use either LCtrl+LAlt or LSuper for shortcuts
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ scrcpy --shortcut-mod=lctrl+lalt,lsuper
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ ```
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Power on | _Right-click²_ | _Right-click²_ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` [Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` | Action | Shortcut
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` | ------------------------------------------- |:-----------------------------
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` | Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` | Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` | Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd>
| Click on `MENU` (unlock screen) | <kbd>MOD</kbd>+<kbd>m</kbd>
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd></kbd> _(up)_
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd></kbd> _(down)_
| Click on `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
| Power on | _Right-click²_
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd>
| Collapse notification panel | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard³ | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard³ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste³ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._ _²Right-click turns the screen on if it was off, presses BACK otherwise._
_³Only on Android >= 7._
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
handled by the active application.
## Custom paths ## Custom paths

531
README.pt-br.md Normal file
View File

@ -0,0 +1,531 @@
_Only the original [README](README.md) is guaranteed to be up-to-date._
# scrcpy (v1.12.1)
Esta aplicação fornece visualização e controle de dispositivos Android conectados via
USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso root.
Funciona em _GNU/Linux_, _Windows_ e _macOS_.
![screenshot](assets/screenshot-debian-600.jpg)
Foco em:
- **leveza** (Nativo, mostra apenas a tela do dispositivo)
- **performance** (30~60fps)
- **qualidade** (1920×1080 ou acima)
- **baixa latência** ([35~70ms][lowlatency])
- **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem)
- **não intrusivo** (nada é deixado instalado no dispositivo)
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
## Requisitos
O Dispositivo Android requer pelo menos a API 21 (Android 5.0).
Tenha certeza de ter [ativado a depuração USB][enable-adb] no(s) seu(s) dispositivo(s).
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
Em alguns dispositivos, você também precisará ativar [uma opção adicional][control] para controlá-lo usando o teclado e mouse.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
## Obtendo o app
### Linux
No Debian (_em testes_ e _sid_ por enquanto):
```
apt install scrcpy
```
O pacote [Snap] está disponível: [`scrcpy`][snap-link].
[snap-link]: https://snapstats.org/snaps/scrcpy
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link].
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
Você também pode [compilar a aplicação manualmente][BUILD] (não se preocupe, não é tão difícil).
### Windows
Para Windows, para simplicidade, um arquivo pré-compilado com todas as dependências
(incluindo `adb`) está disponível:
- [`scrcpy-win64-v1.12.1.zip`][direct-win64]
_(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip
Também disponível em [Chocolatey]:
[Chocolatey]: https://chocolatey.org/
```bash
choco install scrcpy
choco install adb # se você ainda não o tem
```
E no [Scoop]:
```bash
scoop install scrcpy
scoop install adb # se você ainda não o tem
```
[Scoop]: https://scoop.sh
Você também pode [compilar a aplicação manualmente][BUILD].
### macOS
A aplicação está disponível em [Homebrew]. Apenas a instale:
[Homebrew]: https://brew.sh/
```bash
brew install scrcpy
```
Você precisa do `adb`, acessível através do seu `PATH`. Se você ainda não o tem:
```bash
brew cask install android-platform-tools
```
Você também pode [compilar a aplicação manualmente][BUILD].
## Executar
Plugue um dispositivo Android e execute:
```bash
scrcpy
```
Também aceita argumentos de linha de comando, listados por:
```bash
scrcpy --help
```
## Funcionalidades
### Configuração de captura
#### Redução de tamanho
Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para
aumentar performance.
Para limitar ambos(largura e altura) para algum valor (ex: 1024):
```bash
scrcpy --max-size 1024
scrcpy -m 1024 # versão reduzida
```
A outra dimensão é calculada para que a proporção do dispositivo seja preservada.
Dessa forma, um dispositivo em 1920x1080 será espelhado em 1024x576.
#### Mudanças no bit-rate
O Padrão de bit-rate é 8 mbps. Para mudar o bitrate do vídeo (ex: para 2 Mbps):
```bash
scrcpy --bit-rate 2M
scrcpy -b 2M # versão reduzida
```
#### Limitar frame rates
Em dispositivos com Android >= 10, a captura de frame rate pode ser limitada:
```bash
scrcpy --max-fps 15
```
#### Cortar
A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela.
Isso é útil por exemplo, ao espelhar apenas um olho do Oculus Go:
```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0)
```
Se `--max-size` também for especificado, redimensionar é aplicado após os cortes.
### Gravando
É possível gravar a tela enquanto ocorre o espelhamento:
```bash
scrcpy --record file.mp4
scrcpy -r file.mkv
```
Para desativar o espelhamento durante a gravação:
```bash
scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv
# interrompe a gravação com Ctrl+C
# Ctrl+C não encerrar propriamente no Windows, então desconecte o dispositivo
```
"Frames pulados" são gravados, mesmo que não sejam mostrado em tempo real (por motivos de performance).
Frames tem seu _horário_ _carimbado_ no dispositivo, então [Variação de atraso nos pacotes] não impacta na gravação do arquivo.
[Variação de atraso de pacote]: https://en.wikipedia.org/wiki/Packet_delay_variation
### Conexão
#### Wireless/Sem fio
_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se] à um dispositivo via TCP/IP:
1. Conecte o dispositivo a mesma rede Wi-Fi do seu computador.
2. Pegue o endereço de IP do seu dispositivo (Em Configurações → Sobre o Telefone → Status).
3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`.
4. Desplugue seu dispositivo.
5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua o `DEVICE_IP`)_.
6. Execute `scrcpy` como de costume.
Pode ser útil diminuir o bit-rate e a resolução:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # versão reduzida
```
[conectar-se]: https://developer.android.com/studio/command-line/adb.html#wireless
#### N-dispositivos
Se alguns dispositivos estão listados em `adb devices`, você precisa especificar o _serial_:
```bash
scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # versão reduzida
```
Se o dispositivo está conectado via TCP/IP:
```bash
scrcpy --serial 192.168.0.1:5555
scrcpy -s 192.168.0.1:5555 # versão reduzida
```
Você pode iniciar algumas instâncias do _scrcpy_ para alguns dispositivos.
#### Conexão via SSH
Para conectar-se à um dispositivo remoto, é possível se conectar um cliente local `adb` à um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo _adb_):
```bash
adb kill-server # encerra o servidor local na 5037
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
# mantém isso aberto
```
De outro terminal:
```bash
scrcpy
```
Igual para conexões sem fio, pode ser útil reduzir a qualidade:
```
scrcpy -b2M -m800 --max-fps 15
```
### Configurações de Janela
#### Título
Por padrão, o título da janela é o modelo do dispositivo. Isto pode ser mudado:
```bash
scrcpy --window-title 'Meu dispositivo'
```
#### Posição e tamanho
A posição e tamanho iniciais da janela podem ser especificados:
```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
```
#### Sem bordas
Para desativar decorações da janela:
```bash
scrcpy --window-borderless
```
#### Sempre visível
Para manter a janela do scrcpy sempre visível:
```bash
scrcpy --always-on-top
```
#### Tela cheia
A aplicação pode ser iniciada diretamente em tela cheia:
```bash
scrcpy --fullscreen
scrcpy -f # versão reduzida
```
Tela cheia pode ser alternada dinamicamente com `Ctrl`+`f`.
### Outras opções de espelhamento
#### Apenas leitura
Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada, eventos de mouse, arrastar e soltar arquivos):
```bash
scrcpy --no-control
scrcpy -n
```
#### Desligar a tela
É possível desligar a tela do dispositivo durante o início do espelhamento com uma opção de linha de comando:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
Ou apertando `Ctrl`+`o` durante qualquer momento.
Para ligar novamente, pressione `POWER` (ou `Ctrl`+`p`).
#### Frames expirados de renderização
Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado disponível e descarta o anterior.
Para forçar a renderização de todos os frames ( com o custo de aumento de latência), use:
```bash
scrcpy --render-expired-frames
```
#### Mostrar toques
Para apresentações, pode ser útil mostrar toques físicos(dispositivo físico).
Android fornece esta funcionalidade nas _Opções do Desenvolvedor_.
_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e desativar no encerramento:
```bash
scrcpy --show-touches
scrcpy -t
```
Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo).
### Controle de entrada
#### Rotacionar a tela do dispositivo
Pressione `Ctrl`+`r` para mudar entre os modos Retrato e Paisagem.
Note que só será rotacionado se a aplicação em primeiro plano tiver suporte para o modo requisitado.
#### Copiar-Colar
É possível sincronizar áreas de transferência entre computador e o dispositivo,
para ambas direções:
- `Ctrl`+`c` copia a área de transferência do dispositivo para a área de trasferência do computador;
- `Ctrl`+`Shift`+`v` copia a área de transferência do computador para a área de transferência do dispositivo;
- `Ctrl`+`v` _cola_ a área de transferência do computador como uma sequência de eventos de texto (mas
quebra caracteres não-ASCII).
#### Preferências de injeção de texto
Existe dois tipos de [eventos][textevents] gerados ao digitar um texto:
- _eventos de teclas_, sinalizando que a tecla foi pressionada ou solta;
- _eventos de texto_, sinalizando que o texto foi inserido.
Por padrão, letras são injetadas usando eventos de teclas, assim teclados comportam-se
como esperado em jogos (normalmente para tecladas WASD)
Mas isto pode [causar problemas][prefertext]. Se você encontrar tal problema,
pode evitá-lo usando:
```bash
scrcpy --prefer-text
```
(mas isto vai quebrar o comportamento do teclado em jogos)
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
### Transferência de arquivo
#### Instalar APK
Para instalar um APK, arraste e solte o arquivo APK(com extensão `.apk`) na janela _scrcpy_.
Não existe feedback visual, um log é imprimido no console.
#### Enviar arquivo para o dispositivo
Para enviar um arquivo para o diretório `/sdcard/` no dispositivo, arraste e solte um arquivo não APK para a janela do
_scrcpy_.
Não existe feedback visual, um log é imprimido no console.
O diretório alvo pode ser mudado ao iniciar:
```bash
scrcpy --push-target /sdcard/foo/bar/
```
### Encaminhamento de áudio
Áudio não é encaminhando pelo _scrcpy_. Use [USBaudio] (Apenas linux).
Também veja [issue #14].
[USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## Atalhos
| Ação | Atalho | Atalho (macOS)
| ------------------------------------------------------------- |:------------------------------- |:-----------------------------
| Alternar para modo de tela cheia | `Ctrl`+`f` | `Cmd`+`f`
| Redimensionar janela para pixel-perfect(Escala 1:1) | `Ctrl`+`g` | `Cmd`+`g`
| Redimensionar janela para tirar as bordas pretas | `Ctrl`+`x` \| _Clique-duplo¹_ | `Cmd`+`x` \| _Clique-duplo¹_
| Clicar em `HOME` | `Ctrl`+`h` \| _Clique-central_ | `Ctrl`+`h` \| _Clique-central_
| Clicar em `BACK` | `Ctrl`+`b` \| _Clique-direito²_ | `Cmd`+`b` \| _Clique-direito²_
| Clicar em `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
| Clicar em `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
| Clicar em `VOLUME_UP` | `Ctrl`+`↑` _(cima)_ | `Cmd`+`↑` _(cima)_
| Clicar em `VOLUME_DOWN` | `Ctrl`+`↓` _(baixo)_ | `Cmd`+`↓` _(baixo)_
| Clicar em `POWER` | `Ctrl`+`p` | `Cmd`+`p`
| Ligar | _Clique-direito²_ | _Clique-direito²_
| Desligar a tela do dispositivo | `Ctrl`+`o` | `Cmd`+`o`
| Rotacionar tela do dispositivo | `Ctrl`+`r` | `Cmd`+`r`
| Expandir painel de notificação | `Ctrl`+`n` | `Cmd`+`n`
| Esconder painel de notificação | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copiar área de transferência do dispositivo para o computador | `Ctrl`+`c` | `Cmd`+`c`
| Colar área de transferência do computador para o dispositivo | `Ctrl`+`v` | `Cmd`+`v`
| Copiar área de transferência do computador para dispositivo | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Ativar/desativar contador de FPS(Frames por segundo) | `Ctrl`+`i` | `Cmd`+`i`
_¹Clique-duplo em bordas pretas para removê-las._
_²Botão direito liga a tela se ela estiver desligada, clique BACK para o contrário._
## Caminhos personalizados
Para usar um binário específico _adb_, configure seu caminho na variável de ambiente `ADB`:
ADB=/caminho/para/adb scrcpy
Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em
`SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## Por quê _scrcpy_?
Um colega me desafiou a encontrar um nome impronunciável como [gnirehtet].
[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een.
[gnirehtet]: https://github.com/Genymobile/gnirehtet
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
## Como compilar?
Veja [BUILD].
[BUILD]: BUILD.md
## Problemas comuns
Veja [FAQ](FAQ.md).
## Desenvolvedores
Leia a [developers page].
[developers page]: DEVELOP.md
## Licença
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.
## Artigos
- [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

@ -164,12 +164,12 @@ if get_option('buildtype') == 'debug'
'src/cli.c', 'src/cli.c',
'src/util/str_util.c', 'src/util/str_util.c',
]], ]],
['test_control_event_serialize', [ ['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c', 'tests/test_control_msg_serialize.c',
'src/control_msg.c', 'src/control_msg.c',
'src/util/str_util.c', 'src/util/str_util.c',
]], ]],
['test_device_event_deserialize', [ ['test_device_msg_deserialize', [
'tests/test_device_msg_deserialize.c', 'tests/test_device_msg_deserialize.c',
'src/device_msg.c', 'src/device_msg.c',
]], ]],
@ -186,7 +186,7 @@ if get_option('buildtype') == 'debug'
exe = executable(t[0], t[1], exe = executable(t[0], t[1],
include_directories: src_dir, include_directories: src_dir,
dependencies: dependencies, dependencies: dependencies,
c_args: ['-DSDL_MAIN_HANDLED']) c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
test(t[0], exe) test(t[0], exe)
endforeach endforeach
endif endif

View File

@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
Default is 8000000. 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 .TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. Crop the device screen on the server.
@ -33,6 +43,10 @@ The values are expressed in the device natural orientation (typically, portrait
.B \-\-max\-size .B \-\-max\-size
value is computed on the cropped size. value is computed on the cropped size.
.TP
.BI "\-\-disable-screensaver"
Disable screensaver while scrcpy is running.
.TP .TP
.BI "\-\-display " id .BI "\-\-display " id
Specify the display id to mirror. Specify the display id to mirror.
@ -42,6 +56,10 @@ The list of possible display ids can be listed by "adb shell dumpsys display"
Default is 0. Default is 0.
.TP
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
.TP .TP
.B \-f, \-\-fullscreen .B \-f, \-\-fullscreen
Start in fullscreen. Start in fullscreen.
@ -78,6 +96,10 @@ Do not display device (only when screen recording is enabled).
.B \-\-no\-mipmaps .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. 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
.B \-\-no\-key\-repeat
Do not forward repeated key events when a key is held down.
.TP .TP
.BI "\-p, \-\-port " port[:port] .BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen. Set the TCP port (range) used by the client to listen.
@ -115,6 +137,7 @@ Force recording format (either mp4 or mkv).
Request SDL to use the given render driver (this is just a hint). Request SDL to use the given render driver (this is just a hint).
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER .UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
.UE .UE
@ -130,13 +153,23 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
.BI "\-s, \-\-serial " number .BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb. The device serial number. Mandatory only if several devices are connected to adb.
.TP
.BI "\-\-shortcut\-mod " key[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper".
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP .TP
.B \-S, \-\-turn\-screen\-off .B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately. Turn the device screen off immediately.
.TP .TP
.B \-t, \-\-show\-touches .B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit.. Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy). It only shows physical touches (not clicks from scrcpy).
@ -144,9 +177,15 @@ It only shows physical touches (not clicks from scrcpy).
.B \-v, \-\-version .B \-v, \-\-version
Print the version of scrcpy. Print the version of scrcpy.
.TP
.BI "\-V, \-\-verbosity " value
Set the log level ("debug", "info", "warn" or "error").
Default is "info" for release builds, "debug" for debug builds.
.TP .TP
.B \-w, \-\-stay-awake .B \-w, \-\-stay-awake
Keep the device on while scrcpy is running. Keep the device on while scrcpy is running, when the device is plugged in.
.TP .TP
.B \-\-window\-borderless .B \-\-window\-borderless
@ -182,52 +221,55 @@ Default is 0 (automatic).\n
.SH SHORTCUTS .SH SHORTCUTS
In the following list, MOD is the shortcut modifier. By default, it's (left)
Alt or (left) Super, but it can be configured by \-\-shortcut-mod.
.TP .TP
.B Ctrl+f .B MOD+f
Switch fullscreen mode Switch fullscreen mode
.TP .TP
.B Ctrl+Left .B MOD+Left
Rotate display left Rotate display left
.TP .TP
.B Ctrl+Right .B MOD+Right
Rotate display right Rotate display right
.TP .TP
.B Ctrl+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
.TP .TP
.B Ctrl+x, Double\-click on black borders .B MOD+w, Double\-click on black borders
Resize window to remove black borders Resize window to remove black borders
.TP .TP
.B Ctrl+h, Home, Middle\-click .B MOD+h, Home, Middle\-click
Click on HOME Click on HOME
.TP .TP
.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) .B MOD+b, MOD+Backspace, Right\-click (when screen is on)
Click on BACK Click on BACK
.TP .TP
.B Ctrl+s .B MOD+s
Click on APP_SWITCH Click on APP_SWITCH
.TP .TP
.B Ctrl+m .B MOD+m
Click on MENU Click on MENU
.TP .TP
.B Ctrl+Up .B MOD+Up
Click on VOLUME_UP Click on VOLUME_UP
.TP .TP
.B Ctrl+Down .B MOD+Down
Click on VOLUME_DOWN Click on VOLUME_DOWN
.TP .TP
.B Ctrl+p .B MOD+p
Click on POWER (turn screen on/off) Click on POWER (turn screen on/off)
.TP .TP
@ -235,35 +277,43 @@ Click on POWER (turn screen on/off)
Turn screen on Turn screen on
.TP .TP
.B Ctrl+o .B MOD+o
Turn device screen off (keep mirroring) Turn device screen off (keep mirroring)
.TP .TP
.B Ctrl+r .B MOD+Shift+o
Turn device screen on
.TP
.B MOD+r
Rotate device screen Rotate device screen
.TP .TP
.B Ctrl+n .B MOD+n
Expand notification panel Expand notification panel
.TP .TP
.B Ctrl+Shift+n .B MOD+Shift+n
Collapse notification panel Collapse notification panel
.TP .TP
.B Ctrl+c .B Mod+c
Copy device clipboard to computer Copy to clipboard (inject COPY keycode, Android >= 7 only)
.TP .TP
.B Ctrl+v .B Mod+x
Paste computer clipboard to device Cut to clipboard (inject CUT keycode, Android >= 7 only)
.TP .TP
.B Ctrl+Shift+v .B MOD+v
Copy computer clipboard to device Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only)
.TP .TP
.B Ctrl+i .B MOD+Shift+v
Inject computer clipboard text as a sequence of key events
.TP
.B MOD+i
Enable/disable FPS counter (print frames/second in logs) Enable/disable FPS counter (print frames/second in logs)
.TP .TP

View File

@ -3,20 +3,16 @@
#include <assert.h> #include <assert.h>
#include <getopt.h> #include <getopt.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h>
#include <unistd.h> #include <unistd.h>
#include "config.h" #include "config.h"
#include "recorder.h" #include "scrcpy.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
void void
scrcpy_print_usage(const char *arg0) { scrcpy_print_usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr, fprintf(stderr,
"Usage: %s [options]\n" "Usage: %s [options]\n"
"\n" "\n"
@ -30,12 +26,24 @@ scrcpy_print_usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n" " Default is %d.\n"
"\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 width:height:x:y\n"
" Crop the device screen on the server.\n" " Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n" " The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n" " (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n" " Any --max-size value is computed on the cropped size.\n"
"\n" "\n"
" --disable-screensaver\n"
" Disable screensaver while scrcpy is running.\n"
"\n"
" --display id\n" " --display id\n"
" Specify the display id to mirror.\n" " Specify the display id to mirror.\n"
"\n" "\n"
@ -45,6 +53,10 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
" Default is 0.\n" " Default is 0.\n"
"\n" "\n"
" --force-adb-forward\n"
" Do not attempt to use \"adb reverse\" to connect to the\n"
" the device.\n"
"\n"
" -f, --fullscreen\n" " -f, --fullscreen\n"
" Start in fullscreen.\n" " Start in fullscreen.\n"
"\n" "\n"
@ -80,6 +92,9 @@ scrcpy_print_usage(const char *arg0) {
" mipmaps are automatically generated to improve downscaling\n" " mipmaps are automatically generated to improve downscaling\n"
" quality. This option disables the generation of mipmaps.\n" " quality. This option disables the generation of mipmaps.\n"
"\n" "\n"
" --no-key-repeat\n"
" Do not forward repeated key events when a key is held down.\n"
"\n"
" -p, --port port[:port]\n" " -p, --port port[:port]\n"
" Set the TCP port (range) used by the client to listen.\n" " Set the TCP port (range) used by the client to listen.\n"
" Default is %d:%d.\n" " Default is %d:%d.\n"
@ -126,6 +141,19 @@ scrcpy_print_usage(const char *arg0) {
" The device serial number. Mandatory only if several devices\n" " The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n" " are connected to adb.\n"
"\n" "\n"
" --shortcut-mod key[+...]][,...]\n"
" Specify the modifiers to use for scrcpy shortcuts.\n"
" Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n"
" \"lsuper\" and \"rsuper\".\n"
"\n"
" A shortcut can consist in several keys, separated by '+'.\n"
" Several shortcuts can be specified, separated by ','.\n"
"\n"
" For example, to use either LCtrl+LAlt or LSuper for scrcpy\n"
" shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"\n"
" Default is \"lalt,lsuper\" (left-Alt or left-Super).\n"
"\n"
" -S, --turn-screen-off\n" " -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n" " Turn the device screen off immediately.\n"
"\n" "\n"
@ -136,9 +164,18 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
" -v, --version\n" " -v, --version\n"
" Print the version of scrcpy.\n" " Print the version of scrcpy.\n"
"\n"
" -V, --verbosity value\n"
" Set the log level (debug, info, warn or error).\n"
#ifndef NDEBUG
" Default is debug.\n"
#else
" Default is info.\n"
#endif
"\n" "\n"
" -w, --stay-awake\n" " -w, --stay-awake\n"
" Keep the device on while scrcpy is running.\n" " Keep the device on while scrcpy is running, when the device\n"
" is plugged in.\n"
"\n" "\n"
" --window-borderless\n" " --window-borderless\n"
" Disable window decorations (display borderless window).\n" " Disable window decorations (display borderless window).\n"
@ -164,71 +201,82 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
"Shortcuts:\n" "Shortcuts:\n"
"\n" "\n"
" " CTRL_OR_CMD "+f\n" " In the following list, MOD is the shortcut modifier. By default,\n"
" it's (left) Alt or (left) Super, but it can be configured by\n"
" --shortcut-mod.\n"
"\n"
" MOD+f\n"
" Switch fullscreen mode\n" " Switch fullscreen mode\n"
"\n" "\n"
" " CTRL_OR_CMD "+Left\n" " MOD+Left\n"
" Rotate display left\n" " Rotate display left\n"
"\n" "\n"
" " CTRL_OR_CMD "+Right\n" " MOD+Right\n"
" Rotate display right\n" " Rotate display right\n"
"\n" "\n"
" " CTRL_OR_CMD "+g\n" " MOD+g\n"
" Resize window to 1:1 (pixel-perfect)\n" " Resize window to 1:1 (pixel-perfect)\n"
"\n" "\n"
" " CTRL_OR_CMD "+x\n" " MOD+w\n"
" Double-click on black borders\n" " Double-click on black borders\n"
" Resize window to remove black borders\n" " Resize window to remove black borders\n"
"\n" "\n"
" Ctrl+h\n" " MOD+h\n"
" Middle-click\n" " Middle-click\n"
" Click on HOME\n" " Click on HOME\n"
"\n" "\n"
" " CTRL_OR_CMD "+b\n" " MOD+b\n"
" " CTRL_OR_CMD "+Backspace\n" " MOD+Backspace\n"
" Right-click (when screen is on)\n" " Right-click (when screen is on)\n"
" Click on BACK\n" " Click on BACK\n"
"\n" "\n"
" " CTRL_OR_CMD "+s\n" " MOD+s\n"
" Click on APP_SWITCH\n" " Click on APP_SWITCH\n"
"\n" "\n"
" Ctrl+m\n" " MOD+m\n"
" Click on MENU\n" " Click on MENU\n"
"\n" "\n"
" " CTRL_OR_CMD "+Up\n" " MOD+Up\n"
" Click on VOLUME_UP\n" " Click on VOLUME_UP\n"
"\n" "\n"
" " CTRL_OR_CMD "+Down\n" " MOD+Down\n"
" Click on VOLUME_DOWN\n" " Click on VOLUME_DOWN\n"
"\n" "\n"
" " CTRL_OR_CMD "+p\n" " MOD+p\n"
" Click on POWER (turn screen on/off)\n" " Click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click (when screen is off)\n" " Right-click (when screen is off)\n"
" Power on\n" " Power on\n"
"\n" "\n"
" " CTRL_OR_CMD "+o\n" " MOD+o\n"
" Turn device screen off (keep mirroring)\n" " Turn device screen off (keep mirroring)\n"
"\n" "\n"
" " CTRL_OR_CMD "+r\n" " MOD+Shift+o\n"
" Turn device screen on\n"
"\n"
" MOD+r\n"
" Rotate device screen\n" " Rotate device screen\n"
"\n" "\n"
" " CTRL_OR_CMD "+n\n" " MOD+n\n"
" Expand notification panel\n" " Expand notification panel\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+n\n" " MOD+Shift+n\n"
" Collapse notification panel\n" " Collapse notification panel\n"
"\n" "\n"
" " CTRL_OR_CMD "+c\n" " MOD+c\n"
" Copy device clipboard to computer\n" " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n"
"\n" "\n"
" " CTRL_OR_CMD "+v\n" " MOD+x\n"
" Paste computer clipboard to device\n" " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n"
"\n" "\n"
" " CTRL_OR_CMD "+Shift+v\n" " MOD+v\n"
" Copy computer clipboard to device\n" " Copy computer clipboard to device, then paste (inject PASTE\n"
" keycode, Android >= 7 only)\n"
"\n" "\n"
" " CTRL_OR_CMD "+i\n" " MOD+Shift+v\n"
" Inject computer clipboard text as a sequence of key events\n"
"\n"
" MOD+i\n"
" Enable/disable FPS counter (print frames/second in logs)\n" " Enable/disable FPS counter (print frames/second in logs)\n"
"\n" "\n"
" Drag & drop APK file\n" " Drag & drop APK file\n"
@ -353,10 +401,10 @@ parse_rotation(const char *s, uint8_t *rotation) {
static bool static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value");
if (!strcmp(s, "auto")) { if (!strcmp(s, "auto")) {
*position = WINDOW_POSITION_UNDEFINED; *position = SC_WINDOW_POSITION_UNDEFINED;
return true; return true;
} }
@ -385,7 +433,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
} }
static bool static bool
parse_port_range(const char *s, struct port_range *port_range) { parse_port_range(const char *s, struct sc_port_range *port_range) {
long values[2]; long values[2];
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
if (!count) { if (!count) {
@ -425,20 +473,141 @@ parse_display_id(const char *s, uint16_t *display_id) {
} }
static bool static bool
parse_record_format(const char *optarg, enum recorder_format *format) { parse_log_level(const char *s, enum sc_log_level *log_level) {
if (!strcmp(s, "debug")) {
*log_level = SC_LOG_LEVEL_DEBUG;
return true;
}
if (!strcmp(s, "info")) {
*log_level = SC_LOG_LEVEL_INFO;
return true;
}
if (!strcmp(s, "warn")) {
*log_level = SC_LOG_LEVEL_WARN;
return true;
}
if (!strcmp(s, "error")) {
*log_level = SC_LOG_LEVEL_ERROR;
return true;
}
LOGE("Could not parse log level: %s", s);
return false;
}
// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt")
// returns a bitwise-or of SC_MOD_* constants (or 0 on error)
static unsigned
parse_shortcut_mods_item(const char *item, size_t len) {
unsigned mod = 0;
for (;;) {
char *plus = strchr(item, '+');
// strchr() does not consider the "len" parameter, to it could find an
// occurrence too far in the string (there is no strnchr())
bool has_plus = plus && plus < item + len;
assert(!has_plus || plus > item);
size_t key_len = has_plus ? (size_t) (plus - item) : len;
#define STREQ(literal, s, len) \
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
if (STREQ("lctrl", item, key_len)) {
mod |= SC_MOD_LCTRL;
} else if (STREQ("rctrl", item, key_len)) {
mod |= SC_MOD_RCTRL;
} else if (STREQ("lalt", item, key_len)) {
mod |= SC_MOD_LALT;
} else if (STREQ("ralt", item, key_len)) {
mod |= SC_MOD_RALT;
} else if (STREQ("lsuper", item, key_len)) {
mod |= SC_MOD_LSUPER;
} else if (STREQ("rsuper", item, key_len)) {
mod |= SC_MOD_RSUPER;
} else {
LOGW("Unknown modifier key: %.*s", (int) key_len, item);
return 0;
}
#undef STREQ
if (!has_plus) {
break;
}
item = plus + 1;
assert(len >= key_len + 1);
len -= key_len + 1;
}
return mod;
}
static bool
parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
unsigned count = 0;
unsigned current = 0;
// LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper"
for (;;) {
char *comma = strchr(s, ',');
if (comma && count == SC_MAX_SHORTCUT_MODS - 1) {
assert(count < SC_MAX_SHORTCUT_MODS);
LOGW("Too many shortcut modifiers alternatives");
return false;
}
assert(!comma || comma > s);
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
unsigned mod = parse_shortcut_mods_item(s, limit);
if (!mod) {
LOGE("Invalid modifier keys: %.*s", (int) limit, s);
return false;
}
mods->data[current++] = mod;
++count;
if (!comma) {
break;
}
s = comma + 1;
}
mods->count = count;
return true;
}
#ifdef SC_TEST
// expose the function to unit-tests
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
return parse_shortcut_mods(s, mods);
}
#endif
static bool
parse_record_format(const char *optarg, enum sc_record_format *format) {
if (!strcmp(optarg, "mp4")) { if (!strcmp(optarg, "mp4")) {
*format = RECORDER_FORMAT_MP4; *format = SC_RECORD_FORMAT_MP4;
return true; return true;
} }
if (!strcmp(optarg, "mkv")) { if (!strcmp(optarg, "mkv")) {
*format = RECORDER_FORMAT_MKV; *format = SC_RECORD_FORMAT_MKV;
return true; return true;
} }
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false; return false;
} }
static enum recorder_format static enum sc_record_format
guess_record_format(const char *filename) { guess_record_format(const char *filename) {
size_t len = strlen(filename); size_t len = strlen(filename);
if (len < 4) { if (len < 4) {
@ -446,10 +615,10 @@ guess_record_format(const char *filename) {
} }
const char *ext = &filename[len - 4]; const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) { if (!strcmp(ext, ".mp4")) {
return RECORDER_FORMAT_MP4; return SC_RECORD_FORMAT_MP4;
} }
if (!strcmp(ext, ".mkv")) { if (!strcmp(ext, ".mkv")) {
return RECORDER_FORMAT_MKV; return SC_RECORD_FORMAT_MKV;
} }
return 0; return 0;
} }
@ -472,14 +641,24 @@ guess_record_format(const char *filename) {
#define OPT_ROTATION 1015 #define OPT_ROTATION 1015
#define OPT_RENDER_DRIVER 1016 #define OPT_RENDER_DRIVER 1016
#define OPT_NO_MIPMAPS 1017 #define OPT_NO_MIPMAPS 1017
#define OPT_CODEC_OPTIONS 1018
#define OPT_FORCE_ADB_FORWARD 1019
#define OPT_DISABLE_SCREENSAVER 1020
#define OPT_SHORTCUT_MOD 1021
#define OPT_NO_KEY_REPEAT 1022
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
{"crop", required_argument, NULL, OPT_CROP}, {"crop", required_argument, NULL, OPT_CROP},
{"disable-screensaver", no_argument, NULL,
OPT_DISABLE_SCREENSAVER},
{"display", required_argument, NULL, OPT_DISPLAY_ID}, {"display", required_argument, NULL, OPT_DISPLAY_ID},
{"force-adb-forward", no_argument, NULL,
OPT_FORCE_ADB_FORWARD},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"lock-video-orientation", required_argument, NULL, {"lock-video-orientation", required_argument, NULL,
@ -489,6 +668,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS}, {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
{"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
@ -499,9 +679,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
OPT_RENDER_EXPIRED_FRAMES}, OPT_RENDER_EXPIRED_FRAMES},
{"rotation", required_argument, NULL, OPT_ROTATION}, {"rotation", required_argument, NULL, OPT_ROTATION},
{"serial", required_argument, NULL, 's'}, {"serial", required_argument, NULL, 's'},
{"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD},
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'}, {"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"verbosity", required_argument, NULL, 'V'},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-x", required_argument, NULL, OPT_WINDOW_X}, {"window-x", required_argument, NULL, OPT_WINDOW_X},
@ -518,8 +700,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
optind = 0; // reset to start from the first argument in tests optind = 0; // reset to start from the first argument in tests
int c; int c;
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options, while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
NULL)) != -1) { long_options, NULL)) != -1) {
switch (c) { switch (c) {
case 'b': case 'b':
if (!parse_bit_rate(optarg, &opts->bit_rate)) { if (!parse_bit_rate(optarg, &opts->bit_rate)) {
@ -598,6 +780,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case 'v': case 'v':
args->version = true; args->version = true;
break; break;
case 'V':
if (!parse_log_level(optarg, &opts->log_level)) {
return false;
}
break;
case 'w': case 'w':
opts->stay_awake = true; opts->stay_awake = true;
break; break;
@ -647,6 +834,23 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_NO_MIPMAPS: case OPT_NO_MIPMAPS:
opts->mipmaps = false; opts->mipmaps = false;
break; break;
case OPT_NO_KEY_REPEAT:
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS:
opts->codec_options = optarg;
break;
case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true;
break;
case OPT_DISABLE_SCREENSAVER:
opts->disable_screensaver = true;
break;
case OPT_SHORTCUT_MOD:
if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) {
return false;
}
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;

View File

@ -18,4 +18,9 @@ scrcpy_print_usage(const char *arg0);
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]);
#ifdef SC_TEST
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods);
#endif
#endif #endif

View File

@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) {
static size_t static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) { write_string(const char *utf8, size_t max_len, unsigned char *buf) {
size_t len = utf8_truncation_index(utf8, max_len); size_t len = utf8_truncation_index(utf8, max_len);
buffer_write16be(buf, (uint16_t) len); buffer_write32be(buf, len);
memcpy(&buf[2], utf8, len); memcpy(&buf[4], utf8, len);
return 2 + len; return 4 + len;
} }
static uint16_t static uint16_t
@ -42,8 +42,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_INJECT_KEYCODE: case CONTROL_MSG_TYPE_INJECT_KEYCODE:
buf[1] = msg->inject_keycode.action; buf[1] = msg->inject_keycode.action;
buffer_write32be(&buf[2], msg->inject_keycode.keycode); buffer_write32be(&buf[2], msg->inject_keycode.keycode);
buffer_write32be(&buf[6], msg->inject_keycode.metastate); buffer_write32be(&buf[6], msg->inject_keycode.repeat);
return 10; buffer_write32be(&buf[10], msg->inject_keycode.metastate);
return 14;
case CONTROL_MSG_TYPE_INJECT_TEXT: { case CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len = size_t len =
write_string(msg->inject_text.text, write_string(msg->inject_text.text,
@ -67,10 +68,11 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
(uint32_t) msg->inject_scroll_event.vscroll); (uint32_t) msg->inject_scroll_event.vscroll);
return 21; return 21;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: { case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
size_t len = write_string(msg->inject_text.text, buf[1] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text,
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[1]); &buf[2]);
return 1 + len; return 2 + len;
} }
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;

View File

@ -10,10 +10,11 @@
#include "android/keycodes.h" #include "android/keycodes.h"
#include "common.h" #include "common.h"
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 #define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 // type: 1 byte; paste flag: 1 byte; length: 4 bytes
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
#define POINTER_ID_MOUSE UINT64_C(-1); #define POINTER_ID_MOUSE UINT64_C(-1);
@ -43,6 +44,7 @@ struct control_msg {
struct { struct {
enum android_keyevent_action action; enum android_keyevent_action action;
enum android_keycode keycode; enum android_keycode keycode;
uint32_t repeat;
enum android_metastate metastate; enum android_metastate metastate;
} inject_keycode; } inject_keycode;
struct { struct {
@ -62,6 +64,7 @@ struct control_msg {
} inject_scroll_event; } inject_scroll_event;
struct { struct {
char *text; // owned, to be freed by SDL_free() char *text; // owned, to be freed by SDL_free()
bool paste;
} set_clipboard; } set_clipboard;
struct { struct {
enum screen_power_mode mode; enum screen_power_mode mode;
@ -69,7 +72,7 @@ struct control_msg {
}; };
}; };
// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE // buf size must be at least CONTROL_MSG_MAX_SIZE
// return the number of bytes written // return the number of bytes written
size_t size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf); control_msg_serialize(const struct control_msg *msg, unsigned char *buf);

View File

@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller,
static bool static bool
process_msg(struct controller *controller, process_msg(struct controller *controller,
const struct control_msg *msg) { const struct control_msg *msg) {
unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE];
int length = control_msg_serialize(msg, serialized_msg); int length = control_msg_serialize(msg, serialized_msg);
if (!length) { if (!length) {
return false; return false;

View File

@ -9,7 +9,7 @@
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg) { struct device_msg *msg) {
if (len < 3) { if (len < 5) {
// at least type + empty string length // at least type + empty string length
return 0; // not available return 0; // not available
} }
@ -17,8 +17,8 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
msg->type = buf[0]; msg->type = buf[0];
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: { case DEVICE_MSG_TYPE_CLIPBOARD: {
uint16_t clipboard_len = buffer_read16be(&buf[1]); size_t clipboard_len = buffer_read32be(&buf[1]);
if (clipboard_len > len - 3) { if (clipboard_len > len - 5) {
return 0; // not available return 0; // not available
} }
char *text = SDL_malloc(clipboard_len + 1); char *text = SDL_malloc(clipboard_len + 1);
@ -27,12 +27,12 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
return -1; return -1;
} }
if (clipboard_len) { if (clipboard_len) {
memcpy(text, &buf[3], clipboard_len); memcpy(text, &buf[5], clipboard_len);
} }
text[clipboard_len] = '\0'; text[clipboard_len] = '\0';
msg->clipboard.text = text; msg->clipboard.text = text;
return 3 + clipboard_len; return 5 + clipboard_len;
} }
default: default:
LOGW("Unknown device message type: %d", (int) msg->type); LOGW("Unknown device message type: %d", (int) msg->type);

View File

@ -7,8 +7,9 @@
#include "config.h" #include "config.h"
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) // type: 1 byte; length: 4 bytes
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
enum device_msg_type { enum device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,

View File

@ -92,6 +92,10 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
MAP(SDLK_UP, AKEYCODE_DPAD_UP); MAP(SDLK_UP, AKEYCODE_DPAD_UP);
MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT);
MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT);
MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT);
MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT);
} }
if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { if (!(mod & (KMOD_NUM | KMOD_SHIFT))) {
@ -111,8 +115,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
} }
} }
if (prefer_text) { if (prefer_text && !(mod & KMOD_CTRL)) {
// do not forward alpha and space key events // do not forward alpha and space key events (unless Ctrl is pressed)
return false; return false;
} }

View File

@ -1,42 +1,77 @@
#include "input_manager.h" #include "input_manager.h"
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_keycode.h>
#include "config.h" #include "config.h"
#include "event_converter.h" #include "event_converter.h"
#include "util/lock.h" #include "util/lock.h"
#include "util/log.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_DOWN = 1;
static const int ACTION_UP = 1 << 1; static const int ACTION_UP = 1 << 1;
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(unsigned mod) {
uint16_t sdl_mod = 0;
if (mod & SC_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (mod & SC_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (mod & SC_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (mod & SC_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (mod & SC_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (mod & SC_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static bool
is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
assert(im->sdl_shortcut_mods.count);
assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) {
if (im->sdl_shortcut_mods.data[i] == sdl_mod) {
return true;
}
}
return false;
}
void
input_manager_init(struct input_manager *im,
const struct scrcpy_options *options)
{
im->control = options->control;
im->forward_key_repeat = options->forward_key_repeat;
im->prefer_text = options->prefer_text;
const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods;
assert(shortcut_mods->count);
assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < shortcut_mods->count; ++i) {
uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]);
assert(sdl_mod);
im->sdl_shortcut_mods.data[i] = sdl_mod;
}
im->sdl_shortcut_mods.count = shortcut_mods->count;
}
static void static void
send_keycode(struct controller *controller, enum android_keycode keycode, send_keycode(struct controller *controller, enum android_keycode keycode,
int actions, const char *name) { int actions, const char *name) {
@ -45,6 +80,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
msg.inject_keycode.keycode = keycode; msg.inject_keycode.keycode = keycode;
msg.inject_keycode.metastate = 0; msg.inject_keycode.metastate = 0;
msg.inject_keycode.repeat = 0;
if (actions & ACTION_DOWN) { if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
@ -97,6 +133,16 @@ action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
} }
static inline void
action_copy(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_COPY, actions, "COPY");
}
static inline void
action_cut(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_CUT, actions, "CUT");
}
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
static void static void
press_back_or_turn_screen_on(struct controller *controller) { press_back_or_turn_screen_on(struct controller *controller) {
@ -129,17 +175,7 @@ collapse_notification_panel(struct controller *controller) {
} }
static void static void
request_device_clipboard(struct controller *controller) { set_device_clipboard(struct controller *controller, bool paste) {
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(); char *text = SDL_GetClipboardText();
if (!text) { if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError()); LOGW("Could not get clipboard text: %s", SDL_GetError());
@ -154,6 +190,7 @@ set_device_clipboard(struct controller *controller) {
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
msg.set_clipboard.text = text; msg.set_clipboard.text = text;
msg.set_clipboard.paste = paste;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
SDL_free(text); SDL_free(text);
@ -236,6 +273,10 @@ rotate_client_right(struct screen *screen) {
void void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) { const SDL_TextInputEvent *event) {
if (is_shortcut_mod(im, SDL_GetModState())) {
// A shortcut must never generate text events
return;
}
if (!im->prefer_text) { if (!im->prefer_text) {
char c = event->text[0]; char c = event->text[0];
if (isalpha(c) || c == ' ') { if (isalpha(c) || c == ' ') {
@ -260,7 +301,7 @@ input_manager_process_text_input(struct input_manager *im,
static bool static bool
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
bool prefer_text) { bool prefer_text, uint32_t repeat) {
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
@ -273,6 +314,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
return false; return false;
} }
to->inject_keycode.repeat = repeat;
to->inject_keycode.metastate = convert_meta_state(mod); to->inject_keycode.metastate = convert_meta_state(mod);
return true; return true;
@ -280,139 +322,125 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event) {
bool control) {
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
// ctrl: the Ctrl key bool control = im->control;
bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); bool smod = is_shortcut_mod(im, event->keysym.mod);
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; struct controller *controller = im->controller;
// capture all Ctrl events
if (ctrl || cmd) {
SDL_Keycode keycode = event->keysym.sym; SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN; bool down = event->type == SDL_KEYDOWN;
int action = down ? ACTION_DOWN : ACTION_UP; bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat; bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
// The shortcut modifier is pressed
if (smod) {
int action = down ? ACTION_DOWN : ACTION_UP;
switch (keycode) { switch (keycode) {
case SDLK_h: case SDLK_h:
// Ctrl+h on all platform, since Cmd+h is already captured by if (control && !shift && !repeat) {
// the system on macOS to hide the window
if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action); action_home(controller, action);
} }
return; return;
case SDLK_b: // fall-through case SDLK_b: // fall-through
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
if (control && cmd && !shift && !repeat) { if (control && !shift && !repeat) {
action_back(controller, action); action_back(controller, action);
} }
return; return;
case SDLK_s: case SDLK_s:
if (control && cmd && !shift && !repeat) { if (control && !shift && !repeat) {
action_app_switch(controller, action); action_app_switch(controller, action);
} }
return; return;
case SDLK_m: case SDLK_m:
// Ctrl+m on all platform, since Cmd+m is already captured by if (control && !shift && !repeat) {
// the system on macOS to minimize the window
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action); action_menu(controller, action);
} }
return; return;
case SDLK_p: case SDLK_p:
if (control && cmd && !shift && !repeat) { if (control && !shift && !repeat) {
action_power(controller, action); action_power(controller, action);
} }
return; return;
case SDLK_o: case SDLK_o:
if (control && cmd && !shift && down) { if (control && !repeat && down) {
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); enum screen_power_mode mode = shift
? SCREEN_POWER_MODE_NORMAL
: SCREEN_POWER_MODE_OFF;
set_screen_power_mode(controller, mode);
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (control && cmd && !shift) { if (control && !shift) {
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(controller, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (control && cmd && !shift) { if (control && !shift) {
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(controller, action);
} }
return; return;
case SDLK_LEFT: case SDLK_LEFT:
if (cmd && !shift && down) { if (!shift && !repeat && down) {
rotate_client_left(im->screen); rotate_client_left(im->screen);
} }
return; return;
case SDLK_RIGHT: case SDLK_RIGHT:
if (cmd && !shift && down) { if (!shift && !repeat && down) {
rotate_client_right(im->screen); rotate_client_right(im->screen);
} }
return; return;
case SDLK_c: case SDLK_c:
if (control && cmd && !shift && !repeat && down) { if (control && !shift && !repeat) {
request_device_clipboard(controller); action_copy(controller, action);
}
return;
case SDLK_x:
if (control && !shift && !repeat) {
action_cut(controller, action);
} }
return; return;
case SDLK_v: case SDLK_v:
if (control && cmd && !repeat && down) { if (control && !repeat && down) {
if (shift) { if (shift) {
// store the text in the device clipboard
set_device_clipboard(controller);
} else {
// inject the text as input events // inject the text as input events
clipboard_paste(controller); clipboard_paste(controller);
} else {
// store the text in the device clipboard and paste
set_device_clipboard(controller, true);
} }
} }
return; return;
case SDLK_f: case SDLK_f:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
screen_switch_fullscreen(im->screen); screen_switch_fullscreen(im->screen);
} }
return; return;
case SDLK_x: case SDLK_w:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(im->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
screen_resize_to_pixel_perfect(im->screen); screen_resize_to_pixel_perfect(im->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && cmd && !repeat && down) { if (!shift && !repeat && down) {
struct fps_counter *fps_counter = struct fps_counter *fps_counter =
im->video_buffer->fps_counter; im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter); switch_fps_counter_state(fps_counter);
} }
return; return;
case SDLK_n: case SDLK_n:
if (control && cmd && !repeat && down) { if (control && !repeat && down) {
if (shift) { if (shift) {
collapse_notification_panel(controller); collapse_notification_panel(controller);
} else { } else {
@ -421,7 +449,7 @@ input_manager_process_key(struct input_manager *im,
} }
return; return;
case SDLK_r: case SDLK_r:
if (control && cmd && !shift && !repeat && down) { if (control && !shift && !repeat && down) {
rotate_device(controller); rotate_device(controller);
} }
return; return;
@ -434,8 +462,23 @@ input_manager_process_key(struct input_manager *im,
return; return;
} }
if (event->repeat) {
if (!im->forward_key_repeat) {
return;
}
++im->repeat;
} else {
im->repeat = 0;
}
if (ctrl && !shift && keycode == SDLK_v && down && !repeat) {
// Synchronize the computer clipboard to the device clipboard before
// sending Ctrl+v, to allow seamless copy-paste.
set_device_clipboard(controller, false);
}
struct control_msg msg; struct control_msg msg;
if (convert_input_key(event, &msg, im->prefer_text)) { if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'"); LOGW("Could not request 'inject keycode'");
} }
@ -450,7 +493,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point = to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, from->x, from->y); screen_convert_window_to_frame_coords(screen, from->x, from->y);
to->inject_touch_event.pressure = 1.f; to->inject_touch_event.pressure = 1.f;
to->inject_touch_event.buttons = convert_mouse_buttons(from->state); to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
@ -487,11 +530,17 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.pointer_id = from->fingerId;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
int dw;
int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
// SDL touch event coordinates are normalized in the range [0; 1] // SDL touch event coordinates are normalized in the range [0; 1]
float x = from->x * screen->content_size.width; int32_t x = from->x * dw;
float y = from->y * screen->content_size.height; int32_t y = from->y * dh;
to->inject_touch_event.position.point = to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, x, y); screen_convert_drawable_to_frame_coords(screen, x, y);
to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0; to->inject_touch_event.buttons = 0;
return true; return true;
@ -508,13 +557,6 @@ input_manager_process_touch(struct input_manager *im,
} }
} }
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 static bool
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
struct control_msg *to) { struct control_msg *to) {
@ -527,8 +569,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point = to->inject_touch_event.position.point =
screen_convert_to_frame_coords(screen, from->x, from->y); screen_convert_window_to_frame_coords(screen, from->x, from->y);
to->inject_touch_event.pressure = 1.f; to->inject_touch_event.pressure =
from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f;
to->inject_touch_event.buttons = to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button)); convert_mouse_buttons(SDL_BUTTON(from->button));
@ -537,8 +580,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event) {
bool control) { bool control = im->control;
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate // simulated from touch events, so it's a duplicate
return; return;
@ -552,10 +596,15 @@ input_manager_process_mouse_button(struct input_manager *im,
action_home(im->controller, ACTION_DOWN | ACTION_UP); action_home(im->controller, ACTION_DOWN | ACTION_UP);
return; return;
} }
// double-click on black borders resize to fit the device screen // double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
bool outside = int32_t x = event->x;
is_outside_device_screen(im, event->x, event->y); int32_t y = event->y;
screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) { if (outside) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(im->screen);
return; return;
@ -579,9 +628,16 @@ input_manager_process_mouse_button(struct input_manager *im,
static bool static bool
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
struct control_msg *to) { struct control_msg *to) {
// mouse_x and mouse_y are expressed in pixels relative to the window
int mouse_x;
int mouse_y;
SDL_GetMouseState(&mouse_x, &mouse_y);
struct position position = { struct position position = {
.screen_size = screen->frame_size, .screen_size = screen->frame_size,
.point = get_mouse_point(screen), .point = screen_convert_window_to_frame_coords(screen,
mouse_x, mouse_y),
}; };
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;

View File

@ -3,28 +3,46 @@
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL.h>
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "controller.h" #include "controller.h"
#include "fps_counter.h" #include "fps_counter.h"
#include "video_buffer.h" #include "scrcpy.h"
#include "screen.h" #include "screen.h"
#include "video_buffer.h"
struct input_manager { struct input_manager {
struct controller *controller; struct controller *controller;
struct video_buffer *video_buffer; struct video_buffer *video_buffer;
struct screen *screen; struct screen *screen;
// SDL reports repeated events as a boolean, but Android expects the actual
// number of repetitions. This variable keeps track of the count.
unsigned repeat;
bool control;
bool forward_key_repeat;
bool prefer_text; bool prefer_text;
struct {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
} sdl_shortcut_mods;
}; };
void
input_manager_init(struct input_manager *im,
const struct scrcpy_options *options);
void void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event); const SDL_TextInputEvent *event);
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event, const SDL_KeyboardEvent *event);
bool control);
void void
input_manager_process_mouse_motion(struct input_manager *im, input_manager_process_mouse_motion(struct input_manager *im,
@ -36,8 +54,7 @@ input_manager_process_touch(struct input_manager *im,
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event, const SDL_MouseButtonEvent *event);
bool control);
void void
input_manager_process_mouse_wheel(struct input_manager *im, input_manager_process_mouse_wheel(struct input_manager *im,

View File

@ -1,5 +1,6 @@
#include "scrcpy.h" #include "scrcpy.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
@ -29,6 +30,24 @@ print_version(void) {
LIBAVUTIL_VERSION_MICRO); LIBAVUTIL_VERSION_MICRO);
} }
static SDL_LogPriority
convert_log_level_to_sdl(enum sc_log_level level) {
switch (level) {
case SC_LOG_LEVEL_DEBUG:
return SDL_LOG_PRIORITY_DEBUG;
case SC_LOG_LEVEL_INFO:
return SDL_LOG_PRIORITY_INFO;
case SC_LOG_LEVEL_WARN:
return SDL_LOG_PRIORITY_WARN;
case SC_LOG_LEVEL_ERROR:
return SDL_LOG_PRIORITY_ERROR;
default:
assert(!"unexpected log level");
return SDL_LOG_PRIORITY_INFO;
}
}
int int
main(int argc, char *argv[]) { main(int argc, char *argv[]) {
#ifdef __WINDOWS__ #ifdef __WINDOWS__
@ -38,20 +57,23 @@ main(int argc, char *argv[]) {
setbuf(stderr, NULL); setbuf(stderr, NULL);
#endif #endif
#ifndef NDEBUG
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
#endif
struct scrcpy_cli_args args = { struct scrcpy_cli_args args = {
.opts = SCRCPY_OPTIONS_DEFAULT, .opts = SCRCPY_OPTIONS_DEFAULT,
.help = false, .help = false,
.version = false, .version = false,
}; };
#ifndef NDEBUG
args.opts.log_level = SC_LOG_LEVEL_DEBUG;
#endif
if (!scrcpy_parse_args(&args, argc, argv)) { if (!scrcpy_parse_args(&args, argc, argv)) {
return 1; return 1;
} }
SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level);
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
if (args.help) { if (args.help) {
scrcpy_print_usage(argv[0]); scrcpy_print_usage(argv[0]);
return 0; return 0;

View File

@ -25,12 +25,21 @@ receiver_destroy(struct receiver *receiver) {
static void static void
process_msg(struct device_msg *msg) { process_msg(struct device_msg *msg) {
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText();
bool same = current && !strcmp(current, msg->clipboard.text);
SDL_free(current);
if (same) {
LOGD("Computer clipboard unchanged");
return;
}
LOGI("Device clipboard copied"); LOGI("Device clipboard copied");
SDL_SetClipboardText(msg->clipboard.text); SDL_SetClipboardText(msg->clipboard.text);
break; break;
} }
} }
}
static ssize_t static ssize_t
process_msgs(const unsigned char *buf, size_t len) { process_msgs(const unsigned char *buf, size_t len) {
@ -60,28 +69,29 @@ static int
run_receiver(void *data) { run_receiver(void *data) {
struct receiver *receiver = data; struct receiver *receiver = data;
unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; static unsigned char buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {
assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); assert(head < DEVICE_MSG_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf, ssize_t r = net_recv(receiver->control_socket, buf + head,
DEVICE_MSG_SERIALIZED_MAX_SIZE - head); DEVICE_MSG_MAX_SIZE - head);
if (r <= 0) { if (r <= 0) {
LOGD("Receiver stopped"); LOGD("Receiver stopped");
break; break;
} }
ssize_t consumed = process_msgs(buf, r); head += r;
ssize_t consumed = process_msgs(buf, head);
if (consumed == -1) { if (consumed == -1) {
// an error occurred // an error occurred
break; break;
} }
if (consumed) { if (consumed) {
head -= consumed;
// shift the remaining data in the buffer // shift the remaining data in the buffer
memmove(buf, &buf[consumed], r - consumed); memmove(buf, &buf[consumed], head);
head = r - consumed;
} }
} }

View File

@ -63,7 +63,7 @@ recorder_queue_clear(struct recorder_queue *queue) {
bool bool
recorder_init(struct recorder *recorder, recorder_init(struct recorder *recorder,
const char *filename, const char *filename,
enum recorder_format format, enum sc_record_format format,
struct size declared_frame_size) { struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename); recorder->filename = SDL_strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
@ -105,10 +105,10 @@ recorder_destroy(struct recorder *recorder) {
} }
static const char * static const char *
recorder_get_format_name(enum recorder_format format) { recorder_get_format_name(enum sc_record_format format) {
switch (format) { switch (format) {
case RECORDER_FORMAT_MP4: return "mp4"; case SC_RECORD_FORMAT_MP4: return "mp4";
case RECORDER_FORMAT_MKV: return "matroska"; case SC_RECORD_FORMAT_MKV: return "matroska";
default: return NULL; default: return NULL;
} }
} }

View File

@ -8,14 +8,9 @@
#include "config.h" #include "config.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
#include "util/queue.h" #include "util/queue.h"
enum recorder_format {
RECORDER_FORMAT_AUTO,
RECORDER_FORMAT_MP4,
RECORDER_FORMAT_MKV,
};
struct record_packet { struct record_packet {
AVPacket packet; AVPacket packet;
struct record_packet *next; struct record_packet *next;
@ -25,7 +20,7 @@ struct recorder_queue QUEUE(struct record_packet);
struct recorder { struct recorder {
char *filename; char *filename;
enum recorder_format format; enum sc_record_format format;
AVFormatContext *ctx; AVFormatContext *ctx;
struct size declared_frame_size; struct size declared_frame_size;
bool header_written; bool header_written;
@ -46,7 +41,7 @@ struct recorder {
bool bool
recorder_init(struct recorder *recorder, const char *filename, recorder_init(struct recorder *recorder, const char *filename,
enum recorder_format format, struct size declared_frame_size); enum sc_record_format format, struct size declared_frame_size);
void void
recorder_destroy(struct recorder *recorder); recorder_destroy(struct recorder *recorder);

View File

@ -8,6 +8,8 @@
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#ifdef _WIN32 #ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h> # include <windows.h>
#endif #endif
@ -46,7 +48,14 @@ static struct input_manager input_manager = {
.controller = &controller, .controller = &controller,
.video_buffer = &video_buffer, .video_buffer = &video_buffer,
.screen = &screen, .screen = &screen,
.prefer_text = false, // initialized later .repeat = 0,
// initialized later
.prefer_text = false,
.sdl_shortcut_mods = {
.data = {0},
.count = 0,
},
}; };
#ifdef _WIN32 #ifdef _WIN32
@ -63,7 +72,8 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
// init SDL and set appropriate hints // init SDL and set appropriate hints
static bool static bool
sdl_init_and_configure(bool display, const char *render_driver) { sdl_init_and_configure(bool display, const char *render_driver,
bool disable_screensaver) {
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
if (SDL_Init(flags)) { if (SDL_Init(flags)) {
LOGC("Could not initialize SDL: %s", SDL_GetError()); LOGC("Could not initialize SDL: %s", SDL_GetError());
@ -112,8 +122,13 @@ sdl_init_and_configure(bool display, const char *render_driver) {
LOGW("Could not disable minimize on focus loss"); LOGW("Could not disable minimize on focus loss");
} }
// Do not disable the screensaver when scrcpy is running if (disable_screensaver) {
LOGD("Screensaver disabled");
SDL_DisableScreenSaver();
} else {
LOGD("Screensaver enabled");
SDL_EnableScreenSaver(); SDL_EnableScreenSaver();
}
return true; return true;
} }
@ -136,7 +151,7 @@ event_watcher(void *data, SDL_Event *event) {
&& event->window.event == SDL_WINDOWEVENT_RESIZED) { && event->window.event == SDL_WINDOWEVENT_RESIZED) {
// In practice, it seems to always be called from the same thread in // In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround. // that specific case. Anyway, it's just a workaround.
screen_render(&screen); screen_render(&screen, true);
} }
return 0; return 0;
} }
@ -155,7 +170,7 @@ enum event_result {
}; };
static enum event_result static enum event_result
handle_event(SDL_Event *event, bool control) { handle_event(SDL_Event *event, const struct scrcpy_options *options) {
switch (event->type) { switch (event->type) {
case EVENT_STREAM_STOPPED: case EVENT_STREAM_STOPPED:
LOGD("Video stream stopped"); LOGD("Video stream stopped");
@ -177,7 +192,7 @@ handle_event(SDL_Event *event, bool control) {
screen_handle_window_event(&screen, &event->window); screen_handle_window_event(&screen, &event->window);
break; break;
case SDL_TEXTINPUT: case SDL_TEXTINPUT:
if (!control) { if (!options->control) {
break; break;
} }
input_manager_process_text_input(&input_manager, &event->text); input_manager_process_text_input(&input_manager, &event->text);
@ -186,16 +201,16 @@ handle_event(SDL_Event *event, bool control) {
case SDL_KEYUP: case SDL_KEYUP:
// some key events do not interact with the device, so process the // some key events do not interact with the device, so process the
// event even if control is disabled // event even if control is disabled
input_manager_process_key(&input_manager, &event->key, control); input_manager_process_key(&input_manager, &event->key);
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
if (!control) { if (!options->control) {
break; break;
} }
input_manager_process_mouse_motion(&input_manager, &event->motion); input_manager_process_mouse_motion(&input_manager, &event->motion);
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
if (!control) { if (!options->control) {
break; break;
} }
input_manager_process_mouse_wheel(&input_manager, &event->wheel); input_manager_process_mouse_wheel(&input_manager, &event->wheel);
@ -204,8 +219,7 @@ handle_event(SDL_Event *event, bool control) {
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process // some mouse events do not interact with the device, so process
// the event even if control is disabled // the event even if control is disabled
input_manager_process_mouse_button(&input_manager, &event->button, input_manager_process_mouse_button(&input_manager, &event->button);
control);
break; break;
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
@ -213,7 +227,7 @@ handle_event(SDL_Event *event, bool control) {
input_manager_process_touch(&input_manager, &event->tfinger); input_manager_process_touch(&input_manager, &event->tfinger);
break; break;
case SDL_DROPFILE: { case SDL_DROPFILE: {
if (!control) { if (!options->control) {
break; break;
} }
file_handler_action_t action; file_handler_action_t action;
@ -230,16 +244,15 @@ handle_event(SDL_Event *event, bool control) {
} }
static bool static bool
event_loop(bool display, bool control) { event_loop(const struct scrcpy_options *options) {
(void) display;
#ifdef CONTINUOUS_RESIZING_WORKAROUND #ifdef CONTINUOUS_RESIZING_WORKAROUND
if (display) { if (options->display) {
SDL_AddEventWatch(event_watcher, NULL); SDL_AddEventWatch(event_watcher, NULL);
} }
#endif #endif
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, control); enum event_result result = handle_event(&event, options);
switch (result) { switch (result) {
case EVENT_RESULT_STOPPED_BY_USER: case EVENT_RESULT_STOPPED_BY_USER:
return true; return true;
@ -293,6 +306,7 @@ bool
scrcpy(const struct scrcpy_options *options) { scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename; bool record = !!options->record_filename;
struct server_params params = { struct server_params params = {
.log_level = options->log_level,
.crop = options->crop, .crop = options->crop,
.port_range = options->port_range, .port_range = options->port_range,
.max_size = options->max_size, .max_size = options->max_size,
@ -303,6 +317,8 @@ scrcpy(const struct scrcpy_options *options) {
.display_id = options->display_id, .display_id = options->display_id,
.show_touches = options->show_touches, .show_touches = options->show_touches,
.stay_awake = options->stay_awake, .stay_awake = options->stay_awake,
.codec_options = options->codec_options,
.force_adb_forward = options->force_adb_forward,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
return false; return false;
@ -318,7 +334,8 @@ scrcpy(const struct scrcpy_options *options) {
bool controller_initialized = false; bool controller_initialized = false;
bool controller_started = false; bool controller_started = false;
if (!sdl_init_and_configure(options->display, options->render_driver)) { if (!sdl_init_and_configure(options->display, options->render_driver,
options->disable_screensaver)) {
goto end; goto end;
} }
@ -424,9 +441,9 @@ scrcpy(const struct scrcpy_options *options) {
} }
} }
input_manager.prefer_text = options->prefer_text; input_manager_init(&input_manager, options);
ret = event_loop(options->display, options->control); ret = event_loop(options);
LOGD("quit..."); LOGD("quit...");
screen_destroy(&screen); screen_destroy(&screen);

View File

@ -2,12 +2,46 @@
#define SCRCPY_H #define SCRCPY_H
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "config.h" #include "config.h"
#include "common.h"
#include "input_manager.h" enum sc_log_level {
#include "recorder.h" SC_LOG_LEVEL_DEBUG,
SC_LOG_LEVEL_INFO,
SC_LOG_LEVEL_WARN,
SC_LOG_LEVEL_ERROR,
};
enum sc_record_format {
SC_RECORD_FORMAT_AUTO,
SC_RECORD_FORMAT_MP4,
SC_RECORD_FORMAT_MKV,
};
#define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod {
SC_MOD_LCTRL = 1 << 0,
SC_MOD_RCTRL = 1 << 1,
SC_MOD_LALT = 1 << 2,
SC_MOD_RALT = 1 << 3,
SC_MOD_LSUPER = 1 << 4,
SC_MOD_RSUPER = 1 << 5,
};
struct sc_shortcut_mods {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
};
struct sc_port_range {
uint16_t first;
uint16_t last;
};
#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
struct scrcpy_options { struct scrcpy_options {
const char *serial; const char *serial;
@ -16,15 +50,18 @@ struct scrcpy_options {
const char *window_title; const char *window_title;
const char *push_target; const char *push_target;
const char *render_driver; const char *render_driver;
enum recorder_format record_format; const char *codec_options;
struct port_range port_range; enum sc_log_level log_level;
enum sc_record_format record_format;
struct sc_port_range port_range;
struct sc_shortcut_mods shortcut_mods;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
uint8_t rotation; uint8_t rotation;
int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint16_t display_id; uint16_t display_id;
@ -39,6 +76,9 @@ struct scrcpy_options {
bool window_borderless; bool window_borderless;
bool mipmaps; bool mipmaps;
bool stay_awake; bool stay_awake;
bool force_adb_forward;
bool disable_screensaver;
bool forward_key_repeat;
}; };
#define SCRCPY_OPTIONS_DEFAULT { \ #define SCRCPY_OPTIONS_DEFAULT { \
@ -48,18 +88,24 @@ struct scrcpy_options {
.window_title = NULL, \ .window_title = NULL, \
.push_target = NULL, \ .push_target = NULL, \
.render_driver = NULL, \ .render_driver = NULL, \
.record_format = RECORDER_FORMAT_AUTO, \ .codec_options = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \
.record_format = SC_RECORD_FORMAT_AUTO, \
.port_range = { \ .port_range = { \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
}, \ }, \
.shortcut_mods = { \
.data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
.count = 2, \
}, \
.max_size = DEFAULT_MAX_SIZE, \ .max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \ .max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.rotation = 0, \ .rotation = 0, \
.window_x = WINDOW_POSITION_UNDEFINED, \ .window_x = SC_WINDOW_POSITION_UNDEFINED, \
.window_y = WINDOW_POSITION_UNDEFINED, \ .window_y = SC_WINDOW_POSITION_UNDEFINED, \
.window_width = 0, \ .window_width = 0, \
.window_height = 0, \ .window_height = 0, \
.display_id = 0, \ .display_id = 0, \
@ -74,6 +120,9 @@ struct scrcpy_options {
.window_borderless = false, \ .window_borderless = false, \
.mipmaps = true, \ .mipmaps = true, \
.stay_awake = false, \ .stay_awake = false, \
.force_adb_forward = false, \
.disable_screensaver = false, \
.forward_key_repeat = true, \
} }
bool bool

View File

@ -8,6 +8,7 @@
#include "common.h" #include "common.h"
#include "compat.h" #include "compat.h"
#include "icon.xpm" #include "icon.xpm"
#include "scrcpy.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/lock.h" #include "util/lock.h"
@ -30,10 +31,10 @@ get_rotated_size(struct size size, int rotation) {
// get the window size in a struct size // get the window size in a struct size
static struct size static struct size
get_window_size(SDL_Window *window) { get_window_size(const struct screen *screen) {
int width; int width;
int height; int height;
SDL_GetWindowSize(window, &width, &height); SDL_GetWindowSize(screen->window, &width, &height);
struct size size; struct size size;
size.width = width; size.width = width;
@ -41,31 +42,12 @@ get_window_size(SDL_Window *window) {
return size; return size;
} }
// get the windowed window size
static struct size
get_windowed_window_size(const struct screen *screen) {
if (screen->fullscreen || screen->maximized) {
return screen->windowed_window_size;
}
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 // set the window size to be applied when fullscreen is disabled
static void static void
set_window_size(struct screen *screen, struct size new_size) { set_window_size(struct screen *screen, struct size new_size) {
// setting the window size during fullscreen is implementation defined, assert(!screen->fullscreen);
// so apply the resize only after fullscreen is disabled assert(!screen->maximized);
screen->windowed_window_size = new_size; SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
apply_windowed_size(screen);
} }
// get the preferred display bounds (i.e. the screen bounds with some margins) // get the preferred display bounds (i.e. the screen bounds with some margins)
@ -87,6 +69,16 @@ get_preferred_display_bounds(struct size *bounds) {
return true; return true;
} }
static bool
is_optimal_size(struct size current_size, struct size content_size) {
// The size is optimal if we can recompute one dimension of the current
// size from the other
return current_size.height == current_size.width * content_size.height
/ content_size.width
|| current_size.width == current_size.height * content_size.width
/ content_size.height;
}
// return the optimal size of the window, with the following constraints: // 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 // - it attempts to keep at least one dimension of the current_size (i.e. it
// crops the black borders) // crops the black borders)
@ -99,47 +91,43 @@ get_optimal_size(struct size current_size, struct size content_size) {
return current_size; return current_size;
} }
struct size display_size; struct size window_size;
// 32 bits because we need to multiply two 16 bits values
uint32_t w;
uint32_t h;
struct size display_size;
if (!get_preferred_display_bounds(&display_size)) { if (!get_preferred_display_bounds(&display_size)) {
// could not get display bounds, do not constraint the size // could not get display bounds, do not constraint the size
w = current_size.width; window_size.width = current_size.width;
h = current_size.height; window_size.height = current_size.height;
} else { } else {
w = MIN(current_size.width, display_size.width); window_size.width = MIN(current_size.width, display_size.width);
h = MIN(current_size.height, display_size.height); window_size.height = MIN(current_size.height, display_size.height);
} }
if (h == w * content_size.height / content_size.width if (is_optimal_size(window_size, content_size)) {
|| w == h * content_size.width / content_size.height) { return window_size;
// 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; bool keep_width = content_size.width * window_size.height
> content_size.height * window_size.width;
if (keep_width) { if (keep_width) {
// remove black borders on top and bottom // remove black borders on top and bottom
h = content_size.height * w / content_size.width; window_size.height = content_size.height * window_size.width
/ content_size.width;
} else { } else {
// remove black borders on left and right (or none at all if it already // remove black borders on left and right (or none at all if it already
// fits) // fits)
w = content_size.width * h / content_size.height; window_size.width = content_size.width * window_size.height
/ content_size.height;
} }
// w and h must fit into 16 bits return window_size;
assert(w < 0x10000 && h < 0x10000);
return (struct size) {w, h};
} }
// same as get_optimal_size(), but read the current size from the window // same as get_optimal_size(), but read the current size from the window
static inline struct size static inline struct size
get_optimal_window_size(const struct screen *screen, struct size content_size) { get_optimal_window_size(const struct screen *screen, struct size content_size) {
struct size windowed_size = get_windowed_window_size(screen); struct size window_size = get_window_size(screen);
return get_optimal_size(windowed_size, content_size); return get_optimal_size(window_size, content_size);
} }
// initially, there is no current size, so use the frame size as current size // initially, there is no current size, so use the frame size as current size
@ -169,6 +157,43 @@ get_initial_optimal_size(struct size content_size, uint16_t req_width,
return window_size; return window_size;
} }
static void
screen_update_content_rect(struct screen *screen) {
int dw;
int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
struct size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
struct size drawable_size = {dw, dh};
SDL_Rect *rect = &screen->rect;
if (is_optimal_size(drawable_size, content_size)) {
rect->x = 0;
rect->y = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.height;
return;
}
bool keep_width = content_size.width * drawable_size.height
> content_size.height * drawable_size.width;
if (keep_width) {
rect->x = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.width * content_size.height
/ content_size.width;
rect->y = (drawable_size.height - rect->h) / 2;
} else {
rect->y = 0;
rect->h = drawable_size.height;
rect->w = drawable_size.height * content_size.width
/ content_size.height;
rect->x = (drawable_size.width - rect->w) / 2;
}
}
void void
screen_init(struct screen *screen) { screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER; *screen = (struct screen) SCREEN_INITIALIZER;
@ -193,7 +218,7 @@ create_texture(struct screen *screen) {
// Enable trilinear filtering for downscaling // Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR); GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f); gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
SDL_GL_UnbindTexture(texture); SDL_GL_UnbindTexture(texture);
} }
@ -233,9 +258,9 @@ screen_init_rendering(struct screen *screen, const char *window_title,
window_flags |= SDL_WINDOW_BORDERLESS; window_flags |= SDL_WINDOW_BORDERLESS;
} }
int x = window_x != WINDOW_POSITION_UNDEFINED int x = window_x != SC_WINDOW_POSITION_UNDEFINED
? window_x : (int) SDL_WINDOWPOS_UNDEFINED; ? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = window_y != WINDOW_POSITION_UNDEFINED int y = window_y != SC_WINDOW_POSITION_UNDEFINED
? window_y : (int) SDL_WINDOWPOS_UNDEFINED; ? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y, screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height, window_size.width, window_size.height,
@ -258,13 +283,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
const char *renderer_name = r ? NULL : renderer_info.name; const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width,
content_size.height)) {
LOGE("Could not set renderer logical size: %s", SDL_GetError());
screen_destroy(screen);
return false;
}
// starts with "opengl" // starts with "opengl"
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (screen->use_opengl) { if (screen->use_opengl) {
@ -288,7 +306,7 @@ screen_init_rendering(struct screen *screen, const char *window_title,
LOGI("Trilinear filtering disabled"); LOGI("Trilinear filtering disabled");
} }
} else { } else {
LOGW("Trilinear filtering disabled (not an OpenGL renderer)"); LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
} }
SDL_Surface *icon = read_xpm(icon_xpm); SDL_Surface *icon = read_xpm(icon_xpm);
@ -308,7 +326,12 @@ screen_init_rendering(struct screen *screen, const char *window_title,
return false; return false;
} }
screen->windowed_window_size = window_size; // Reset the window size to trigger a SIZE_CHANGED event, to workaround
// HiDPI issues with some SDL renderers when several displays having
// different HiDPI scaling are connected
SDL_SetWindowSize(screen->window, window_size.width, window_size.height);
screen_update_content_rect(screen);
return true; return true;
} }
@ -331,6 +354,45 @@ screen_destroy(struct screen *screen) {
} }
} }
static void
resize_for_content(struct screen *screen, struct size old_content_size,
struct size new_content_size) {
struct size window_size = get_window_size(screen);
struct size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width
/ old_content_size.width,
.height = (uint32_t) window_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);
}
static void
set_content_size(struct screen *screen, struct size new_content_size) {
if (!screen->fullscreen && !screen->maximized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size once
// fullscreen and maximized are disabled
screen->windowed_content_size = screen->content_size;
screen->resize_pending = true;
}
screen->content_size = new_content_size;
}
static void
apply_pending_resize(struct screen *screen) {
assert(!screen->fullscreen);
assert(!screen->maximized);
if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size,
screen->content_size);
screen->resize_pending = false;
}
}
void void
screen_set_rotation(struct screen *screen, unsigned rotation) { screen_set_rotation(struct screen *screen, unsigned rotation) {
assert(rotation < 4); assert(rotation < 4);
@ -338,32 +400,15 @@ screen_set_rotation(struct screen *screen, unsigned rotation) {
return; return;
} }
struct size old_content_size = screen->content_size;
struct size new_content_size = struct size new_content_size =
get_rotated_size(screen->frame_size, rotation); get_rotated_size(screen->frame_size, rotation);
if (SDL_RenderSetLogicalSize(screen->renderer, set_content_size(screen, new_content_size);
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; screen->rotation = rotation;
LOGI("Display rotation set to %u", rotation); LOGI("Display rotation set to %u", rotation);
screen_render(screen); screen_render(screen, true);
} }
// recreate the texture and resize the window if the frame size has changed // recreate the texture and resize the window if the frame size has changed
@ -371,31 +416,16 @@ static bool
prepare_for_frame(struct screen *screen, struct size new_frame_size) { prepare_for_frame(struct screen *screen, struct size new_frame_size) {
if (screen->frame_size.width != new_frame_size.width if (screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) { || 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 false;
}
// frame dimension changed, destroy texture // frame dimension changed, destroy texture
SDL_DestroyTexture(screen->texture); SDL_DestroyTexture(screen->texture);
struct size content_size = screen->content_size;
struct size windowed_size = get_windowed_window_size(screen);
struct size target_size = {
(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_content_size);
set_window_size(screen, target_size);
screen->frame_size = new_frame_size; screen->frame_size = new_frame_size;
screen->content_size = new_content_size;
struct size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size);
screen_update_content_rect(screen);
LOGI("New texture: %" PRIu16 "x%" PRIu16, LOGI("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height); screen->frame_size.width, screen->frame_size.height);
@ -437,15 +467,19 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) {
update_texture(screen, frame); update_texture(screen, frame);
mutex_unlock(vb->mutex); mutex_unlock(vb->mutex);
screen_render(screen); screen_render(screen, false);
return true; return true;
} }
void void
screen_render(struct screen *screen) { screen_render(struct screen *screen, bool update_content_rect) {
if (update_content_rect) {
screen_update_content_rect(screen);
}
SDL_RenderClear(screen->renderer); SDL_RenderClear(screen->renderer);
if (screen->rotation == 0) { if (screen->rotation == 0) {
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
} else { } else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is // rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation) // counterclockwise (to be consistent with --lock-video-orientation)
@ -455,12 +489,14 @@ screen_render(struct screen *screen) {
SDL_Rect *dstrect = NULL; SDL_Rect *dstrect = NULL;
SDL_Rect rect; SDL_Rect rect;
if (screen->rotation & 1) { if (screen->rotation & 1) {
struct size size = screen->content_size; rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
rect.x = (size.width - size.height) / 2; rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
rect.y = (size.height - size.width) / 2; rect.w = screen->rect.h;
rect.w = size.height; rect.h = screen->rect.w;
rect.h = size.width;
dstrect = &rect; dstrect = &rect;
} else {
assert(screen->rotation == 2);
dstrect = &screen->rect;
} }
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
@ -478,23 +514,20 @@ screen_switch_fullscreen(struct screen *screen) {
} }
screen->fullscreen = !screen->fullscreen; screen->fullscreen = !screen->fullscreen;
apply_windowed_size(screen); if (!screen->fullscreen && !screen->maximized) {
apply_pending_resize(screen);
}
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
screen_render(screen); screen_render(screen, true);
} }
void void
screen_resize_to_fit(struct screen *screen) { screen_resize_to_fit(struct screen *screen) {
if (screen->fullscreen) { if (screen->fullscreen || screen->maximized) {
return; return;
} }
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
struct size optimal_size = struct size optimal_size =
get_optimal_window_size(screen, screen->content_size); get_optimal_window_size(screen, screen->content_size);
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
@ -524,50 +557,42 @@ screen_handle_window_event(struct screen *screen,
const SDL_WindowEvent *event) { const SDL_WindowEvent *event) {
switch (event->event) { switch (event->event) {
case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_EXPOSED:
screen_render(screen); screen_render(screen, true);
break; break;
case SDL_WINDOWEVENT_SIZE_CHANGED: case SDL_WINDOWEVENT_SIZE_CHANGED:
if (!screen->fullscreen && !screen->maximized) { screen_render(screen, true);
// 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; break;
case SDL_WINDOWEVENT_MAXIMIZED: 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; screen->maximized = true;
break; break;
case SDL_WINDOWEVENT_RESTORED: case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling fullscreen
// mode unexpectedly triggers the "restored" then "maximized"
// events, leaving the window in a weird state (maximized
// according to the events, but not maximized visually).
break;
}
screen->maximized = false; screen->maximized = false;
apply_windowed_size(screen); apply_pending_resize(screen);
break; break;
} }
} }
struct point struct point
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
unsigned rotation = screen->rotation; unsigned rotation = screen->rotation;
assert(rotation < 4); assert(rotation < 4);
int32_t w = screen->content_size.width; int32_t w = screen->content_size.width;
int32_t h = screen->content_size.height; int32_t h = screen->content_size.height;
x = (int64_t) (x - screen->rect.x) * w / screen->rect.w;
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
// rotate
struct point result; struct point result;
switch (rotation) { switch (rotation) {
case 0: case 0:
@ -590,3 +615,22 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
} }
return result; return result;
} }
struct point
screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
screen_hidpi_scale_coords(screen, &x, &y);
return screen_convert_drawable_to_frame_coords(screen, x, y);
}
void
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) {
// take the HiDPI scaling (dw/ww and dh/wh) into account
int ww, wh, dw, dh;
SDL_GetWindowSize(screen->window, &ww, &wh);
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
// scale for HiDPI (64 bits for intermediate multiplications)
*x = (int64_t) *x * dw / ww;
*y = (int64_t) *y * dh / wh;
}

View File

@ -9,8 +9,6 @@
#include "common.h" #include "common.h"
#include "opengl.h" #include "opengl.h"
#define WINDOW_POSITION_UNDEFINED (-0x8000)
struct video_buffer; struct video_buffer;
struct screen { struct screen {
@ -21,13 +19,16 @@ struct screen {
struct sc_opengl gl; struct sc_opengl gl;
struct size frame_size; struct size frame_size;
struct size content_size; // rotated frame_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; bool resize_pending; // resize requested while fullscreen or maximized
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be // The content size the last time the window was not maximized or
// able to revert the size to its non-maximized value. // fullscreen (meaningful only when resize_pending is true)
struct size windowed_window_size_backup; struct size windowed_content_size;
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
unsigned rotation; unsigned rotation;
// rectangle of the content (excluding black borders)
struct SDL_Rect rect;
bool has_frame; bool has_frame;
bool fullscreen; bool fullscreen;
bool maximized; bool maximized;
@ -49,15 +50,18 @@ struct screen {
.width = 0, \ .width = 0, \
.height = 0, \ .height = 0, \
}, \ }, \
.windowed_window_size = { \ .resize_pending = false, \
.width = 0, \ .windowed_content_size = { \
.height = 0, \
}, \
.windowed_window_size_backup = { \
.width = 0, \ .width = 0, \
.height = 0, \ .height = 0, \
}, \ }, \
.rotation = 0, \ .rotation = 0, \
.rect = { \
.x = 0, \
.y = 0, \
.w = 0, \
.h = 0, \
}, \
.has_frame = false, \ .has_frame = false, \
.fullscreen = false, \ .fullscreen = false, \
.maximized = false, \ .maximized = false, \
@ -70,7 +74,7 @@ void
screen_init(struct screen *screen); screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden) // initialize screen, create window, renderer and texture (window is hidden)
// window_x and window_y accept WINDOW_POSITION_UNDEFINED // window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init_rendering(struct screen *screen, const char *window_title,
struct size frame_size, bool always_on_top, struct size frame_size, bool always_on_top,
@ -91,8 +95,11 @@ bool
screen_update_frame(struct screen *screen, struct video_buffer *vb); screen_update_frame(struct screen *screen, struct video_buffer *vb);
// render the texture to the renderer // render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
// changed, so that the content rectangle is recomputed
void void
screen_render(struct screen *screen); screen_render(struct screen *screen, bool update_content_rect);
// switch the fullscreen mode // switch the fullscreen mode
void void
@ -117,6 +124,20 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
// convert point from window coordinates to frame coordinates // convert point from window coordinates to frame coordinates
// x and y are expressed in pixels // x and y are expressed in pixels
struct point struct point
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y); screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// convert point from drawable coordinates to frame coordinates
// x and y are expressed in pixels
struct point
screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// Convert coordinates from window to drawable.
// Events are expressed in window coordinates, but content is expressed in
// drawable coordinates. They are the same if HiDPI scaling is 1, but differ
// otherwise.
void
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y);
#endif #endif

View File

@ -143,7 +143,7 @@ listen_on_port(uint16_t port) {
static bool static bool
enable_tunnel_reverse_any_port(struct server *server, enable_tunnel_reverse_any_port(struct server *server,
struct port_range port_range) { struct sc_port_range port_range) {
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (!enable_tunnel_reverse(server->serial, port)) { if (!enable_tunnel_reverse(server->serial, port)) {
@ -189,7 +189,7 @@ enable_tunnel_reverse_any_port(struct server *server,
static bool static bool
enable_tunnel_forward_any_port(struct server *server, enable_tunnel_forward_any_port(struct server *server,
struct port_range port_range) { struct sc_port_range port_range) {
server->tunnel_forward = true; server->tunnel_forward = true;
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
@ -217,24 +217,46 @@ enable_tunnel_forward_any_port(struct server *server,
} }
static bool static bool
enable_tunnel_any_port(struct server *server, struct port_range port_range) { enable_tunnel_any_port(struct server *server, struct sc_port_range port_range,
bool force_adb_forward) {
if (!force_adb_forward) {
// Attempt to use "adb reverse"
if (enable_tunnel_reverse_any_port(server, port_range)) { if (enable_tunnel_reverse_any_port(server, port_range)) {
return true; return true;
} }
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to // if "adb reverse" does not work (e.g. over "adb connect"), it
// "adb forward", so the app socket is the client // fallbacks to "adb forward", so the app socket is the client
LOGW("'adb reverse' failed, fallback to 'adb forward'"); LOGW("'adb reverse' failed, fallback to 'adb forward'");
}
return enable_tunnel_forward_any_port(server, port_range); return enable_tunnel_forward_any_port(server, port_range);
} }
static const char *
log_level_to_server_string(enum sc_log_level level) {
switch (level) {
case SC_LOG_LEVEL_DEBUG:
return "debug";
case SC_LOG_LEVEL_INFO:
return "info";
case SC_LOG_LEVEL_WARN:
return "warn";
case SC_LOG_LEVEL_ERROR:
return "error";
default:
assert(!"unexpected log level");
return "(unknown)";
}
}
static process_t static process_t
execute_server(struct server *server, const struct server_params *params) { execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6]; char max_fps_string[6];
char lock_video_orientation_string[3]; char lock_video_orientation_string[5];
char display_id_string[6]; char display_id_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
@ -259,6 +281,7 @@ execute_server(struct server *server, const struct server_params *params) {
"/", // unused "/", // unused
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
SCRCPY_VERSION, SCRCPY_VERSION,
log_level_to_server_string(params->log_level),
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
max_fps_string, max_fps_string,
@ -270,6 +293,7 @@ execute_server(struct server *server, const struct server_params *params) {
display_id_string, display_id_string,
params->show_touches ? "true" : "false", params->show_touches ? "true" : "false",
params->stay_awake ? "true" : "false", params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-",
}; };
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port " LOGI("Server debugger waiting for a client on device port "
@ -365,7 +389,8 @@ server_start(struct server *server, const char *serial,
goto error1; goto error1;
} }
if (!enable_tunnel_any_port(server, params->port_range)) { if (!enable_tunnel_any_port(server, params->port_range,
params->force_adb_forward)) {
goto error1; goto error1;
} }

View File

@ -9,6 +9,8 @@
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
#include "util/log.h"
#include "util/net.h" #include "util/net.h"
struct server { struct server {
@ -19,7 +21,7 @@ struct server {
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket; socket_t video_socket;
socket_t control_socket; socket_t control_socket;
struct port_range port_range; struct sc_port_range port_range;
uint16_t local_port; // selected from port_range uint16_t local_port; // selected from port_range
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
@ -43,8 +45,10 @@ struct server {
} }
struct server_params { struct server_params {
enum sc_log_level log_level;
const char *crop; const char *crop;
struct port_range port_range; const char *codec_options;
struct sc_port_range port_range;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
@ -53,6 +57,7 @@ struct server_params {
uint16_t display_id; uint16_t display_id;
bool show_touches; bool show_touches;
bool stay_awake; bool stay_awake;
bool force_adb_forward;
}; };
// init default values // init default values

View File

@ -65,7 +65,10 @@ static void test_buffer_read64be(void) {
assert(val == 0xABCD1234567890EF); assert(val == 0xABCD1234567890EF);
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_buffer_write16be(); test_buffer_write16be();
test_buffer_write32be(); test_buffer_write32be();
test_buffer_write64be(); test_buffer_write64be();

View File

@ -65,7 +65,10 @@ static void test_cbuf_push_take(void) {
assert(item == 35); assert(item == 35);
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_cbuf_empty(); test_cbuf_empty();
test_cbuf_full(); test_cbuf_full();
test_cbuf_push_take(); test_cbuf_push_take();

View File

@ -1,7 +1,9 @@
#include <assert.h> #include <assert.h>
#include <string.h>
#include "cli.h" #include "cli.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
static void test_flag_version(void) { static void test_flag_version(void) {
struct scrcpy_cli_args args = { struct scrcpy_cli_args args = {
@ -73,7 +75,6 @@ static void test_options(void) {
const struct scrcpy_options *opts = &args.opts; const struct scrcpy_options *opts = &args.opts;
assert(opts->always_on_top); assert(opts->always_on_top);
fprintf(stderr, "%d\n", (int) opts->bit_rate);
assert(opts->bit_rate == 5000000); assert(opts->bit_rate == 5000000);
assert(!strcmp(opts->crop, "100:200:300:400")); assert(!strcmp(opts->crop, "100:200:300:400"));
assert(opts->fullscreen); assert(opts->fullscreen);
@ -84,7 +85,7 @@ static void test_options(void) {
assert(opts->port_range.last == 1236); assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file")); assert(!strcmp(opts->record_filename, "file"));
assert(opts->record_format == RECORDER_FORMAT_MKV); assert(opts->record_format == SC_RECORD_FORMAT_MKV);
assert(opts->render_expired_frames); assert(opts->render_expired_frames);
assert(!strcmp(opts->serial, "0123456789abcdef")); assert(!strcmp(opts->serial, "0123456789abcdef"));
assert(opts->show_touches); assert(opts->show_touches);
@ -119,13 +120,54 @@ static void test_options2(void) {
assert(!opts->control); assert(!opts->control);
assert(!opts->display); assert(!opts->display);
assert(!strcmp(opts->record_filename, "file.mp4")); assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == RECORDER_FORMAT_MP4); assert(opts->record_format == SC_RECORD_FORMAT_MP4);
} }
int main(void) { static void test_parse_shortcut_mods(void) {
struct sc_shortcut_mods mods;
bool ok;
ok = sc_parse_shortcut_mods("lctrl", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == SC_MOD_LCTRL);
ok = sc_parse_shortcut_mods("lctrl+lalt", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT));
ok = sc_parse_shortcut_mods("rctrl,lalt", &mods);
assert(ok);
assert(mods.count == 2);
assert(mods.data[0] == SC_MOD_RCTRL);
assert(mods.data[1] == SC_MOD_LALT);
ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods);
assert(ok);
assert(mods.count == 3);
assert(mods.data[0] == SC_MOD_LSUPER);
assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT));
assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT));
ok = sc_parse_shortcut_mods("", &mods);
assert(!ok);
ok = sc_parse_shortcut_mods("lctrl+", &mods);
assert(!ok);
ok = sc_parse_shortcut_mods("lctrl,", &mods);
assert(!ok);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_flag_version(); test_flag_version();
test_flag_help(); test_flag_help();
test_options(); test_options();
test_options2(); test_options2();
test_parse_shortcut_mods();
return 0; return 0;
}; };

View File

@ -9,18 +9,20 @@ static void test_serialize_inject_keycode(void) {
.inject_keycode = { .inject_keycode = {
.action = AKEY_EVENT_ACTION_UP, .action = AKEY_EVENT_ACTION_UP,
.keycode = AKEYCODE_ENTER, .keycode = AKEYCODE_ENTER,
.repeat = 5,
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 10); assert(size == 14);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_KEYCODE,
0x01, // AKEY_EVENT_ACTION_UP 0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
0x00, 0x00, 0x00, 0X05, // repeat
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -34,13 +36,13 @@ static void test_serialize_inject_text(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 16); assert(size == 18);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_TEXT, CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x0d, // text length 0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -54,15 +56,17 @@ static void test_serialize_inject_text_long(void) {
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text; msg.inject_text.text = text;
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x01; expected[1] = 0x00;
expected[2] = 0x2c; // text length (16 bits) expected[2] = 0x00;
memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); expected[3] = 0x01;
expected[4] = 0x2c; // text length (32 bits)
memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -88,7 +92,7 @@ static void test_serialize_inject_touch_event(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 28); assert(size == 28);
@ -123,7 +127,7 @@ static void test_serialize_inject_scroll_event(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 21); assert(size == 21);
@ -142,7 +146,7 @@ static void test_serialize_back_or_screen_on(void) {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -157,7 +161,7 @@ static void test_serialize_expand_notification_panel(void) {
.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -172,7 +176,7 @@ static void test_serialize_collapse_notification_panel(void) {
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -187,7 +191,7 @@ static void test_serialize_get_clipboard(void) {
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD, .type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -200,18 +204,20 @@ static void test_serialize_get_clipboard(void) {
static void test_serialize_set_clipboard(void) { static void test_serialize_set_clipboard(void) {
struct control_msg msg = { struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD, .type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
.inject_text = { .set_clipboard = {
.paste = true,
.text = "hello, world!", .text = "hello, world!",
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 16); assert(size == 19);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x00, 0x0d, // text length 1, // paste
0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -225,7 +231,7 @@ static void test_serialize_set_screen_power_mode(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
@ -241,7 +247,7 @@ static void test_serialize_rotate_device(void) {
.type = CONTROL_MSG_TYPE_ROTATE_DEVICE, .type = CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -251,7 +257,10 @@ static void test_serialize_rotate_device(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_serialize_inject_keycode(); test_serialize_inject_keycode();
test_serialize_inject_text(); test_serialize_inject_text();
test_serialize_inject_text_long(); test_serialize_inject_text_long();

View File

@ -4,16 +4,17 @@
#include "device_msg.h" #include "device_msg.h"
#include <stdio.h> #include <stdio.h>
static void test_deserialize_clipboard(void) { static void test_deserialize_clipboard(void) {
const unsigned char input[] = { const unsigned char input[] = {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,
0x00, 0x03, // text length 0x00, 0x00, 0x00, 0x03, // text length
0x41, 0x42, 0x43, // "ABC" 0x41, 0x42, 0x43, // "ABC"
}; };
struct device_msg msg; struct device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 6); assert(r == 8);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text); assert(msg.clipboard.text);
@ -22,7 +23,33 @@ static void test_deserialize_clipboard(void) {
device_msg_destroy(&msg); device_msg_destroy(&msg);
} }
int main(void) { static void test_deserialize_clipboard_big(void) {
unsigned char input[DEVICE_MSG_MAX_SIZE];
input[0] = DEVICE_MSG_TYPE_CLIPBOARD;
input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24;
input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16;
input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8;
input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu;
memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH);
struct device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
assert(r == DEVICE_MSG_MAX_SIZE);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text);
assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH);
assert(msg.clipboard.text[0] == 'a');
device_msg_destroy(&msg);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_deserialize_clipboard(); test_deserialize_clipboard();
test_deserialize_clipboard_big();
return 0; return 0;
} }

View File

@ -32,7 +32,10 @@ static void test_queue(void) {
assert(queue_is_empty(&queue)); assert(queue_is_empty(&queue));
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_queue(); test_queue();
return 0; return 0;
} }

View File

@ -286,7 +286,10 @@ static void test_parse_integer_with_suffix(void) {
assert(!ok); assert(!ok);
} }
int main(void) { int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_xstrncpy_simple(); test_xstrncpy_simple();
test_xstrncpy_just_fit(); test_xstrncpy_just_fit();
test_xstrncpy_truncated(); test_xstrncpy_truncated();

View File

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

View File

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

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '1.13', version: '1.15',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',

View File

@ -10,24 +10,24 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32: prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.3.1-win32-shared.zip \
ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \ 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \
ffmpeg-4.2.2-win32-shared ffmpeg-4.3.1-win32-shared
prepare-ffmpeg-dev-win32: prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.3.1-win32-dev.zip \
8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \ 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \
ffmpeg-4.2.2-win32-dev ffmpeg-4.3.1-win32-dev
prepare-ffmpeg-shared-win64: prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.3.1-win64-shared.zip \
5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \ dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \
ffmpeg-4.2.2-win64-shared ffmpeg-4.3.1-win64-shared
prepare-ffmpeg-dev-win64: prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.3.1-win64-dev.zip \
f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \ 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \
ffmpeg-4.2.2-win64-dev ffmpeg-4.3.1-win64-dev
prepare-sdl2: prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \
@ -35,6 +35,6 @@ prepare-sdl2:
SDL2-2.0.12 SDL2-2.0.12
prepare-adb: prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.4-windows.zip \
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ 413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee \
platform-tools platform-tools

View File

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

View File

@ -12,7 +12,7 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.13 SCRCPY_VERSION_NAME=1.15
PLATFORM=${ANDROID_PLATFORM:-29} PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
@ -42,6 +42,8 @@ echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl" cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ "$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/view/IRotationWatcher.aidl android/view/IRotationWatcher.aidl
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
@ -55,6 +57,7 @@ cd "$CLASSES_DIR"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \ --output "$BUILD_DIR/classes.dex" \
android/view/*.class \ android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class com/genymobile/scrcpy/wrappers/*.class

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2008, The Android Open Source Project
*
* 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.
*/
package android.content;
/**
* {@hide}
*/
oneway interface IOnPrimaryClipChangedListener {
void dispatchPrimaryClipChanged();
}

View File

@ -19,18 +19,19 @@ public final class CleanUp {
// not instantiable // not instantiable
} }
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException { public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1; boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode;
if (needProcess) { if (needProcess) {
startProcess(disableShowTouches, restoreStayOn); startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode);
} else { } else {
// There is no additional clean up to do when scrcpy dies // There is no additional clean up to do when scrcpy dies
unlinkSelf(); unlinkSelf();
} }
} }
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException { private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)}; String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
restoreStayOn), String.valueOf(restoreNormalPowerMode)};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH); builder.environment().put("CLASSPATH", SERVER_PATH);
@ -59,6 +60,7 @@ public final class CleanUp {
boolean disableShowTouches = Boolean.parseBoolean(args[0]); boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]); int restoreStayOn = Integer.parseInt(args[1]);
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
if (disableShowTouches || restoreStayOn != -1) { if (disableShowTouches || restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager(); ServiceManager serviceManager = new ServiceManager();
@ -73,5 +75,10 @@ public final class CleanUp {
} }
} }
} }
if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}
} }
} }

View File

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

View File

@ -28,15 +28,18 @@ public final class ControlMessage {
private Position position; private Position position;
private int hScroll; private int hScroll;
private int vScroll; private int vScroll;
private boolean paste;
private int repeat;
private ControlMessage() { private ControlMessage() {
} }
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_KEYCODE; msg.type = TYPE_INJECT_KEYCODE;
msg.action = action; msg.action = action;
msg.keycode = keycode; msg.keycode = keycode;
msg.repeat = repeat;
msg.metaState = metaState; msg.metaState = metaState;
return msg; return msg;
} }
@ -68,10 +71,11 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createSetClipboard(String text) { public static ControlMessage createSetClipboard(String text, boolean paste) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD; msg.type = TYPE_SET_CLIPBOARD;
msg.text = text; msg.text = text;
msg.paste = paste;
return msg; return msg;
} }
@ -134,4 +138,12 @@ public final class ControlMessage {
public int getVScroll() { public int getVScroll() {
return vScroll; return vScroll;
} }
public boolean getPaste() {
return paste;
}
public int getRepeat() {
return repeat;
}
} }

View File

@ -8,18 +8,19 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader { public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes
public static final int INJECT_TEXT_MAX_LENGTH = 300; public static final int INJECT_TEXT_MAX_LENGTH = 300;
private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
public ControlMessageReader() { public ControlMessageReader() {
// invariant: the buffer is always in "get" mode // invariant: the buffer is always in "get" mode
@ -97,20 +98,23 @@ public class ControlMessageReader {
} }
int action = toUnsigned(buffer.get()); int action = toUnsigned(buffer.get());
int keycode = buffer.getInt(); int keycode = buffer.getInt();
int repeat = buffer.getInt();
int metaState = buffer.getInt(); int metaState = buffer.getInt();
return ControlMessage.createInjectKeycode(action, keycode, metaState); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
} }
private String parseString() { private String parseString() {
if (buffer.remaining() < 2) { if (buffer.remaining() < 4) {
return null; return null;
} }
int len = toUnsigned(buffer.getShort()); int len = buffer.getInt();
if (buffer.remaining() < len) { if (buffer.remaining() < len) {
return null; return null;
} }
buffer.get(textBuffer, 0, len); int position = buffer.position();
return new String(textBuffer, 0, len, StandardCharsets.UTF_8); // Move the buffer position to consume the text
buffer.position(position + len);
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
} }
private ControlMessage parseInjectText() { private ControlMessage parseInjectText() {
@ -147,11 +151,15 @@ public class ControlMessageReader {
} }
private ControlMessage parseSetClipboard() { private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;
}
boolean paste = buffer.get() != 0;
String text = parseString(); String text = parseString();
if (text == null) { if (text == null) {
return null; return null;
} }
return ControlMessage.createSetClipboard(text); return ControlMessage.createSetClipboard(text, paste);
} }
private ControlMessage parseSetScreenPowerMode() { private ControlMessage parseSetScreenPowerMode() {

View File

@ -1,20 +1,23 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Controller { public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1; private static final int DEVICE_ID_VIRTUAL = -1;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@ -26,6 +29,8 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
@ -50,10 +55,10 @@ public class Controller {
public void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, power on the device
if (!device.isScreenOn()) { if (!device.isScreenOn()) {
injectKeycode(KeyEvent.KEYCODE_POWER); device.injectKeycode(KeyEvent.KEYCODE_WAKEUP);
// dirty hack // dirty hack
// After POWER is injected, the device is powered on asynchronously. // After the keycode is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that // To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would // would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on. // be "canceled" once the device is turned back on.
@ -76,7 +81,7 @@ public class Controller {
switch (msg.getType()) { switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
} }
break; break;
case ControlMessage.TYPE_INJECT_TEXT: case ControlMessage.TYPE_INJECT_TEXT:
@ -107,14 +112,21 @@ public class Controller {
break; break;
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = device.getClipboardText(); String clipboardText = device.getClipboardText();
if (clipboardText != null) {
sender.pushClipboardText(clipboardText); sender.pushClipboardText(clipboardText);
}
break; break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
device.setClipboardText(msg.getText()); setClipboard(msg.getText(), msg.getPaste());
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
device.setScreenPowerMode(msg.getAction()); int mode = msg.getAction();
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
} }
break; break;
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
@ -125,8 +137,11 @@ public class Controller {
} }
} }
private boolean injectKeycode(int action, int keycode, int metaState) { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
return injectKeyEvent(action, keycode, 0, metaState); if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
schedulePowerModeOff();
}
return device.injectKeyEvent(action, keycode, repeat, metaState);
} }
private boolean injectChar(char c) { private boolean injectChar(char c) {
@ -137,7 +152,7 @@ public class Controller {
return false; return false;
} }
for (KeyEvent event : events) { for (KeyEvent event : events) {
if (!injectEvent(event)) { if (!device.injectEvent(event)) {
return false; return false;
} }
} }
@ -161,7 +176,7 @@ public class Controller {
Point point = device.getPhysicalPoint(position); Point point = device.getPhysicalPoint(position);
if (point == null) { if (point == null) {
// ignore event Ln.w("Ignore touch event, it was generated for a different device size");
return false; return false;
} }
@ -193,7 +208,7 @@ public class Controller {
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0); InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event); return device.injectEvent(event);
} }
private boolean injectScroll(Position position, int hScroll, int vScroll) { private boolean injectScroll(Position position, int hScroll, int vScroll) {
@ -216,26 +231,41 @@ public class Controller {
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0); InputDevice.SOURCE_TOUCHSCREEN, 0);
return injectEvent(event); return device.injectEvent(event);
} }
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { /**
long now = SystemClock.uptimeMillis(); * Schedule a call to set power mode to off after a small delay.
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, */
InputDevice.SOURCE_KEYBOARD); private static void schedulePowerModeOff() {
return injectEvent(event); EXECUTOR.schedule(new Runnable() {
@Override
public void run() {
Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
} }
}, 200, TimeUnit.MILLISECONDS);
private boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
private boolean injectEvent(InputEvent event) {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} }
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_WAKEUP;
return injectKeycode(keycode); if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_WAKEUP) {
schedulePowerModeOff();
}
return device.injectKeycode(keycode);
}
private boolean setClipboard(String text, boolean paste) {
boolean ok = device.setClipboardText(text);
if (ok) {
Ln.i("Device clipboard set");
}
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
}
return ok;
} }
} }

View File

@ -1,17 +1,24 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager; import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.SystemClock;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import android.view.InputDevice;
import android.view.InputEvent; import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
public final class Device { public final class Device {
@ -22,10 +29,16 @@ public final class Device {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
} }
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
private final ServiceManager serviceManager = new ServiceManager(); private final ServiceManager serviceManager = new ServiceManager();
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/** /**
* Logical display identifier * Logical display identifier
@ -52,9 +65,9 @@ public final class Device {
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
layerStack = displayInfo.getLayerStack(); layerStack = displayInfo.getLayerStack();
registerRotationWatcher(new IRotationWatcher.Stub() { serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) throws RemoteException { public void onRotationChanged(int rotation) {
synchronized (Device.this) { synchronized (Device.this) {
screenInfo = screenInfo.withDeviceRotation(rotation); screenInfo = screenInfo.withDeviceRotation(rotation);
@ -66,6 +79,32 @@ public final class Device {
} }
}, displayId); }, displayId);
if (options.getControl()) {
// If control is enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = serviceManager.getClipboardManager();
if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it
return;
}
synchronized (Device.this) {
if (clipboardListener != null) {
String text = getClipboardText();
if (text != null) {
clipboardListener.onClipboardTextChanged(text);
}
}
}
}
});
} else {
Ln.w("No clipboard manager, copy-paste between device and computer will not work");
}
}
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
} }
@ -118,7 +157,7 @@ public final class Device {
return supportsInputEvents; return supportsInputEvents;
} }
public boolean injectInputEvent(InputEvent inputEvent, int mode) { public boolean injectEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) { if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()"); throw new AssertionError("Could not inject input event if !supportsInputEvents()");
} }
@ -130,18 +169,33 @@ public final class Device {
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
} }
public boolean injectEvent(InputEvent event) {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEvent(event);
}
public boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
public boolean isScreenOn() { public boolean isScreenOn() {
return serviceManager.getPowerManager().isScreenOn(); return serviceManager.getPowerManager().isScreenOn();
} }
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}
public synchronized void setRotationListener(RotationListener rotationListener) { public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener; this.rotationListener = rotationListener;
} }
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}
public void expandNotificationPanel() { public void expandNotificationPanel() {
serviceManager.getStatusBarManager().expandNotificationsPanel(); serviceManager.getStatusBarManager().expandNotificationsPanel();
} }
@ -151,33 +205,48 @@ public final class Device {
} }
public String getClipboardText() { public String getClipboardText() {
CharSequence s = serviceManager.getClipboardManager().getText(); ClipboardManager clipboardManager = serviceManager.getClipboardManager();
if (clipboardManager == null) {
return null;
}
CharSequence s = clipboardManager.getText();
if (s == null) { if (s == null) {
return null; return null;
} }
return s.toString(); return s.toString();
} }
public void setClipboardText(String text) { public boolean setClipboardText(String text) {
boolean ok = serviceManager.getClipboardManager().setText(text); ClipboardManager clipboardManager = serviceManager.getClipboardManager();
if (ok) { if (clipboardManager == null) {
Ln.i("Device clipboard set"); return false;
} }
String currentClipboard = getClipboardText();
if (currentClipboard == null || currentClipboard.equals(text)) {
// The clipboard already contains the requested text.
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
// problem, do not explicitly set the clipboard text if it already contains the expected content.
return false;
}
isSettingClipboard.set(true);
boolean ok = clipboardManager.setText(text);
isSettingClipboard.set(false);
return ok;
} }
/** /**
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants * @param mode one of the {@code POWER_MODE_*} constants
*/ */
public void setScreenPowerMode(int mode) { public static boolean setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay(); IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) { if (d == null) {
Ln.e("Could not get built-in display"); Ln.e("Could not get built-in display");
return; return false;
}
boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
if (ok) {
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
} }
return SurfaceControl.setDisplayPowerMode(d, mode);
} }
/** /**

View File

@ -7,10 +7,10 @@ import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter { public class DeviceMessageWriter {
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
@ -21,7 +21,7 @@ public class DeviceMessageWriter {
String text = msg.getText(); String text = msg.getText();
byte[] raw = text.getBytes(StandardCharsets.UTF_8); byte[] raw = text.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
buffer.putShort((short) len); buffer.putInt(len);
buffer.put(raw, 0, len); buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position()); output.write(rawBuffer, 0, buffer.position());
break; break;

View File

@ -15,14 +15,25 @@ public final class Ln {
DEBUG, INFO, WARN, ERROR DEBUG, INFO, WARN, ERROR
} }
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; private static Level threshold = Level.INFO;
private Ln() { private Ln() {
// not instantiable // not instantiable
} }
/**
* Initialize the log level.
* <p>
* Must be called before starting any new thread.
*
* @param level the log level
*/
public static void initLogLevel(Level level) {
threshold = level;
}
public static boolean isEnabled(Level level) { public static boolean isEnabled(Level level) {
return level.ordinal() >= THRESHOLD.ordinal(); return level.ordinal() >= threshold.ordinal();
} }
public static void d(String message) { public static void d(String message) {

View File

@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
import android.graphics.Rect; import android.graphics.Rect;
public class Options { public class Options {
private Ln.Level logLevel;
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
@ -14,6 +15,15 @@ public class Options {
private int displayId; private int displayId;
private boolean showTouches; private boolean showTouches;
private boolean stayAwake; private boolean stayAwake;
private String codecOptions;
public Ln.Level getLogLevel() {
return logLevel;
}
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
@ -102,4 +112,12 @@ public class Options {
public void setStayAwake(boolean stayAwake) { public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake; this.stayAwake = stayAwake;
} }
public String getCodecOptions() {
return codecOptions;
}
public void setCodecOptions(String codecOptions) {
this.codecOptions = codecOptions;
}
} }

View File

@ -12,6 +12,7 @@ import android.view.Surface;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener { public class ScreenEncoder implements Device.RotationListener {
@ -25,15 +26,17 @@ public class ScreenEncoder implements Device.RotationListener {
private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private List<CodecOption> codecOptions;
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
private boolean sendFrameMeta; private boolean sendFrameMeta;
private long ptsOrigin; private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
this.sendFrameMeta = sendFrameMeta; this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
this.codecOptions = codecOptions;
} }
@Override @Override
@ -61,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener {
} }
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps); MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
try { try {
@ -151,7 +154,24 @@ public class ScreenEncoder implements Device.RotationListener {
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
} }
private static MediaFormat createFormat(int bitRate, int maxFps) { private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
String key = codecOption.getKey();
Object value = codecOption.getValue();
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
@ -167,6 +187,13 @@ public class ScreenEncoder implements Device.RotationListener {
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437> // <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
} }
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
setCodecOption(format, option);
}
}
return format; return format;
} }

View File

@ -8,6 +8,8 @@ import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Locale;
public final class Server { public final class Server {
@ -19,6 +21,7 @@ public final class Server {
private static void scrcpy(Options options) throws IOException { private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options); final Device device = new Device(options);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
boolean mustDisableShowTouchesOnCleanUp = false; boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1; int restoreStayOn = -1;
@ -46,18 +49,26 @@ public final class Server {
} }
} }
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn); CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
if (options.getControl()) { if (options.getControl()) {
Controller controller = new Controller(device, connection); final Controller controller = new Controller(device, connection);
// asynchronous // asynchronous
startController(controller); startController(controller);
startDeviceMessageSender(controller.getSender()); startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
} }
try { try {
@ -109,47 +120,53 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
} }
final int expectedParameters = 12; final int expectedParameters = 14;
if (args.length != expectedParameters) { if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
} }
Options options = new Options(); Options options = new Options();
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[2]); int bitRate = Integer.parseInt(args[3]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[3]); int maxFps = Integer.parseInt(args[4]);
options.setMaxFps(maxFps); options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[4]); int lockedVideoOrientation = Integer.parseInt(args[5]);
options.setLockedVideoOrientation(lockedVideoOrientation); options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[5]); boolean tunnelForward = Boolean.parseBoolean(args[6]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[6]); Rect crop = parseCrop(args[7]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[7]); boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[8]); boolean control = Boolean.parseBoolean(args[9]);
options.setControl(control); options.setControl(control);
int displayId = Integer.parseInt(args[9]); int displayId = Integer.parseInt(args[10]);
options.setDisplayId(displayId); options.setDisplayId(displayId);
boolean showTouches = Boolean.parseBoolean(args[10]); boolean showTouches = Boolean.parseBoolean(args[11]);
options.setShowTouches(showTouches); options.setShowTouches(showTouches);
boolean stayAwake = Boolean.parseBoolean(args[11]); boolean stayAwake = Boolean.parseBoolean(args[12]);
options.setStayAwake(stayAwake); options.setStayAwake(stayAwake);
String codecOptions = args[13];
options.setCodecOptions(codecOptions);
return options; return options;
} }
@ -202,6 +219,9 @@ public final class Server {
}); });
Options options = createOptions(args); Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
scrcpy(options); scrcpy(options);
} }
} }

View File

@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Ln;
import android.content.ClipData; import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
import android.os.Build; import android.os.Build;
import android.os.IInterface; import android.os.IInterface;
@ -13,6 +14,7 @@ public class ClipboardManager {
private final IInterface manager; private final IInterface manager;
private Method getPrimaryClipMethod; private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod; private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
public ClipboardManager(IInterface manager) { public ClipboardManager(IInterface manager) {
this.manager = manager; this.manager = manager;
@ -81,4 +83,37 @@ public class ClipboardManager {
return false; return false;
} }
} }
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
} else {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
}
}
return addPrimaryClipChangedListener;
}
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try {
Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, manager, listener);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
} }

View File

@ -77,7 +77,14 @@ public final class ServiceManager {
public ClipboardManager getClipboardManager() { public ClipboardManager getClipboardManager() {
if (clipboardManager == null) { if (clipboardManager == null) {
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); IInterface clipboard = getService("clipboard", "android.content.IClipboard");
if (clipboard == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
clipboardManager = new ClipboardManager(clipboard);
} }
return clipboardManager; return clipboardManager;
} }

View File

@ -0,0 +1,114 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
public class CodecOptionsTest {
@Test
public void testIntegerImplicit() {
List<CodecOption> codecOptions = CodecOption.parse("some_key=5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertEquals(5, option.getValue());
}
@Test
public void testInteger() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:int=5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof Integer);
Assert.assertEquals(5, option.getValue());
}
@Test
public void testLong() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:long=5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof Long);
Assert.assertEquals(5L, option.getValue());
}
@Test
public void testFloat() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:float=4.5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof Float);
Assert.assertEquals(4.5f, option.getValue());
}
@Test
public void testString() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=some_value");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof String);
Assert.assertEquals("some_value", option.getValue());
}
@Test
public void testStringEscaped() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof String);
Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue());
}
@Test
public void testList() {
List<CodecOption> codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c");
Assert.assertEquals(5, codecOptions.size());
CodecOption option;
option = codecOptions.get(0);
Assert.assertEquals("a", option.getKey());
Assert.assertTrue(option.getValue() instanceof Integer);
Assert.assertEquals(1, option.getValue());
option = codecOptions.get(1);
Assert.assertEquals("b", option.getKey());
Assert.assertTrue(option.getValue() instanceof Integer);
Assert.assertEquals(2, option.getValue());
option = codecOptions.get(2);
Assert.assertEquals("c", option.getKey());
Assert.assertTrue(option.getValue() instanceof Long);
Assert.assertEquals(3L, option.getValue());
option = codecOptions.get(3);
Assert.assertEquals("d", option.getKey());
Assert.assertTrue(option.getValue() instanceof Float);
Assert.assertEquals(4.5f, option.getValue());
option = codecOptions.get(4);
Assert.assertEquals("e", option.getKey());
Assert.assertTrue(option.getValue() instanceof String);
Assert.assertEquals("a,b=c", option.getValue());
}
}

View File

@ -25,6 +25,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP); dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -37,6 +38,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
} }
@ -48,7 +50,7 @@ public class ControlMessageReaderTest {
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8); byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length); dos.writeInt(text.length);
dos.write(text); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -68,7 +70,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a'); Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length); dos.writeInt(text.length);
dos.write(text); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -216,8 +218,9 @@ public class ControlMessageReaderTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
dos.writeByte(1); // paste
byte[] text = "testé".getBytes(StandardCharsets.UTF_8); byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeShort(text.length); dos.writeInt(text.length);
dos.write(text); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -227,6 +230,33 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals("testé", event.getText()); Assert.assertEquals("testé", event.getText());
Assert.assertTrue(event.getPaste());
}
@Test
public void testParseBigSetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
dos.writeByte(1); // paste
Arrays.fill(rawText, (byte) 'a');
String text = new String(rawText, 0, rawText.length);
dos.writeInt(rawText.length);
dos.write(rawText);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(text, event.getText());
Assert.assertTrue(event.getPaste());
} }
@Test @Test
@ -276,11 +306,13 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP); dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(0); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(1); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
@ -290,12 +322,14 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(0, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next(); event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(1, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
} }
@ -309,6 +343,7 @@ public class ControlMessageReaderTest {
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP); dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(4); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
@ -321,6 +356,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(4, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next(); event = reader.next();
@ -328,6 +364,7 @@ public class ControlMessageReaderTest {
bos.reset(); bos.reset();
dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeInt(KeyEvent.META_CTRL_ON);
packet = bos.toByteArray(); packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet)); reader.readFrom(new ByteArrayInputStream(packet));
@ -337,6 +374,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
} }
} }

View File

@ -19,7 +19,7 @@ public class DeviceMessageWriterTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeShort(data.length); dos.writeInt(data.length);
dos.write(data); dos.write(data);
byte[] expected = bos.toByteArray(); byte[] expected = bos.toByteArray();