Compare commits
157 Commits
Author | SHA1 | Date | |
---|---|---|---|
9a7e8da12c | |||
dc7b60e619 | |||
1e4ee547b5 | |||
488d22d4e2 | |||
00d292b2f5 | |||
245999aec4 | |||
d91c5dcfd5 | |||
d202d7b205 | |||
08c0c64af6 | |||
a00a8763d6 | |||
8f314c74b0 | |||
6e1069a822 | |||
c4323df976 | |||
8ff07e0c88 | |||
e4efd75766 | |||
0e4a6f462b | |||
8b73c90427 | |||
ef91ab2841 | |||
44fa4a090e | |||
dcde578a50 | |||
93a5c5149d | |||
2ca8318b9d | |||
d499ee53c9 | |||
8f619f337b | |||
fc1dec0270 | |||
274b591d18 | |||
4bbabfb4ef | |||
ffc57512b3 | |||
c7a33fac36 | |||
81573d81a0 | |||
5c2cf88a1d | |||
8f46e18426 | |||
ee3882f8be | |||
a3ef461d73 | |||
3df63c579d | |||
56bff2f718 | |||
080a4ee365 | |||
c7155a1954 | |||
517dbd9c85 | |||
acc4ef31df | |||
73e722784d | |||
e1cd75792c | |||
ac4c8b4a3f | |||
fae3f9eeab | |||
f5aeecbc62 | |||
e40532a376 | |||
d860ad48e6 | |||
ec047b501e | |||
4c2e10fd74 | |||
6b1da2fcff | |||
2608b1dc62 | |||
31fa115655 | |||
a85848a541 | |||
82446b6b0d | |||
24c8039244 | |||
28abd98f7f | |||
e2d5f0e7fc | |||
1a9429f3c7 | |||
0b4e484da0 | |||
ead7ee4a03 | |||
74ece9b45b | |||
c77024314d | |||
828327365a | |||
4668638ee1 | |||
dbb0df607c | |||
2f74ec2518 | |||
8c6799297b | |||
62c0c1321f | |||
d4eeb1c84d | |||
4e9e712312 | |||
9babe26805 | |||
76567e684a | |||
b55ca127f8 | |||
a12b938234 | |||
a14840a515 | |||
8581d6850b | |||
92cb3a6661 | |||
561ede444e | |||
3c9ae99dda | |||
44f720e4a4 | |||
125c5561e8 | |||
14ead499fd | |||
94a7f1a0f8 | |||
cc22f4622a | |||
11a61b2cb3 | |||
bea7658807 | |||
8a9b20b27e | |||
d62eb2b11c | |||
eb8f7a1f28 | |||
270d0bf639 | |||
95fa1a69e4 | |||
ea46d3ab68 | |||
7eb16ce364 | |||
927d655ff6 | |||
ee2894779a | |||
1c6207f8ce | |||
ab52b36895 | |||
9f4735ede3 | |||
6295c1a110 | |||
f3fba3c4b9 | |||
c1ebea26e6 | |||
f07d21f050 | |||
a8fd4aec9a | |||
cbde7b964a | |||
28c71c528f | |||
d48b375a1d | |||
fd63e7eb5a | |||
cdd8edbbb6 | |||
9b9e717c41 | |||
15e4da08a3 | |||
2afbfc2c75 | |||
2cf022491f | |||
d30593e1d5 | |||
9e78b765da | |||
271de0954a | |||
54ccccd883 | |||
bea1c11f8e | |||
94e1696869 | |||
a346bb80f4 | |||
d421741a83 | |||
64d5edce92 | |||
4150eedcdf | |||
5031b2c8ff | |||
4adf5fde6d | |||
e050cfdcd6 | |||
dc7c677728 | |||
3504c0016b | |||
89d1602185 | |||
566ba766af | |||
a0af402d96 | |||
7bb91638ad | |||
600df37753 | |||
902b99174d | |||
cd69eb4a4f | |||
bc7508427b | |||
24ade6ad77 | |||
ae2d094362 | |||
c5f5d1e456 | |||
63286424bb | |||
da18c9cdab | |||
c396758b4e | |||
d3281f4b67 | |||
1982bc439b | |||
ef56cc6ff7 | |||
c0f428eb05 | |||
4794ca8ae7 | |||
f903cd376d | |||
e8127375ae | |||
1144f64214 | |||
39356602ed | |||
0fb22c3e98 | |||
96bd2c974d | |||
dc7fcf3c7a | |||
2a3a9d4ea9 | |||
ca0031cbde | |||
d1a9a76cc6 | |||
a8ceaf5284 |
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md).
|
||||||
|
- [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues).
|
||||||
|
|
||||||
|
**Environment**
|
||||||
|
- OS: [e.g. Debian, Windows, macOS...]
|
||||||
|
- scrcpy version: [e.g. 1.12.1]
|
||||||
|
- installation method: [e.g. manual build, apt, snap, brew, Windows release...]
|
||||||
|
- device model:
|
||||||
|
- Android version: [e.g. 10]
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
On errors, please provide the output of the console (and `adb logcat` if relevant).
|
||||||
|
|
||||||
|
```
|
||||||
|
Please paste terminal output in a code block.
|
||||||
|
```
|
||||||
|
|
||||||
|
Please do not post screenshots of your terminal, just post the content as text instead.
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist.
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,8 @@
|
|||||||
build/
|
build/
|
||||||
/dist/
|
/dist/
|
||||||
|
/build-*/
|
||||||
|
/build_*/
|
||||||
|
/release-*/
|
||||||
.idea/
|
.idea/
|
||||||
.gradle/
|
.gradle/
|
||||||
|
/x/
|
||||||
|
25
BUILD.md
25
BUILD.md
@ -8,6 +8,22 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
|
|||||||
|
|
||||||
[prebuilt server]: #prebuilt-server
|
[prebuilt server]: #prebuilt-server
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
### `master`
|
||||||
|
|
||||||
|
The `master` branch concerns the latest release, and is the home page of the
|
||||||
|
project on Github.
|
||||||
|
|
||||||
|
|
||||||
|
### `dev`
|
||||||
|
|
||||||
|
`dev` is the current development branch. Every commit present in `dev` will be
|
||||||
|
in the next release.
|
||||||
|
|
||||||
|
If you want to contribute code, please base your commits on the latest `dev`
|
||||||
|
branch.
|
||||||
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@ -233,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_.
|
|||||||
|
|
||||||
## Prebuilt server
|
## Prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v1.12.1`][direct-scrcpy-server]
|
- [`scrcpy-server-v1.14`][direct-scrcpy-server]
|
||||||
_(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_
|
_(SHA-256: 1d1b18a2b80e956771fd63b99b414d2d028713a8f12ddfa5a369709ad4295620)_
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-server-v1.14
|
||||||
|
|
||||||
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:
|
||||||
@ -247,3 +263,6 @@ meson x --buildtype release --strip -Db_lto=true \
|
|||||||
ninja -Cx
|
ninja -Cx
|
||||||
sudo ninja -Cx install
|
sudo ninja -Cx install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The server only works with a matching client version (this server works with the
|
||||||
|
`master` branch).
|
||||||
|
13
DEVELOP.md
13
DEVELOP.md
@ -189,7 +189,7 @@ The client uses 4 threads:
|
|||||||
recording,
|
recording,
|
||||||
- the **controller** thread, sending _control messages_ to the server,
|
- the **controller** thread, sending _control messages_ to the server,
|
||||||
- the **receiver** thread (managed by the controller), receiving _device
|
- the **receiver** thread (managed by the controller), receiving _device
|
||||||
messages_ from the client.
|
messages_ from the server.
|
||||||
|
|
||||||
In addition, another thread can be started if necessary to handle APK
|
In addition, another thread can be started if necessary to handle APK
|
||||||
installation or file push requests (via drag&drop on the main window) or to
|
installation or file push requests (via drag&drop on the main window) or to
|
||||||
@ -214,7 +214,7 @@ When a new decoded frame is available, the decoder _swaps_ the decoding and
|
|||||||
rendering frame (with proper synchronization). Thus, it immediatly starts
|
rendering frame (with proper synchronization). Thus, it immediatly starts
|
||||||
to decode a new frame while the main thread renders the last one.
|
to decode a new frame while the main thread renders the last one.
|
||||||
|
|
||||||
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
|
If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw
|
||||||
H.264 packet to the output video file.
|
H.264 packet to the output video file.
|
||||||
|
|
||||||
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
|
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
|
||||||
@ -282,6 +282,15 @@ meson x -Dserver_debugger=true
|
|||||||
meson configure x -Dserver_debugger=true
|
meson configure x -Dserver_debugger=true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If your device runs Android 8 or below, set the `server_debugger_method` to
|
||||||
|
`old` in addition:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
meson x -Dserver_debugger=true -Dserver_debugger_method=old
|
||||||
|
# or, if x is already configured
|
||||||
|
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
|
||||||
|
```
|
||||||
|
|
||||||
Then recompile.
|
Then recompile.
|
||||||
|
|
||||||
When you start scrcpy, it will start a debugger on port 5005 on the device.
|
When you start scrcpy, it will start a debugger on port 5005 on the device.
|
||||||
|
167
FAQ.md
167
FAQ.md
@ -3,19 +3,102 @@
|
|||||||
Here are the common reported problems and their status.
|
Here are the common reported problems and their status.
|
||||||
|
|
||||||
|
|
||||||
### On Windows, my device is not detected
|
## `adb` issues
|
||||||
|
|
||||||
The most common is your device not being detected by `adb`, or is unauthorized.
|
`scrcpy` execute `adb` commands to initialize the connection with the device. If
|
||||||
Check everything is ok by calling:
|
`adb` fails, then scrcpy will not work.
|
||||||
|
|
||||||
adb devices
|
In that case, it will print this error:
|
||||||
|
|
||||||
Windows may need some [drivers] to detect your device.
|
> ERROR: "adb push" returned with value 1
|
||||||
|
|
||||||
|
This is typically not a bug in _scrcpy_, but a problem in your environment.
|
||||||
|
|
||||||
|
To find out the cause, execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb devices
|
||||||
|
```
|
||||||
|
|
||||||
|
### `adb` not found
|
||||||
|
|
||||||
|
You need `adb` accessible from your `PATH`.
|
||||||
|
|
||||||
|
On Windows, the current directory is in your `PATH`, and `adb.exe` is included
|
||||||
|
in the release, so it should work out-of-the-box.
|
||||||
|
|
||||||
|
|
||||||
|
### Device unauthorized
|
||||||
|
|
||||||
|
Check [stackoverflow][device-unauthorized].
|
||||||
|
|
||||||
|
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
|
||||||
|
|
||||||
|
|
||||||
|
### Device not detected
|
||||||
|
|
||||||
|
If your device is not detected, you may need some [drivers] (on Windows).
|
||||||
|
|
||||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||||
|
|
||||||
|
|
||||||
### I can only mirror, I cannot interact with the device
|
### Several devices connected
|
||||||
|
|
||||||
|
If several devices are connected, you will encounter this error:
|
||||||
|
|
||||||
|
> adb: error: failed to get feature set: more than one device/emulator
|
||||||
|
|
||||||
|
the identifier of the device you want to mirror must be provided:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy -s 01234567890abcdef
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that if your device is connected over TCP/IP, you'll get this message:
|
||||||
|
|
||||||
|
> adb: error: more than one device/emulator
|
||||||
|
> ERROR: "adb reverse" returned with value 1
|
||||||
|
> WARN: 'adb reverse' failed, fallback to 'adb forward'
|
||||||
|
|
||||||
|
This is expected (due to a bug on old Android versions, see [#5]), but in that
|
||||||
|
case, scrcpy fallbacks to a different method, which should work.
|
||||||
|
|
||||||
|
[#5]: https://github.com/Genymobile/scrcpy/issues/5
|
||||||
|
|
||||||
|
|
||||||
|
### Conflicts between adb versions
|
||||||
|
|
||||||
|
> adb server version (41) doesn't match this client (39); killing...
|
||||||
|
|
||||||
|
This error occurs when you use several `adb` versions simultaneously. You must
|
||||||
|
find the program using a different `adb` version, and use the same `adb` version
|
||||||
|
everywhere.
|
||||||
|
|
||||||
|
You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to
|
||||||
|
use a specific `adb` binary, by setting the `ADB` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set ADB=/path/to/your/adb
|
||||||
|
scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Device disconnected
|
||||||
|
|
||||||
|
If _scrcpy_ stops itself with the warning "Device disconnected", then the
|
||||||
|
`adb` connection has been closed.
|
||||||
|
|
||||||
|
Try with another USB cable or plug it into another USB port. See [#281] and
|
||||||
|
[#283].
|
||||||
|
|
||||||
|
[#281]: https://github.com/Genymobile/scrcpy/issues/281
|
||||||
|
[#283]: https://github.com/Genymobile/scrcpy/issues/283
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Control issues
|
||||||
|
|
||||||
|
### Mouse and keyboard do not work
|
||||||
|
|
||||||
On some devices, you may need to enable an option to allow [simulating input].
|
On some devices, you may need to enable an option to allow [simulating input].
|
||||||
In developer options, enable:
|
In developer options, enable:
|
||||||
@ -29,22 +112,43 @@ In developer options, enable:
|
|||||||
### Mouse clicks at wrong location
|
### Mouse clicks at wrong location
|
||||||
|
|
||||||
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
||||||
scaled. See [issue 15].
|
scaled. See [#15].
|
||||||
|
|
||||||
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
|
[#15]: https://github.com/Genymobile/scrcpy/issues/15
|
||||||
|
|
||||||
A workaround is to build with HiDPI support disabled:
|
Open _scrcpy_ directly on the monitor you use it.
|
||||||
|
|
||||||
```bash
|
|
||||||
meson x --buildtype release -Dhidpi_support=false
|
### Special characters do not work
|
||||||
|
|
||||||
|
Injecting text input is [limited to ASCII characters][text-input]. A trick
|
||||||
|
allows to also inject some [accented characters][accented-characters], but
|
||||||
|
that's all. See [#37].
|
||||||
|
|
||||||
|
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
||||||
|
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
||||||
|
[#37]: https://github.com/Genymobile/scrcpy/issues/37
|
||||||
|
|
||||||
|
|
||||||
|
## Client issues
|
||||||
|
|
||||||
|
### The quality is low
|
||||||
|
|
||||||
|
If the definition of your client window is smaller than that of your device
|
||||||
|
screen, then you might get poor quality, especially visible on text (see [#40]).
|
||||||
|
|
||||||
|
[#40]: https://github.com/Genymobile/scrcpy/issues/40
|
||||||
|
|
||||||
|
To improve downscaling quality, trilinear filtering is enabled automatically
|
||||||
|
if the renderer is OpenGL and if it supports mipmapping.
|
||||||
|
|
||||||
|
On Windows, you might want to force OpenGL:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --render-driver=opengl
|
||||||
```
|
```
|
||||||
|
|
||||||
However, the video will be displayed at lower resolution.
|
You may also need to configure the [scaling behavior]:
|
||||||
|
|
||||||
|
|
||||||
### The quality is low on HiDPI display
|
|
||||||
|
|
||||||
On Windows, you may need to configure the [scaling behavior].
|
|
||||||
|
|
||||||
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
|
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
|
||||||
> Override high DPI scaling behavior > Scaling performed by: _Application_.
|
> Override high DPI scaling behavior > Scaling performed by: _Application_.
|
||||||
@ -52,6 +156,7 @@ On Windows, you may need to configure the [scaling behavior].
|
|||||||
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### KWin compositor crashes
|
### KWin compositor crashes
|
||||||
|
|
||||||
On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
|
On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
|
||||||
@ -61,19 +166,29 @@ As a workaround, [disable "Block compositing"][kwin].
|
|||||||
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
||||||
|
|
||||||
|
|
||||||
### I get an error "Could not open video stream"
|
## Crashes
|
||||||
|
|
||||||
|
### Exception
|
||||||
|
|
||||||
There may be many reasons. One common cause is that the hardware encoder of your
|
There may be many reasons. One common cause is that the hardware encoder of your
|
||||||
device is not able to encode at the given definition:
|
device is not able to encode at the given definition:
|
||||||
|
|
||||||
```
|
> ```
|
||||||
ERROR: Exception on thread Thread[main,5,main]
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
||||||
...
|
> ...
|
||||||
Exit due to uncaughtException in main thread:
|
> Exit due to uncaughtException in main thread:
|
||||||
ERROR: Could not open video stream
|
> ERROR: Could not open video stream
|
||||||
INFO: Initial texture: 1080x2336
|
> INFO: Initial texture: 1080x2336
|
||||||
```
|
> ```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
|
> java.lang.IllegalStateException
|
||||||
|
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||||
|
> ```
|
||||||
|
|
||||||
Just try with a lower definition:
|
Just try with a lower definition:
|
||||||
|
|
||||||
|
2
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
|||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
Copyright (C) 2018-2019 Romain Vimont
|
Copyright (C) 2018-2020 Romain Vimont
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -100,30 +100,30 @@ 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.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/SDL2-2.0.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
|
||||||
dist-win64: build-server build-win64 build-win64-noconsole
|
dist-win64: build-server build-win64 build-win64-noconsole
|
||||||
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(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.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp prebuilt-deps/SDL2-2.0.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
|
||||||
zip-win32: dist-win32
|
zip-win32: dist-win32
|
||||||
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
|
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
|
||||||
|
@ -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
|
||||||
@ -475,7 +477,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상
|
|||||||
## 라이선스
|
## 라이선스
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
Copyright (C) 2018-2019 Romain Vimont
|
Copyright (C) 2018-2020 Romain Vimont
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
228
README.md
228
README.md
@ -1,4 +1,4 @@
|
|||||||
# scrcpy (v1.12.1)
|
# scrcpy (v1.14)
|
||||||
|
|
||||||
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.
|
||||||
@ -37,8 +37,11 @@ control it using keyboard and mouse.
|
|||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
|
On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04):
|
||||||
it's not that hard.
|
|
||||||
|
```
|
||||||
|
apt install scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
A [Snap] package is available: [`scrcpy`][snap-link].
|
A [Snap] package is available: [`scrcpy`][snap-link].
|
||||||
|
|
||||||
@ -56,19 +59,38 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
|||||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||||
|
|
||||||
|
You could also [build the app manually][BUILD] (don't worry, it's not that
|
||||||
|
hard).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
For Windows, for simplicity, prebuilt archives with all the dependencies
|
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
||||||
(including `adb`) are available:
|
(including `adb`) is available:
|
||||||
|
|
||||||
- [`scrcpy-win32-v1.12.1.zip`][direct-win32]
|
- [`scrcpy-win64-v1.14.zip`][direct-win64]
|
||||||
_(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_
|
_(SHA-256: 2be9139e46e29cf2f5f695848bb2b75a543b8f38be1133257dc5068252abc25f)_
|
||||||
- [`scrcpy-win64-v1.12.1.zip`][direct-win64]
|
|
||||||
_(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_
|
|
||||||
|
|
||||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win32-v1.12.1.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-win64-v1.14.zip
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip
|
|
||||||
|
It is also available in [Chocolatey]:
|
||||||
|
|
||||||
|
[Chocolatey]: https://chocolatey.org/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
choco install scrcpy
|
||||||
|
choco install adb # if you don't have it yet
|
||||||
|
```
|
||||||
|
|
||||||
|
And in [Scoop]:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scoop install scrcpy
|
||||||
|
scoop install adb # if you don't have it yet
|
||||||
|
```
|
||||||
|
|
||||||
|
[Scoop]: https://scoop.sh
|
||||||
|
|
||||||
You can also [build the app manually][BUILD].
|
You can also [build the app manually][BUILD].
|
||||||
|
|
||||||
@ -158,6 +180,21 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
|
|||||||
If `--max-size` is also specified, resizing is applied after cropping.
|
If `--max-size` is also specified, resizing is applied after cropping.
|
||||||
|
|
||||||
|
|
||||||
|
#### Lock video orientation
|
||||||
|
|
||||||
|
|
||||||
|
To lock the orientation of the mirroring:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --lock-video-orientation 0 # natural orientation
|
||||||
|
scrcpy --lock-video-orientation 1 # 90° counterclockwise
|
||||||
|
scrcpy --lock-video-orientation 2 # 180°
|
||||||
|
scrcpy --lock-video-orientation 3 # 90° clockwise
|
||||||
|
```
|
||||||
|
|
||||||
|
This affects recording orientation.
|
||||||
|
|
||||||
|
|
||||||
### Recording
|
### Recording
|
||||||
|
|
||||||
It is possible to record the screen while mirroring:
|
It is possible to record the screen while mirroring:
|
||||||
@ -173,7 +210,6 @@ To disable mirroring while recording:
|
|||||||
scrcpy --no-display --record file.mp4
|
scrcpy --no-display --record file.mp4
|
||||||
scrcpy -Nr file.mkv
|
scrcpy -Nr file.mkv
|
||||||
# interrupt recording with Ctrl+C
|
# interrupt recording with Ctrl+C
|
||||||
# Ctrl+C does not terminate properly on Windows, so disconnect the device
|
|
||||||
```
|
```
|
||||||
|
|
||||||
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
||||||
@ -216,8 +252,25 @@ scrcpy --serial 0123456789abcdef
|
|||||||
scrcpy -s 0123456789abcdef # short version
|
scrcpy -s 0123456789abcdef # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the device is connected over TCP/IP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --serial 192.168.0.1:5555
|
||||||
|
scrcpy -s 192.168.0.1:5555 # short version
|
||||||
|
```
|
||||||
|
|
||||||
You can start several instances of _scrcpy_ for several devices.
|
You can start several instances of _scrcpy_ for several devices.
|
||||||
|
|
||||||
|
#### Autostart on device connection
|
||||||
|
|
||||||
|
You could use [AutoAdb]:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
autoadb scrcpy -s '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||||
|
|
||||||
#### SSH tunnel
|
#### SSH tunnel
|
||||||
|
|
||||||
To connect to a remote device, it is possible to connect a local `adb` client to
|
To connect to a remote device, it is possible to connect a local `adb` client to
|
||||||
@ -236,6 +289,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-forwrad
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Like for wireless connections, it may be useful to reduce quality:
|
Like for wireless connections, it may be useful to reduce quality:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -287,6 +356,33 @@ scrcpy -f # short version
|
|||||||
|
|
||||||
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
|
Fullscreen can then be toggled dynamically with `Ctrl`+`f`.
|
||||||
|
|
||||||
|
#### Rotation
|
||||||
|
|
||||||
|
The window may be rotated:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --rotation 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Possibles values are:
|
||||||
|
- `0`: no rotation
|
||||||
|
- `1`: 90 degrees counterclockwise
|
||||||
|
- `2`: 180 degrees
|
||||||
|
- `3`: 90 degrees clockwise
|
||||||
|
|
||||||
|
The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and
|
||||||
|
`Ctrl`+`→` _(right)_.
|
||||||
|
|
||||||
|
Note that _scrcpy_ manages 3 different rotations:
|
||||||
|
- `Ctrl`+`r` requests the device to switch between portrait and landscape (the
|
||||||
|
current running app may refuse, if it does support the requested
|
||||||
|
orientation).
|
||||||
|
- `--lock-video-orientation` changes the mirroring orientation (the orientation
|
||||||
|
of the video sent from the device to the computer). This affects the
|
||||||
|
recording.
|
||||||
|
- `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This
|
||||||
|
affects only the display, not the recording.
|
||||||
|
|
||||||
|
|
||||||
### Other mirroring options
|
### Other mirroring options
|
||||||
|
|
||||||
@ -300,6 +396,37 @@ scrcpy --no-control
|
|||||||
scrcpy -n
|
scrcpy -n
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Display
|
||||||
|
|
||||||
|
If several displays are available, it is possible to select the display to
|
||||||
|
mirror:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --display 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The list of display ids can be retrieved by:
|
||||||
|
|
||||||
|
```
|
||||||
|
adb shell dumpsys display # search "mDisplayId=" in the output
|
||||||
|
```
|
||||||
|
|
||||||
|
The secondary display may only be controlled if the device runs at least Android
|
||||||
|
10 (otherwise it is mirrored in read-only).
|
||||||
|
|
||||||
|
|
||||||
|
#### Stay awake
|
||||||
|
|
||||||
|
To prevent the device to sleep after some delay when the device is plugged in:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --stay-awake
|
||||||
|
scrcpy -w
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial state is restored when scrcpy is closed.
|
||||||
|
|
||||||
|
|
||||||
#### Turn screen off
|
#### Turn screen off
|
||||||
|
|
||||||
It is possible to turn the device screen off while mirroring on start with a
|
It is possible to turn the device screen off while mirroring on start with a
|
||||||
@ -312,7 +439,15 @@ scrcpy -S
|
|||||||
|
|
||||||
Or by pressing `Ctrl`+`o` at any time.
|
Or by pressing `Ctrl`+`o` at any time.
|
||||||
|
|
||||||
To turn it back on, press `POWER` (or `Ctrl`+`p`).
|
To turn it back on, press `Ctrl`+`Shift`+`o` (or `POWER`, `Ctrl`+`p`).
|
||||||
|
|
||||||
|
It can be useful to also prevent the device to sleep:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --turn-screen-off --stay-awake
|
||||||
|
scrcpy -Sw
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Render expired frames
|
#### Render expired frames
|
||||||
|
|
||||||
@ -333,7 +468,8 @@ device).
|
|||||||
|
|
||||||
Android provides this feature in _Developers options_.
|
Android provides this feature in _Developers options_.
|
||||||
|
|
||||||
_Scrcpy_ provides an option to enable this feature on start and disable on exit:
|
_Scrcpy_ provides an option to enable this feature on start and restore the
|
||||||
|
initial value on exit:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --show-touches
|
scrcpy --show-touches
|
||||||
@ -343,6 +479,17 @@ 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
|
||||||
@ -358,10 +505,14 @@ It is possible to synchronize clipboards between the computer and the device, in
|
|||||||
both directions:
|
both directions:
|
||||||
|
|
||||||
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
||||||
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
|
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard (and
|
||||||
|
pastes if the device runs Android >= 7);
|
||||||
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||||
breaks non-ASCII characters).
|
breaks non-ASCII characters).
|
||||||
|
|
||||||
|
Moreover, any time the Android clipboard changes, it is automatically
|
||||||
|
synchronized to the computer clipboard.
|
||||||
|
|
||||||
#### Text injection preference
|
#### Text injection preference
|
||||||
|
|
||||||
There are two kinds of [events][textevents] generated when typing text:
|
There are two kinds of [events][textevents] generated when typing text:
|
||||||
@ -420,27 +571,30 @@ Also see [issue #14].
|
|||||||
|
|
||||||
## Shortcuts
|
## Shortcuts
|
||||||
|
|
||||||
| Action | Shortcut | Shortcut (macOS)
|
| Action | Shortcut | Shortcut (macOS)
|
||||||
| -------------------------------------- |:----------------------------- |:-----------------------------
|
| ------------------------------------------- |:----------------------------- |:-----------------------------
|
||||||
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
|
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
|
||||||
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
|
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
|
||||||
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
|
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
|
||||||
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
|
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
|
||||||
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
|
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
|
||||||
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
|
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
|
||||||
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
|
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
|
||||||
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
|
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
|
||||||
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
|
||||||
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
|
||||||
| Power on | _Right-click²_ | _Right-click²_
|
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
||||||
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||||
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
| Power on | _Right-click²_ | _Right-click²_
|
||||||
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
| Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o`
|
||||||
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
| Turn device screen on | `Ctrl`+`Shift`+`o` | `Cmd`+`Shift`+`o`
|
||||||
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
||||||
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
|
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
||||||
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||||
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
||||||
|
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
|
||||||
|
| Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
||||||
|
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
||||||
|
|
||||||
_¹Double-click on black borders to remove them._
|
_¹Double-click on black borders to remove them._
|
||||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||||
@ -491,7 +645,7 @@ Read the [developers page].
|
|||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
Copyright (C) 2018-2019 Romain Vimont
|
Copyright (C) 2018-2020 Romain Vimont
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
531
README.pt-br.md
Normal file
531
README.pt-br.md
Normal 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_.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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/
|
@ -11,6 +11,7 @@ src = [
|
|||||||
'src/file_handler.c',
|
'src/file_handler.c',
|
||||||
'src/fps_counter.c',
|
'src/fps_counter.c',
|
||||||
'src/input_manager.c',
|
'src/input_manager.c',
|
||||||
|
'src/opengl.c',
|
||||||
'src/receiver.c',
|
'src/receiver.c',
|
||||||
'src/recorder.c',
|
'src/recorder.c',
|
||||||
'src/scrcpy.c',
|
'src/scrcpy.c',
|
||||||
@ -96,14 +97,21 @@ conf.set_quoted('PREFIX', get_option('prefix'))
|
|||||||
# directory as the executable)
|
# directory as the executable)
|
||||||
conf.set('PORTABLE', get_option('portable'))
|
conf.set('PORTABLE', get_option('portable'))
|
||||||
|
|
||||||
# the default client TCP port for the "adb reverse" tunnel
|
# the default client TCP port range for the "adb reverse" tunnel
|
||||||
# overridden by option --port
|
# overridden by option --port
|
||||||
conf.set('DEFAULT_LOCAL_PORT', '27183')
|
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
||||||
|
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
||||||
|
|
||||||
# the default max video size for both dimensions, in pixels
|
# the default max video size for both dimensions, in pixels
|
||||||
# overridden by option --max-size
|
# overridden by option --max-size
|
||||||
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
|
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
|
||||||
|
|
||||||
|
# the default video orientation
|
||||||
|
# natural device orientation is 0 and each increment adds 90 degrees
|
||||||
|
# counterclockwise
|
||||||
|
# overridden by option --lock-video-orientation
|
||||||
|
conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked
|
||||||
|
|
||||||
# the default video bitrate, in bits/second
|
# the default video bitrate, in bits/second
|
||||||
# overridden by option --bit-rate
|
# overridden by option --bit-rate
|
||||||
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||||
@ -117,6 +125,9 @@ conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
|
|||||||
# run a server debugger and wait for a client to be attached
|
# run a server debugger and wait for a client to be attached
|
||||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||||
|
|
||||||
|
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
|
||||||
|
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
|
||||||
|
|
||||||
configure_file(configuration: conf, output: 'config.h')
|
configure_file(configuration: conf, output: 'config.h')
|
||||||
|
|
||||||
src_dir = include_directories('src')
|
src_dir = include_directories('src')
|
||||||
@ -153,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',
|
||||||
]],
|
]],
|
||||||
|
112
app/scrcpy.1
112
app/scrcpy.1
@ -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,23 @@ 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
|
||||||
|
.BI "\-\-display " id
|
||||||
|
Specify the display id to mirror.
|
||||||
|
|
||||||
|
The list of possible display ids can be listed by "adb shell dumpsys display"
|
||||||
|
(search "mDisplayId=" in the output).
|
||||||
|
|
||||||
|
Default is 0.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-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.
|
||||||
@ -41,6 +68,12 @@ Start in fullscreen.
|
|||||||
.B \-h, \-\-help
|
.B \-h, \-\-help
|
||||||
Print this help.
|
Print this help.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-lock\-video\-orientation " value
|
||||||
|
Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
|
||||||
|
|
||||||
|
Default is -1 (unlocked).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-max\-fps " value
|
.BI "\-\-max\-fps " value
|
||||||
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
|
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
|
||||||
@ -60,10 +93,14 @@ Disable device control (mirror the device in read\-only).
|
|||||||
Do not display device (only when screen recording is enabled).
|
Do not display device (only when screen recording is enabled).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-p, \-\-port " port
|
.B \-\-no\-mipmaps
|
||||||
Set the TCP port the client listens on.
|
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.
|
||||||
|
|
||||||
Default is 27183.
|
.TP
|
||||||
|
.BI "\-p, \-\-port " port[:port]
|
||||||
|
Set the TCP port (range) used by the client to listen.
|
||||||
|
|
||||||
|
Default is 27183:27199.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-prefer\-text
|
.B \-\-prefer\-text
|
||||||
@ -91,10 +128,23 @@ option if set, or by the file extension (.mp4 or .mkv).
|
|||||||
.BI "\-\-record\-format " format
|
.BI "\-\-record\-format " format
|
||||||
Force recording format (either mp4 or mkv).
|
Force recording format (either mp4 or mkv).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-render\-driver " name
|
||||||
|
Request SDL to use the given render driver (this is just a hint).
|
||||||
|
|
||||||
|
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
|
||||||
|
|
||||||
|
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
||||||
|
.UE
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-render\-expired\-frames
|
.B \-\-render\-expired\-frames
|
||||||
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
|
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-rotation " value
|
||||||
|
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.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.
|
||||||
@ -105,7 +155,7 @@ Turn the device screen off immediately.
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-t, \-\-show\-touches
|
.B \-t, \-\-show\-touches
|
||||||
Enable "show touches" on start, disable on quit.
|
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).
|
||||||
|
|
||||||
@ -113,6 +163,16 @@ 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
|
||||||
|
.B \-w, \-\-stay-awake
|
||||||
|
Keep the device on while scrcpy is running, when the device is plugged in.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-window\-borderless
|
.B \-\-window\-borderless
|
||||||
Disable window decorations (display borderless window).
|
Disable window decorations (display borderless window).
|
||||||
@ -125,13 +185,13 @@ Set a custom window title.
|
|||||||
.BI "\-\-window\-x " value
|
.BI "\-\-window\-x " value
|
||||||
Set the initial window horizontal position.
|
Set the initial window horizontal position.
|
||||||
|
|
||||||
Default is -1 (automatic).\n
|
Default is "auto".\n
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-window\-y " value
|
.BI "\-\-window\-y " value
|
||||||
Set the initial window vertical position.
|
Set the initial window vertical position.
|
||||||
|
|
||||||
Default is -1 (automatic).\n
|
Default is "auto".\n
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-window\-width " value
|
.BI "\-\-window\-width " value
|
||||||
@ -149,15 +209,23 @@ Default is 0 (automatic).\n
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+f
|
.B Ctrl+f
|
||||||
switch fullscreen mode
|
Switch fullscreen mode
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B Ctrl+Left
|
||||||
|
Rotate display left
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B Ctrl+Right
|
||||||
|
Rotate display right
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+g
|
.B Ctrl+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 Ctrl+x, 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 Ctrl+h, Home, Middle\-click
|
||||||
@ -189,43 +257,47 @@ Click on POWER (turn screen on/off)
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Right\-click (when screen is off)
|
.B Right\-click (when screen is off)
|
||||||
turn screen on
|
Turn screen on
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+o
|
.B Ctrl+o
|
||||||
turn device screen off (keep mirroring)
|
Turn device screen off (keep mirroring)
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B Ctrl+Shift+o
|
||||||
|
Turn device screen on
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+r
|
.B Ctrl+r
|
||||||
rotate device screen
|
Rotate device screen
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+n
|
.B Ctrl+n
|
||||||
expand notification panel
|
Expand notification panel
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+Shift+n
|
.B Ctrl+Shift+n
|
||||||
collapse notification panel
|
Collapse notification panel
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+c
|
.B Ctrl+c
|
||||||
copy device clipboard to computer
|
Copy device clipboard to computer
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+v
|
.B Ctrl+v
|
||||||
paste computer clipboard to device
|
Paste computer clipboard to device
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+Shift+v
|
.B Ctrl+Shift+v
|
||||||
copy computer clipboard to device
|
Copy computer clipboard to device (and paste if the device runs Android >= 7)
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+i
|
.B Ctrl+i
|
||||||
enable/disable FPS counter (print frames/second in logs)
|
Enable/disable FPS counter (print frames/second in logs)
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Drag & drop APK file
|
.B Drag & drop APK file
|
||||||
install APK from computer
|
Install APK from computer
|
||||||
|
|
||||||
|
|
||||||
.SH Environment variables
|
.SH Environment variables
|
||||||
@ -261,7 +333,7 @@ Copyright \(co 2018 Genymobile
|
|||||||
Genymobile
|
Genymobile
|
||||||
.UE
|
.UE
|
||||||
|
|
||||||
Copyright \(co 2018\-2019
|
Copyright \(co 2018\-2020
|
||||||
.MT rom@rom1v.com
|
.MT rom@rom1v.com
|
||||||
Romain Vimont
|
Romain Vimont
|
||||||
.ME
|
.ME
|
||||||
|
403
app/src/cli.c
403
app/src/cli.c
@ -1,5 +1,6 @@
|
|||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
#include <getopt.h>
|
#include <getopt.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@ -29,18 +30,50 @@ 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"
|
||||||
|
" Specify the display id to mirror.\n"
|
||||||
|
"\n"
|
||||||
|
" The list of possible display ids can be listed by:\n"
|
||||||
|
" adb shell dumpsys display\n"
|
||||||
|
" (search \"mDisplayId=\" in the output)\n"
|
||||||
|
"\n"
|
||||||
|
" Default is 0.\n"
|
||||||
|
"\n"
|
||||||
|
" --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"
|
||||||
" -h, --help\n"
|
" -h, --help\n"
|
||||||
" Print this help.\n"
|
" Print this help.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --lock-video-orientation value\n"
|
||||||
|
" Lock video orientation to value.\n"
|
||||||
|
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n"
|
||||||
|
" Natural device orientation is 0, and each increment adds a\n"
|
||||||
|
" 90 degrees rotation counterclockwise.\n"
|
||||||
|
" Default is %d%s.\n"
|
||||||
|
"\n"
|
||||||
" --max-fps value\n"
|
" --max-fps value\n"
|
||||||
" Limit the frame rate of screen capture (officially supported\n"
|
" Limit the frame rate of screen capture (officially supported\n"
|
||||||
" since Android 10, but may work on earlier versions).\n"
|
" since Android 10, but may work on earlier versions).\n"
|
||||||
@ -58,9 +91,14 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" Do not display device (only when screen recording is\n"
|
" Do not display device (only when screen recording is\n"
|
||||||
" enabled).\n"
|
" enabled).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" -p, --port port\n"
|
" --no-mipmaps\n"
|
||||||
" Set the TCP port the client listens on.\n"
|
" If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n"
|
||||||
" Default is %d.\n"
|
" mipmaps are automatically generated to improve downscaling\n"
|
||||||
|
" quality. This option disables the generation of mipmaps.\n"
|
||||||
|
"\n"
|
||||||
|
" -p, --port port[:port]\n"
|
||||||
|
" Set the TCP port (range) used by the client to listen.\n"
|
||||||
|
" Default is %d:%d.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --prefer-text\n"
|
" --prefer-text\n"
|
||||||
" Inject alpha characters and space as text events instead of\n"
|
" Inject alpha characters and space as text events instead of\n"
|
||||||
@ -82,12 +120,24 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" --record-format format\n"
|
" --record-format format\n"
|
||||||
" Force recording format (either mp4 or mkv).\n"
|
" Force recording format (either mp4 or mkv).\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --render-driver name\n"
|
||||||
|
" Request SDL to use the given render driver (this is just a\n"
|
||||||
|
" hint).\n"
|
||||||
|
" Supported names are currently \"direct3d\", \"opengl\",\n"
|
||||||
|
" \"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
|
||||||
|
" <https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>\n"
|
||||||
|
"\n"
|
||||||
" --render-expired-frames\n"
|
" --render-expired-frames\n"
|
||||||
" By default, to minimize latency, scrcpy always renders the\n"
|
" By default, to minimize latency, scrcpy always renders the\n"
|
||||||
" last available decoded frame, and drops any previous ones.\n"
|
" last available decoded frame, and drops any previous ones.\n"
|
||||||
" This flag forces to render all frames, at a cost of a\n"
|
" This flag forces to render all frames, at a cost of a\n"
|
||||||
" possible increased latency.\n"
|
" possible increased latency.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --rotation value\n"
|
||||||
|
" Set the initial display rotation.\n"
|
||||||
|
" Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n"
|
||||||
|
" degrees rotation counterclockwise.\n"
|
||||||
|
"\n"
|
||||||
" -s, --serial serial\n"
|
" -s, --serial serial\n"
|
||||||
" The device serial number. Mandatory only if several devices\n"
|
" The device serial number. Mandatory only if several devices\n"
|
||||||
" are connected to adb.\n"
|
" are connected to adb.\n"
|
||||||
@ -96,12 +146,25 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" Turn the device screen off immediately.\n"
|
" Turn the device screen off immediately.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" -t, --show-touches\n"
|
" -t, --show-touches\n"
|
||||||
" Enable \"show touches\" on start, disable on quit.\n"
|
" Enable \"show touches\" on start, restore the initial value\n"
|
||||||
|
" on exit.\n"
|
||||||
" It only shows physical touches (not clicks from scrcpy).\n"
|
" It only shows physical touches (not clicks from scrcpy).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" -v, --version\n"
|
" -v, --version\n"
|
||||||
" Print the version of scrcpy.\n"
|
" Print the version of scrcpy.\n"
|
||||||
"\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"
|
||||||
|
" -w, --stay-awake\n"
|
||||||
|
" Keep the device on while scrcpy is running, when the device\n"
|
||||||
|
" is plugged in.\n"
|
||||||
|
"\n"
|
||||||
" --window-borderless\n"
|
" --window-borderless\n"
|
||||||
" Disable window decorations (display borderless window).\n"
|
" Disable window decorations (display borderless window).\n"
|
||||||
"\n"
|
"\n"
|
||||||
@ -110,11 +173,11 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" --window-x value\n"
|
" --window-x value\n"
|
||||||
" Set the initial window horizontal position.\n"
|
" Set the initial window horizontal position.\n"
|
||||||
" Default is -1 (automatic).\n"
|
" Default is \"auto\".\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --window-y value\n"
|
" --window-y value\n"
|
||||||
" Set the initial window vertical position.\n"
|
" Set the initial window vertical position.\n"
|
||||||
" Default is -1 (automatic).\n"
|
" Default is \"auto\".\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --window-width value\n"
|
" --window-width value\n"
|
||||||
" Set the initial window width.\n"
|
" Set the initial window width.\n"
|
||||||
@ -127,73 +190,84 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
"Shortcuts:\n"
|
"Shortcuts:\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+f\n"
|
" " CTRL_OR_CMD "+f\n"
|
||||||
" switch fullscreen mode\n"
|
" Switch fullscreen mode\n"
|
||||||
|
"\n"
|
||||||
|
" " CTRL_OR_CMD "+Left\n"
|
||||||
|
" Rotate display left\n"
|
||||||
|
"\n"
|
||||||
|
" " CTRL_OR_CMD "+Right\n"
|
||||||
|
" Rotate display right\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+g\n"
|
" " CTRL_OR_CMD "+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"
|
" " CTRL_OR_CMD "+x\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"
|
" Ctrl+h\n"
|
||||||
" Middle-click\n"
|
" Middle-click\n"
|
||||||
" click on HOME\n"
|
" Click on HOME\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+b\n"
|
" " CTRL_OR_CMD "+b\n"
|
||||||
" " CTRL_OR_CMD "+Backspace\n"
|
" " CTRL_OR_CMD "+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"
|
" " CTRL_OR_CMD "+s\n"
|
||||||
" click on APP_SWITCH\n"
|
" Click on APP_SWITCH\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Ctrl+m\n"
|
" Ctrl+m\n"
|
||||||
" click on MENU\n"
|
" Click on MENU\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+Up\n"
|
" " CTRL_OR_CMD "+Up\n"
|
||||||
" click on VOLUME_UP\n"
|
" Click on VOLUME_UP\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+Down\n"
|
" " CTRL_OR_CMD "+Down\n"
|
||||||
" click on VOLUME_DOWN\n"
|
" Click on VOLUME_DOWN\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+p\n"
|
" " CTRL_OR_CMD "+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"
|
" " CTRL_OR_CMD "+o\n"
|
||||||
" turn device screen off (keep mirroring)\n"
|
" Turn device screen off (keep mirroring)\n"
|
||||||
|
"\n"
|
||||||
|
" " CTRL_OR_CMD "+Shift+o\n"
|
||||||
|
" Turn device screen on\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+r\n"
|
" " CTRL_OR_CMD "+r\n"
|
||||||
" rotate device screen\n"
|
" Rotate device screen\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+n\n"
|
" " CTRL_OR_CMD "+n\n"
|
||||||
" expand notification panel\n"
|
" Expand notification panel\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+Shift+n\n"
|
" " CTRL_OR_CMD "+Shift+n\n"
|
||||||
" collapse notification panel\n"
|
" Collapse notification panel\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+c\n"
|
" " CTRL_OR_CMD "+c\n"
|
||||||
" copy device clipboard to computer\n"
|
" Copy device clipboard to computer\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+v\n"
|
" " CTRL_OR_CMD "+v\n"
|
||||||
" paste computer clipboard to device\n"
|
" Paste computer clipboard to device\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+Shift+v\n"
|
" " CTRL_OR_CMD "+Shift+v\n"
|
||||||
" copy computer clipboard to device\n"
|
" Copy computer clipboard to device (and paste if the device\n"
|
||||||
|
" runs Android >= 7)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+i\n"
|
" " CTRL_OR_CMD "+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"
|
||||||
" install APK from computer\n"
|
" Install APK from computer\n"
|
||||||
"\n",
|
"\n",
|
||||||
arg0,
|
arg0,
|
||||||
DEFAULT_BIT_RATE,
|
DEFAULT_BIT_RATE,
|
||||||
|
DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)",
|
||||||
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
|
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
|
||||||
DEFAULT_LOCAL_PORT);
|
DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@ -221,6 +295,27 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
parse_integers_arg(const char *s, size_t max_items, long *out, long min,
|
||||||
|
long max, const char *name) {
|
||||||
|
size_t count = parse_integers(s, ':', max_items, out);
|
||||||
|
if (!count) {
|
||||||
|
LOGE("Could not parse %s: %s", name, s);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
long value = out[i];
|
||||||
|
if (value < min || value > max) {
|
||||||
|
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
|
||||||
|
name, value, min, max);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_bit_rate(const char *s, uint32_t *bit_rate) {
|
parse_bit_rate(const char *s, uint32_t *bit_rate) {
|
||||||
long value;
|
long value;
|
||||||
@ -260,9 +355,42 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_window_position(const char *s, int16_t *position) {
|
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
|
||||||
long value;
|
long value;
|
||||||
bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF,
|
bool ok = parse_integer_arg(s, &value, false, -1, 3,
|
||||||
|
"lock video orientation");
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*lock_video_orientation = (int8_t) value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_rotation(const char *s, uint8_t *rotation) {
|
||||||
|
long value;
|
||||||
|
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*rotation = (uint8_t) value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_window_position(const char *s, int16_t *position) {
|
||||||
|
// special value for "auto"
|
||||||
|
static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value");
|
||||||
|
|
||||||
|
if (!strcmp(s, "auto")) {
|
||||||
|
*position = WINDOW_POSITION_UNDEFINED;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
long value;
|
||||||
|
bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF,
|
||||||
"window position");
|
"window position");
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
@ -286,17 +414,71 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_port(const char *s, uint16_t *port) {
|
parse_port_range(const char *s, struct port_range *port_range) {
|
||||||
|
long values[2];
|
||||||
|
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
|
||||||
|
if (!count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t v0 = (uint16_t) values[0];
|
||||||
|
if (count == 1) {
|
||||||
|
port_range->first = v0;
|
||||||
|
port_range->last = v0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(count == 2);
|
||||||
|
uint16_t v1 = (uint16_t) values[1];
|
||||||
|
if (v0 < v1) {
|
||||||
|
port_range->first = v0;
|
||||||
|
port_range->last = v1;
|
||||||
|
} else {
|
||||||
|
port_range->first = v1;
|
||||||
|
port_range->last = v0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_display_id(const char *s, uint16_t *display_id) {
|
||||||
long value;
|
long value;
|
||||||
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port");
|
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id");
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*port = (uint16_t) value;
|
*display_id = (uint16_t) value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_record_format(const char *optarg, enum recorder_format *format) {
|
parse_record_format(const char *optarg, enum recorder_format *format) {
|
||||||
if (!strcmp(optarg, "mp4")) {
|
if (!strcmp(optarg, "mp4")) {
|
||||||
@ -327,51 +509,72 @@ guess_record_format(const char *filename) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
||||||
#define OPT_WINDOW_TITLE 1001
|
#define OPT_WINDOW_TITLE 1001
|
||||||
#define OPT_PUSH_TARGET 1002
|
#define OPT_PUSH_TARGET 1002
|
||||||
#define OPT_ALWAYS_ON_TOP 1003
|
#define OPT_ALWAYS_ON_TOP 1003
|
||||||
#define OPT_CROP 1004
|
#define OPT_CROP 1004
|
||||||
#define OPT_RECORD_FORMAT 1005
|
#define OPT_RECORD_FORMAT 1005
|
||||||
#define OPT_PREFER_TEXT 1006
|
#define OPT_PREFER_TEXT 1006
|
||||||
#define OPT_WINDOW_X 1007
|
#define OPT_WINDOW_X 1007
|
||||||
#define OPT_WINDOW_Y 1008
|
#define OPT_WINDOW_Y 1008
|
||||||
#define OPT_WINDOW_WIDTH 1009
|
#define OPT_WINDOW_WIDTH 1009
|
||||||
#define OPT_WINDOW_HEIGHT 1010
|
#define OPT_WINDOW_HEIGHT 1010
|
||||||
#define OPT_WINDOW_BORDERLESS 1011
|
#define OPT_WINDOW_BORDERLESS 1011
|
||||||
#define OPT_MAX_FPS 1012
|
#define OPT_MAX_FPS 1012
|
||||||
|
#define OPT_LOCK_VIDEO_ORIENTATION 1013
|
||||||
|
#define OPT_DISPLAY_ID 1014
|
||||||
|
#define OPT_ROTATION 1015
|
||||||
|
#define OPT_RENDER_DRIVER 1016
|
||||||
|
#define OPT_NO_MIPMAPS 1017
|
||||||
|
#define OPT_CODEC_OPTIONS 1018
|
||||||
|
#define OPT_FORCE_ADB_FORWARD 1019
|
||||||
|
#define OPT_DISABLE_SCREENSAVER 1020
|
||||||
|
|
||||||
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'},
|
||||||
{"crop", required_argument, NULL, OPT_CROP},
|
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
|
||||||
{"fullscreen", no_argument, NULL, 'f'},
|
{"crop", required_argument, NULL, OPT_CROP},
|
||||||
{"help", no_argument, NULL, 'h'},
|
{"disable-screensaver", no_argument, NULL,
|
||||||
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
OPT_DISABLE_SCREENSAVER},
|
||||||
{"max-size", required_argument, NULL, 'm'},
|
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
||||||
{"no-control", no_argument, NULL, 'n'},
|
{"force-adb-forward", no_argument, NULL,
|
||||||
{"no-display", no_argument, NULL, 'N'},
|
OPT_FORCE_ADB_FORWARD},
|
||||||
{"port", required_argument, NULL, 'p'},
|
{"fullscreen", no_argument, NULL, 'f'},
|
||||||
{"push-target", required_argument, NULL, OPT_PUSH_TARGET},
|
{"help", no_argument, NULL, 'h'},
|
||||||
{"record", required_argument, NULL, 'r'},
|
{"lock-video-orientation", required_argument, NULL,
|
||||||
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
|
OPT_LOCK_VIDEO_ORIENTATION},
|
||||||
{"render-expired-frames", no_argument, NULL,
|
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
||||||
OPT_RENDER_EXPIRED_FRAMES},
|
{"max-size", required_argument, NULL, 'm'},
|
||||||
{"serial", required_argument, NULL, 's'},
|
{"no-control", no_argument, NULL, 'n'},
|
||||||
{"show-touches", no_argument, NULL, 't'},
|
{"no-display", no_argument, NULL, 'N'},
|
||||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
|
||||||
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
|
{"port", required_argument, NULL, 'p'},
|
||||||
{"version", no_argument, NULL, 'v'},
|
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
|
||||||
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
{"push-target", required_argument, NULL, OPT_PUSH_TARGET},
|
||||||
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
{"record", required_argument, NULL, 'r'},
|
||||||
{"window-y", required_argument, NULL, OPT_WINDOW_Y},
|
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
|
||||||
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
|
{"render-driver", required_argument, NULL, OPT_RENDER_DRIVER},
|
||||||
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
|
{"render-expired-frames", no_argument, NULL,
|
||||||
{"window-borderless", no_argument, NULL,
|
OPT_RENDER_EXPIRED_FRAMES},
|
||||||
OPT_WINDOW_BORDERLESS},
|
{"rotation", required_argument, NULL, OPT_ROTATION},
|
||||||
{NULL, 0, NULL, 0 },
|
{"serial", required_argument, NULL, 's'},
|
||||||
|
{"show-touches", no_argument, NULL, 't'},
|
||||||
|
{"stay-awake", no_argument, NULL, 'w'},
|
||||||
|
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||||
|
{"verbosity", required_argument, NULL, 'V'},
|
||||||
|
{"version", no_argument, NULL, 'v'},
|
||||||
|
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
||||||
|
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
||||||
|
{"window-y", required_argument, NULL, OPT_WINDOW_Y},
|
||||||
|
{"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
|
||||||
|
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
|
||||||
|
{"window-borderless", no_argument, NULL,
|
||||||
|
OPT_WINDOW_BORDERLESS},
|
||||||
|
{NULL, 0, NULL, 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
struct scrcpy_options *opts = &args->opts;
|
struct scrcpy_options *opts = &args->opts;
|
||||||
@ -379,8 +582,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:StTv", 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)) {
|
||||||
@ -393,6 +596,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case OPT_CROP:
|
case OPT_CROP:
|
||||||
opts->crop = optarg;
|
opts->crop = optarg;
|
||||||
break;
|
break;
|
||||||
|
case OPT_DISPLAY_ID:
|
||||||
|
if (!parse_display_id(optarg, &opts->display_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'f':
|
case 'f':
|
||||||
opts->fullscreen = true;
|
opts->fullscreen = true;
|
||||||
break;
|
break;
|
||||||
@ -417,6 +625,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case OPT_LOCK_VIDEO_ORIENTATION:
|
||||||
|
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
opts->control = false;
|
opts->control = false;
|
||||||
break;
|
break;
|
||||||
@ -424,7 +637,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
opts->display = false;
|
opts->display = false;
|
||||||
break;
|
break;
|
||||||
case 'p':
|
case 'p':
|
||||||
if (!parse_port(optarg, &opts->port)) {
|
if (!parse_port_range(optarg, &opts->port_range)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -449,6 +662,14 @@ 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':
|
||||||
|
opts->stay_awake = true;
|
||||||
|
break;
|
||||||
case OPT_RENDER_EXPIRED_FRAMES:
|
case OPT_RENDER_EXPIRED_FRAMES:
|
||||||
opts->render_expired_frames = true;
|
opts->render_expired_frames = true;
|
||||||
break;
|
break;
|
||||||
@ -484,6 +705,26 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case OPT_PREFER_TEXT:
|
case OPT_PREFER_TEXT:
|
||||||
opts->prefer_text = true;
|
opts->prefer_text = true;
|
||||||
break;
|
break;
|
||||||
|
case OPT_ROTATION:
|
||||||
|
if (!parse_rotation(optarg, &opts->rotation)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OPT_RENDER_DRIVER:
|
||||||
|
opts->render_driver = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_NO_MIPMAPS:
|
||||||
|
opts->mipmaps = false;
|
||||||
|
break;
|
||||||
|
case OPT_CODEC_OPTIONS:
|
||||||
|
opts->codec_options = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_FORCE_ADB_FORWARD:
|
||||||
|
opts->force_adb_forward = true;
|
||||||
|
break;
|
||||||
|
case OPT_DISABLE_SCREENSAVER:
|
||||||
|
opts->disable_screensaver = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
@ -495,11 +736,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts->display && opts->fullscreen) {
|
|
||||||
LOGE("-f/--fullscreen-window is incompatible with -N/--no-display");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = optind;
|
int index = optind;
|
||||||
if (index < argc) {
|
if (index < argc) {
|
||||||
LOGE("Unexpected additional argument: %s", argv[index]);
|
LOGE("Unexpected additional argument: %s", argv[index]);
|
||||||
@ -525,5 +761,10 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts->control && opts->stay_awake) {
|
||||||
|
LOGE("Could not request to stay awake if control is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,32 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
|
|||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
show_adb_installation_msg() {
|
||||||
|
#ifndef __WINDOWS__
|
||||||
|
static const struct {
|
||||||
|
const char *binary;
|
||||||
|
const char *command;
|
||||||
|
} pkg_managers[] = {
|
||||||
|
{"apt", "apt install adb"},
|
||||||
|
{"apt-get", "apt-get install adb"},
|
||||||
|
{"brew", "brew cask install android-platform-tools"},
|
||||||
|
{"dnf", "dnf install android-tools"},
|
||||||
|
{"emerge", "emerge dev-util/android-tools"},
|
||||||
|
{"pacman", "pacman -S android-tools"},
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
|
||||||
|
if (cmd_search(pkg_managers[i].binary)) {
|
||||||
|
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
LOGI("You may download and install 'adb' from "
|
||||||
|
"https://developer.android.com/studio/releases/platform-tools");
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
show_adb_err_msg(enum process_result err, const char *const argv[]) {
|
show_adb_err_msg(enum process_result err, const char *const argv[]) {
|
||||||
char buf[512];
|
char buf[512];
|
||||||
@ -68,6 +94,7 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
|
|||||||
LOGE("Command not found: %s", buf);
|
LOGE("Command not found: %s", buf);
|
||||||
LOGE("(make 'adb' accessible from your PATH or define its full"
|
LOGE("(make 'adb' accessible from your PATH or define its full"
|
||||||
"path in the ADB environment variable)");
|
"path in the ADB environment variable)");
|
||||||
|
show_adb_installation_msg();
|
||||||
break;
|
break;
|
||||||
case PROCESS_SUCCESS:
|
case PROCESS_SUCCESS:
|
||||||
// do nothing
|
// do nothing
|
||||||
|
@ -43,6 +43,11 @@ enum process_result {
|
|||||||
PROCESS_ERROR_MISSING_BINARY,
|
PROCESS_ERROR_MISSING_BINARY,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef __WINDOWS__
|
||||||
|
bool
|
||||||
|
cmd_search(const char *file);
|
||||||
|
#endif
|
||||||
|
|
||||||
enum process_result
|
enum process_result
|
||||||
cmd_execute(const char *const argv[], process_t *process);
|
cmd_execute(const char *const argv[], process_t *process);
|
||||||
|
|
||||||
|
@ -27,4 +27,9 @@ struct position {
|
|||||||
struct point point;
|
struct point point;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct port_range {
|
||||||
|
uint16_t first;
|
||||||
|
uint16_t last;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -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,11 +42,13 @@ 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 = write_string(msg->inject_text.text,
|
size_t len =
|
||||||
CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]);
|
write_string(msg->inject_text.text,
|
||||||
|
CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]);
|
||||||
return 1 + len;
|
return 1 + len;
|
||||||
}
|
}
|
||||||
case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
|
case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
|
||||||
@ -66,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;
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
#include "android/keycodes.h"
|
#include "android/keycodes.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
#define CONTROL_MSG_TEXT_MAX_LENGTH 300
|
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
|
||||||
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
|
|
||||||
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \
|
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
|
||||||
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
|
// type: 1 byte; paste flag: 1 byte; length: 4 bytes
|
||||||
|
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
|
||||||
|
|
||||||
#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);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -94,6 +94,23 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
|||||||
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
|
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(mod & (KMOD_NUM | KMOD_SHIFT))) {
|
||||||
|
// Handle Numpad events when Num Lock is disabled
|
||||||
|
// If SHIFT is pressed, a text event will be sent instead
|
||||||
|
switch(from) {
|
||||||
|
MAP(SDLK_KP_0, AKEYCODE_INSERT);
|
||||||
|
MAP(SDLK_KP_1, AKEYCODE_MOVE_END);
|
||||||
|
MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN);
|
||||||
|
MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN);
|
||||||
|
MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT);
|
||||||
|
MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT);
|
||||||
|
MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME);
|
||||||
|
MAP(SDLK_KP_8, AKEYCODE_DPAD_UP);
|
||||||
|
MAP(SDLK_KP_9, AKEYCODE_PAGE_UP);
|
||||||
|
MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (prefer_text) {
|
if (prefer_text) {
|
||||||
// do not forward alpha and space key events
|
// do not forward alpha and space key events
|
||||||
return false;
|
return false;
|
||||||
|
@ -23,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
counter->thread = NULL;
|
counter->thread = NULL;
|
||||||
SDL_AtomicSet(&counter->started, 0);
|
atomic_init(&counter->started, 0);
|
||||||
// no need to initialize the other fields, they are unused until started
|
// no need to initialize the other fields, they are unused until started
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -35,6 +35,16 @@ fps_counter_destroy(struct fps_counter *counter) {
|
|||||||
SDL_DestroyMutex(counter->mutex);
|
SDL_DestroyMutex(counter->mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline bool
|
||||||
|
is_started(struct fps_counter *counter) {
|
||||||
|
return atomic_load_explicit(&counter->started, memory_order_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
set_started(struct fps_counter *counter, bool started) {
|
||||||
|
atomic_store_explicit(&counter->started, started, memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
// must be called with mutex locked
|
// must be called with mutex locked
|
||||||
static void
|
static void
|
||||||
display_fps(struct fps_counter *counter) {
|
display_fps(struct fps_counter *counter) {
|
||||||
@ -70,10 +80,10 @@ run_fps_counter(void *data) {
|
|||||||
|
|
||||||
mutex_lock(counter->mutex);
|
mutex_lock(counter->mutex);
|
||||||
while (!counter->interrupted) {
|
while (!counter->interrupted) {
|
||||||
while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) {
|
while (!counter->interrupted && !is_started(counter)) {
|
||||||
cond_wait(counter->state_cond, counter->mutex);
|
cond_wait(counter->state_cond, counter->mutex);
|
||||||
}
|
}
|
||||||
while (!counter->interrupted && SDL_AtomicGet(&counter->started)) {
|
while (!counter->interrupted && is_started(counter)) {
|
||||||
uint32_t now = SDL_GetTicks();
|
uint32_t now = SDL_GetTicks();
|
||||||
check_interval_expired(counter, now);
|
check_interval_expired(counter, now);
|
||||||
|
|
||||||
@ -96,7 +106,7 @@ fps_counter_start(struct fps_counter *counter) {
|
|||||||
counter->nr_skipped = 0;
|
counter->nr_skipped = 0;
|
||||||
mutex_unlock(counter->mutex);
|
mutex_unlock(counter->mutex);
|
||||||
|
|
||||||
SDL_AtomicSet(&counter->started, 1);
|
set_started(counter, true);
|
||||||
cond_signal(counter->state_cond);
|
cond_signal(counter->state_cond);
|
||||||
|
|
||||||
// counter->thread is always accessed from the same thread, no need to lock
|
// counter->thread is always accessed from the same thread, no need to lock
|
||||||
@ -114,13 +124,13 @@ fps_counter_start(struct fps_counter *counter) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
fps_counter_stop(struct fps_counter *counter) {
|
fps_counter_stop(struct fps_counter *counter) {
|
||||||
SDL_AtomicSet(&counter->started, 0);
|
set_started(counter, false);
|
||||||
cond_signal(counter->state_cond);
|
cond_signal(counter->state_cond);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
fps_counter_is_started(struct fps_counter *counter) {
|
fps_counter_is_started(struct fps_counter *counter) {
|
||||||
return SDL_AtomicGet(&counter->started);
|
return is_started(counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -145,7 +155,7 @@ fps_counter_join(struct fps_counter *counter) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
fps_counter_add_rendered_frame(struct fps_counter *counter) {
|
fps_counter_add_rendered_frame(struct fps_counter *counter) {
|
||||||
if (!SDL_AtomicGet(&counter->started)) {
|
if (!is_started(counter)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +168,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
fps_counter_add_skipped_frame(struct fps_counter *counter) {
|
fps_counter_add_skipped_frame(struct fps_counter *counter) {
|
||||||
if (!SDL_AtomicGet(&counter->started)) {
|
if (!is_started(counter)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
#ifndef FPSCOUNTER_H
|
#ifndef FPSCOUNTER_H
|
||||||
#define FPSCOUNTER_H
|
#define FPSCOUNTER_H
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <SDL2/SDL_atomic.h>
|
|
||||||
#include <SDL2/SDL_mutex.h>
|
#include <SDL2/SDL_mutex.h>
|
||||||
#include <SDL2/SDL_thread.h>
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ struct fps_counter {
|
|||||||
|
|
||||||
// atomic so that we can check without locking the mutex
|
// atomic so that we can check without locking the mutex
|
||||||
// if the FPS counter is disabled, we don't want to lock unnecessarily
|
// if the FPS counter is disabled, we don't want to lock unnecessarily
|
||||||
SDL_atomic_t started;
|
atomic_bool started;
|
||||||
|
|
||||||
// the following fields are protected by the mutex
|
// the following fields are protected by the mutex
|
||||||
bool interrupted;
|
bool interrupted;
|
||||||
|
@ -7,33 +7,6 @@
|
|||||||
#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;
|
||||||
|
|
||||||
@ -139,7 +112,7 @@ request_device_clipboard(struct controller *controller) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
set_device_clipboard(struct controller *controller) {
|
set_device_clipboard(struct controller *controller, bool paste) {
|
||||||
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 +127,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);
|
||||||
@ -221,6 +195,18 @@ rotate_device(struct controller *controller) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
rotate_client_left(struct screen *screen) {
|
||||||
|
unsigned new_rotation = (screen->rotation + 1) % 4;
|
||||||
|
screen_set_rotation(screen, new_rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
rotate_client_right(struct screen *screen) {
|
||||||
|
unsigned new_rotation = (screen->rotation + 3) % 4;
|
||||||
|
screen_set_rotation(screen, new_rotation);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
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) {
|
||||||
@ -248,7 +234,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)) {
|
||||||
@ -261,6 +247,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;
|
||||||
@ -335,8 +322,11 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_o:
|
case SDLK_o:
|
||||||
if (control && cmd && !shift && down) {
|
if (control && cmd && !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:
|
||||||
@ -351,6 +341,16 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
action_volume_up(controller, action);
|
action_volume_up(controller, action);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case SDLK_LEFT:
|
||||||
|
if (cmd && !shift && !repeat && down) {
|
||||||
|
rotate_client_left(im->screen);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case SDLK_RIGHT:
|
||||||
|
if (cmd && !shift && !repeat && down) {
|
||||||
|
rotate_client_right(im->screen);
|
||||||
|
}
|
||||||
|
return;
|
||||||
case SDLK_c:
|
case SDLK_c:
|
||||||
if (control && cmd && !shift && !repeat && down) {
|
if (control && cmd && !shift && !repeat && down) {
|
||||||
request_device_clipboard(controller);
|
request_device_clipboard(controller);
|
||||||
@ -359,8 +359,8 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
case SDLK_v:
|
case SDLK_v:
|
||||||
if (control && cmd && !repeat && down) {
|
if (control && cmd && !repeat && down) {
|
||||||
if (shift) {
|
if (shift) {
|
||||||
// store the text in the device clipboard
|
// store the text in the device clipboard and paste
|
||||||
set_device_clipboard(controller);
|
set_device_clipboard(controller, true);
|
||||||
} else {
|
} else {
|
||||||
// inject the text as input events
|
// inject the text as input events
|
||||||
clipboard_paste(controller);
|
clipboard_paste(controller);
|
||||||
@ -412,8 +412,14 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event->repeat) {
|
||||||
|
++im->repeat;
|
||||||
|
} else {
|
||||||
|
im->repeat = 0;
|
||||||
|
}
|
||||||
|
|
||||||
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'");
|
||||||
}
|
}
|
||||||
@ -427,8 +433,8 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
|
|||||||
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
|
to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
|
||||||
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.x = from->x;
|
to->inject_touch_event.position.point =
|
||||||
to->inject_touch_event.position.point.y = from->y;
|
screen_convert_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);
|
||||||
|
|
||||||
@ -463,13 +469,19 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct size frame_size = screen->frame_size;
|
|
||||||
|
|
||||||
to->inject_touch_event.pointer_id = from->fingerId;
|
to->inject_touch_event.pointer_id = from->fingerId;
|
||||||
to->inject_touch_event.position.screen_size = frame_size;
|
to->inject_touch_event.position.screen_size = screen->frame_size;
|
||||||
|
|
||||||
|
int ww;
|
||||||
|
int wh;
|
||||||
|
SDL_GL_GetDrawableSize(screen->window, &ww, &wh);
|
||||||
|
|
||||||
// SDL touch event coordinates are normalized in the range [0; 1]
|
// SDL touch event coordinates are normalized in the range [0; 1]
|
||||||
to->inject_touch_event.position.point.x = from->x * frame_size.width;
|
int32_t x = from->x * ww;
|
||||||
to->inject_touch_event.position.point.y = from->y * frame_size.height;
|
int32_t y = from->y * wh;
|
||||||
|
to->inject_touch_event.position.point =
|
||||||
|
screen_convert_to_frame_coords(screen, x, y);
|
||||||
|
|
||||||
to->inject_touch_event.pressure = from->pressure;
|
to->inject_touch_event.pressure = from->pressure;
|
||||||
to->inject_touch_event.buttons = 0;
|
to->inject_touch_event.buttons = 0;
|
||||||
return true;
|
return true;
|
||||||
@ -486,13 +498,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->frame_size.width ||
|
|
||||||
y < 0 || y >= im->screen->frame_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) {
|
||||||
@ -504,8 +509,8 @@ 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.x = from->x;
|
to->inject_touch_event.position.point =
|
||||||
to->inject_touch_event.position.point.y = from->y;
|
screen_convert_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 =
|
to->inject_touch_event.buttons =
|
||||||
convert_mouse_buttons(SDL_BUTTON(from->button));
|
convert_mouse_buttons(SDL_BUTTON(from->button));
|
||||||
@ -530,10 +535,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;
|
||||||
@ -557,9 +567,15 @@ 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_to_frame_coords(screen, mouse_x, mouse_y),
|
||||||
};
|
};
|
||||||
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
||||||
|
@ -14,6 +14,11 @@ 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 prefer_text;
|
bool prefer_text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,6 +29,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 +56,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;
|
||||||
|
56
app/src/opengl.c
Normal file
56
app/src/opengl.c
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#include "opengl.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "SDL2/SDL.h"
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_opengl_init(struct sc_opengl *gl) {
|
||||||
|
gl->GetString = SDL_GL_GetProcAddress("glGetString");
|
||||||
|
assert(gl->GetString);
|
||||||
|
|
||||||
|
gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf");
|
||||||
|
assert(gl->TexParameterf);
|
||||||
|
|
||||||
|
gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri");
|
||||||
|
assert(gl->TexParameteri);
|
||||||
|
|
||||||
|
// optional
|
||||||
|
gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap");
|
||||||
|
|
||||||
|
const char *version = (const char *) gl->GetString(GL_VERSION);
|
||||||
|
assert(version);
|
||||||
|
gl->version = version;
|
||||||
|
|
||||||
|
#define OPENGL_ES_PREFIX "OpenGL ES "
|
||||||
|
/* starts with "OpenGL ES " */
|
||||||
|
gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX,
|
||||||
|
sizeof(OPENGL_ES_PREFIX) - 1);
|
||||||
|
if (gl->is_opengles) {
|
||||||
|
/* skip the prefix */
|
||||||
|
version += sizeof(PREFIX) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor);
|
||||||
|
if (r != 2) {
|
||||||
|
// failed to parse the version
|
||||||
|
gl->version_major = 0;
|
||||||
|
gl->version_minor = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_opengl_version_at_least(struct sc_opengl *gl,
|
||||||
|
int minver_major, int minver_minor,
|
||||||
|
int minver_es_major, int minver_es_minor)
|
||||||
|
{
|
||||||
|
if (gl->is_opengles) {
|
||||||
|
return gl->version_major > minver_es_major
|
||||||
|
|| (gl->version_major == minver_es_major
|
||||||
|
&& gl->version_minor >= minver_es_minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gl->version_major > minver_major
|
||||||
|
|| (gl->version_major == minver_major
|
||||||
|
&& gl->version_minor >= minver_minor);
|
||||||
|
}
|
36
app/src/opengl.h
Normal file
36
app/src/opengl.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#ifndef SC_OPENGL_H
|
||||||
|
#define SC_OPENGL_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <SDL2/SDL_opengl.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
struct sc_opengl {
|
||||||
|
const char *version;
|
||||||
|
bool is_opengles;
|
||||||
|
int version_major;
|
||||||
|
int version_minor;
|
||||||
|
|
||||||
|
const GLubyte *
|
||||||
|
(*GetString)(GLenum name);
|
||||||
|
|
||||||
|
void
|
||||||
|
(*TexParameterf)(GLenum target, GLenum pname, GLfloat param);
|
||||||
|
|
||||||
|
void
|
||||||
|
(*TexParameteri)(GLenum target, GLenum pname, GLint param);
|
||||||
|
|
||||||
|
void
|
||||||
|
(*GenerateMipmap)(GLenum target);
|
||||||
|
};
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_opengl_init(struct sc_opengl *gl);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_opengl_version_at_least(struct sc_opengl *gl,
|
||||||
|
int minver_major, int minver_minor,
|
||||||
|
int minver_es_major, int minver_es_minor);
|
||||||
|
|
||||||
|
#endif
|
@ -60,28 +60,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
105
app/src/scrcpy.c
105
app/src/scrcpy.c
@ -7,6 +7,10 @@
|
|||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
# include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@ -42,12 +46,26 @@ static struct input_manager input_manager = {
|
|||||||
.controller = &controller,
|
.controller = &controller,
|
||||||
.video_buffer = &video_buffer,
|
.video_buffer = &video_buffer,
|
||||||
.screen = &screen,
|
.screen = &screen,
|
||||||
|
.repeat = 0,
|
||||||
.prefer_text = false, // initialized later
|
.prefer_text = false, // initialized later
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
|
||||||
|
if (ctrl_type == CTRL_C_EVENT) {
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
// init SDL and set appropriate hints
|
// init SDL and set appropriate hints
|
||||||
static bool
|
static bool
|
||||||
sdl_init_and_configure(bool display) {
|
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());
|
||||||
@ -56,13 +74,25 @@ sdl_init_and_configure(bool display) {
|
|||||||
|
|
||||||
atexit(SDL_Quit);
|
atexit(SDL_Quit);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Clean up properly on Ctrl+C on Windows
|
||||||
|
bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
|
||||||
|
if (!ok) {
|
||||||
|
LOGW("Could not set Ctrl+C handler");
|
||||||
|
}
|
||||||
|
#endif // _WIN32
|
||||||
|
|
||||||
if (!display) {
|
if (!display) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the best available scale quality
|
if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) {
|
||||||
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
|
LOGW("Could not set render driver");
|
||||||
LOGW("Could not enable bilinear filtering");
|
}
|
||||||
|
|
||||||
|
// Linear filtering
|
||||||
|
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
|
||||||
|
LOGW("Could not enable linear filtering");
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
|
#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
|
||||||
@ -84,8 +114,13 @@ sdl_init_and_configure(bool display) {
|
|||||||
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) {
|
||||||
SDL_EnableScreenSaver();
|
LOGD("Screensaver disabled");
|
||||||
|
SDL_DisableScreenSaver();
|
||||||
|
} else {
|
||||||
|
LOGD("Screensaver enabled");
|
||||||
|
SDL_EnableScreenSaver();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -106,8 +141,9 @@ event_watcher(void *data, SDL_Event *event) {
|
|||||||
(void) data;
|
(void) data;
|
||||||
if (event->type == SDL_WINDOWEVENT
|
if (event->type == SDL_WINDOWEVENT
|
||||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||||
// called from another thread, not very safe, but it's a workaround!
|
// In practice, it seems to always be called from the same thread in
|
||||||
screen_render(&screen);
|
// that specific case. Anyway, it's just a workaround.
|
||||||
|
screen_render(&screen, true);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -224,21 +260,6 @@ event_loop(bool display, bool control) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static process_t
|
|
||||||
set_show_touches_enabled(const char *serial, bool enabled) {
|
|
||||||
const char *value = enabled ? "1" : "0";
|
|
||||||
const char *const adb_cmd[] = {
|
|
||||||
"shell", "settings", "put", "system", "show_touches", value
|
|
||||||
};
|
|
||||||
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
wait_show_touches(process_t process) {
|
|
||||||
// reap the process, ignore the result
|
|
||||||
process_check_success(process, "show_touches");
|
|
||||||
}
|
|
||||||
|
|
||||||
static SDL_LogPriority
|
static SDL_LogPriority
|
||||||
sdl_priority_from_av_level(int level) {
|
sdl_priority_from_av_level(int level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
@ -279,25 +300,24 @@ 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,
|
||||||
.local_port = options->port,
|
.port_range = options->port_range,
|
||||||
.max_size = options->max_size,
|
.max_size = options->max_size,
|
||||||
.bit_rate = options->bit_rate,
|
.bit_rate = options->bit_rate,
|
||||||
.max_fps = options->max_fps,
|
.max_fps = options->max_fps,
|
||||||
|
.lock_video_orientation = options->lock_video_orientation,
|
||||||
.control = options->control,
|
.control = options->control,
|
||||||
|
.display_id = options->display_id,
|
||||||
|
.show_touches = options->show_touches,
|
||||||
|
.stay_awake = options->stay_awake,
|
||||||
|
.codec_options = options->codec_options,
|
||||||
|
.force_adb_forward = options->force_adb_forward,
|
||||||
};
|
};
|
||||||
if (!server_start(&server, options->serial, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
process_t proc_show_touches = PROCESS_NONE;
|
|
||||||
bool show_touches_waited;
|
|
||||||
if (options->show_touches) {
|
|
||||||
LOGI("Enable show_touches");
|
|
||||||
proc_show_touches = set_show_touches_enabled(options->serial, true);
|
|
||||||
show_touches_waited = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ret = false;
|
bool ret = false;
|
||||||
|
|
||||||
bool fps_counter_initialized = false;
|
bool fps_counter_initialized = false;
|
||||||
@ -308,7 +328,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)) {
|
if (!sdl_init_and_configure(options->display, options->render_driver,
|
||||||
|
options->disable_screensaver)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +415,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
options->always_on_top, options->window_x,
|
options->always_on_top, options->window_x,
|
||||||
options->window_y, options->window_width,
|
options->window_y, options->window_width,
|
||||||
options->window_height,
|
options->window_height,
|
||||||
options->window_borderless)) {
|
options->window_borderless,
|
||||||
|
options->rotation, options-> mipmaps)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,11 +435,6 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->show_touches) {
|
|
||||||
wait_show_touches(proc_show_touches);
|
|
||||||
show_touches_waited = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
input_manager.prefer_text = options->prefer_text;
|
input_manager.prefer_text = options->prefer_text;
|
||||||
|
|
||||||
ret = event_loop(options->display, options->control);
|
ret = event_loop(options->display, options->control);
|
||||||
@ -474,16 +491,6 @@ end:
|
|||||||
fps_counter_destroy(&fps_counter);
|
fps_counter_destroy(&fps_counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->show_touches) {
|
|
||||||
if (!show_touches_waited) {
|
|
||||||
// wait the process which enabled "show touches"
|
|
||||||
wait_show_touches(proc_show_touches);
|
|
||||||
}
|
|
||||||
LOGI("Disable show_touches");
|
|
||||||
proc_show_touches = set_show_touches_enabled(options->serial, false);
|
|
||||||
wait_show_touches(proc_show_touches);
|
|
||||||
}
|
|
||||||
|
|
||||||
server_destroy(&server);
|
server_destroy(&server);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "common.h"
|
||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
struct scrcpy_options {
|
struct scrcpy_options {
|
||||||
const char *serial;
|
const char *serial;
|
||||||
@ -14,15 +16,21 @@ struct scrcpy_options {
|
|||||||
const char *record_filename;
|
const char *record_filename;
|
||||||
const char *window_title;
|
const char *window_title;
|
||||||
const char *push_target;
|
const char *push_target;
|
||||||
|
const char *render_driver;
|
||||||
|
const char *codec_options;
|
||||||
|
enum sc_log_level log_level;
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
uint16_t port;
|
struct 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;
|
||||||
int16_t window_x;
|
int8_t lock_video_orientation;
|
||||||
int16_t window_y;
|
uint8_t rotation;
|
||||||
|
int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto"
|
||||||
|
int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto"
|
||||||
uint16_t window_width;
|
uint16_t window_width;
|
||||||
uint16_t window_height;
|
uint16_t window_height;
|
||||||
|
uint16_t display_id;
|
||||||
bool show_touches;
|
bool show_touches;
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool always_on_top;
|
bool always_on_top;
|
||||||
@ -32,6 +40,10 @@ struct scrcpy_options {
|
|||||||
bool render_expired_frames;
|
bool render_expired_frames;
|
||||||
bool prefer_text;
|
bool prefer_text;
|
||||||
bool window_borderless;
|
bool window_borderless;
|
||||||
|
bool mipmaps;
|
||||||
|
bool stay_awake;
|
||||||
|
bool force_adb_forward;
|
||||||
|
bool disable_screensaver;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCRCPY_OPTIONS_DEFAULT { \
|
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||||
@ -40,15 +52,24 @@ struct scrcpy_options {
|
|||||||
.record_filename = NULL, \
|
.record_filename = NULL, \
|
||||||
.window_title = NULL, \
|
.window_title = NULL, \
|
||||||
.push_target = NULL, \
|
.push_target = NULL, \
|
||||||
|
.render_driver = NULL, \
|
||||||
|
.codec_options = NULL, \
|
||||||
|
.log_level = SC_LOG_LEVEL_INFO, \
|
||||||
.record_format = RECORDER_FORMAT_AUTO, \
|
.record_format = RECORDER_FORMAT_AUTO, \
|
||||||
.port = DEFAULT_LOCAL_PORT, \
|
.port_range = { \
|
||||||
|
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
||||||
|
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
|
||||||
|
}, \
|
||||||
.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, \
|
||||||
.window_x = -1, \
|
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
|
||||||
.window_y = -1, \
|
.rotation = 0, \
|
||||||
|
.window_x = WINDOW_POSITION_UNDEFINED, \
|
||||||
|
.window_y = WINDOW_POSITION_UNDEFINED, \
|
||||||
.window_width = 0, \
|
.window_width = 0, \
|
||||||
.window_height = 0, \
|
.window_height = 0, \
|
||||||
|
.display_id = 0, \
|
||||||
.show_touches = false, \
|
.show_touches = false, \
|
||||||
.fullscreen = false, \
|
.fullscreen = false, \
|
||||||
.always_on_top = false, \
|
.always_on_top = false, \
|
||||||
@ -58,6 +79,10 @@ struct scrcpy_options {
|
|||||||
.render_expired_frames = false, \
|
.render_expired_frames = false, \
|
||||||
.prefer_text = false, \
|
.prefer_text = false, \
|
||||||
.window_borderless = false, \
|
.window_borderless = false, \
|
||||||
|
.mipmaps = true, \
|
||||||
|
.stay_awake = false, \
|
||||||
|
.force_adb_forward = false, \
|
||||||
|
.disable_screensaver = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
460
app/src/screen.c
460
app/src/screen.c
@ -15,12 +15,25 @@
|
|||||||
|
|
||||||
#define DISPLAY_MARGINS 96
|
#define DISPLAY_MARGINS 96
|
||||||
|
|
||||||
|
static inline struct size
|
||||||
|
get_rotated_size(struct size size, int rotation) {
|
||||||
|
struct size rotated_size;
|
||||||
|
if (rotation & 1) {
|
||||||
|
rotated_size.width = size.height;
|
||||||
|
rotated_size.height = size.width;
|
||||||
|
} else {
|
||||||
|
rotated_size.width = size.width;
|
||||||
|
rotated_size.height = size.height;
|
||||||
|
}
|
||||||
|
return rotated_size;
|
||||||
|
}
|
||||||
|
|
||||||
// get the window size in a struct size
|
// 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;
|
||||||
@ -28,31 +41,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)
|
||||||
@ -74,102 +68,179 @@ 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)
|
||||||
// - it keeps the aspect ratio
|
// - it keeps the aspect ratio
|
||||||
// - it scales down to make it fit in the display_size
|
// - it scales down to make it fit in the display_size
|
||||||
static struct size
|
static struct size
|
||||||
get_optimal_size(struct size current_size, struct size frame_size) {
|
get_optimal_size(struct size current_size, struct size content_size) {
|
||||||
if (frame_size.width == 0 || frame_size.height == 0) {
|
if (content_size.width == 0 || content_size.height == 0) {
|
||||||
// avoid division by 0
|
// avoid division by 0
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool keep_width = frame_size.width * h > frame_size.height * w;
|
if (is_optimal_size(window_size, content_size)) {
|
||||||
|
return window_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = frame_size.height * w / frame_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 = frame_size.width * h / frame_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 frame_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, frame_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
|
||||||
// req_width and req_height, if not 0, are the sizes requested by the user
|
// req_width and req_height, if not 0, are the sizes requested by the user
|
||||||
static inline struct size
|
static inline struct size
|
||||||
get_initial_optimal_size(struct size frame_size, uint16_t req_width,
|
get_initial_optimal_size(struct size content_size, uint16_t req_width,
|
||||||
uint16_t req_height) {
|
uint16_t req_height) {
|
||||||
struct size window_size;
|
struct size window_size;
|
||||||
if (!req_width && !req_height) {
|
if (!req_width && !req_height) {
|
||||||
window_size = get_optimal_size(frame_size, frame_size);
|
window_size = get_optimal_size(content_size, content_size);
|
||||||
} else {
|
} else {
|
||||||
if (req_width) {
|
if (req_width) {
|
||||||
window_size.width = req_width;
|
window_size.width = req_width;
|
||||||
} else {
|
} else {
|
||||||
// compute from the requested height
|
// compute from the requested height
|
||||||
window_size.width = (uint32_t) req_height * frame_size.width
|
window_size.width = (uint32_t) req_height * content_size.width
|
||||||
/ frame_size.height;
|
/ content_size.height;
|
||||||
}
|
}
|
||||||
if (req_height) {
|
if (req_height) {
|
||||||
window_size.height = req_height;
|
window_size.height = req_height;
|
||||||
} else {
|
} else {
|
||||||
// compute from the requested width
|
// compute from the requested width
|
||||||
window_size.height = (uint32_t) req_width * frame_size.height
|
window_size.height = (uint32_t) req_width * content_size.height
|
||||||
/ frame_size.width;
|
/ content_size.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline SDL_Texture *
|
static inline SDL_Texture *
|
||||||
create_texture(SDL_Renderer *renderer, struct size frame_size) {
|
create_texture(struct screen *screen) {
|
||||||
return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
|
SDL_Renderer *renderer = screen->renderer;
|
||||||
SDL_TEXTUREACCESS_STREAMING,
|
struct size size = screen->frame_size;
|
||||||
frame_size.width, frame_size.height);
|
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
|
||||||
|
SDL_TEXTUREACCESS_STREAMING,
|
||||||
|
size.width, size.height);
|
||||||
|
if (!texture) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen->mipmaps) {
|
||||||
|
struct sc_opengl *gl = &screen->gl;
|
||||||
|
|
||||||
|
SDL_GL_BindTexture(texture, NULL, NULL);
|
||||||
|
|
||||||
|
// Enable trilinear filtering for downscaling
|
||||||
|
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
||||||
|
GL_LINEAR_MIPMAP_LINEAR);
|
||||||
|
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
||||||
|
|
||||||
|
SDL_GL_UnbindTexture(texture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
int16_t window_x, int16_t window_y, uint16_t window_width,
|
int16_t window_x, int16_t window_y, uint16_t window_width,
|
||||||
uint16_t window_height, bool window_borderless) {
|
uint16_t window_height, bool window_borderless,
|
||||||
|
uint8_t rotation, bool mipmaps) {
|
||||||
screen->frame_size = frame_size;
|
screen->frame_size = frame_size;
|
||||||
|
screen->rotation = rotation;
|
||||||
|
if (rotation) {
|
||||||
|
LOGI("Initial display rotation set to %u", rotation);
|
||||||
|
}
|
||||||
|
struct size content_size = get_rotated_size(frame_size, screen->rotation);
|
||||||
|
screen->content_size = content_size;
|
||||||
|
|
||||||
struct size window_size =
|
struct size window_size =
|
||||||
get_initial_optimal_size(frame_size, window_width, window_height);
|
get_initial_optimal_size(content_size, window_width, window_height);
|
||||||
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
|
||||||
#ifdef HIDPI_SUPPORT
|
#ifdef HIDPI_SUPPORT
|
||||||
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
@ -186,8 +257,10 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
window_flags |= SDL_WINDOW_BORDERLESS;
|
window_flags |= SDL_WINDOW_BORDERLESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
|
int x = window_x != WINDOW_POSITION_UNDEFINED
|
||||||
int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
|
? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
|
||||||
|
int y = window_y != WINDOW_POSITION_UNDEFINED
|
||||||
|
? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
|
||||||
screen->window = SDL_CreateWindow(window_title, x, y,
|
screen->window = SDL_CreateWindow(window_title, x, y,
|
||||||
window_size.width, window_size.height,
|
window_size.width, window_size.height,
|
||||||
window_flags);
|
window_flags);
|
||||||
@ -204,11 +277,35 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width,
|
SDL_RendererInfo renderer_info;
|
||||||
frame_size.height)) {
|
int r = SDL_GetRendererInfo(screen->renderer, &renderer_info);
|
||||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
const char *renderer_name = r ? NULL : renderer_info.name;
|
||||||
screen_destroy(screen);
|
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||||
return false;
|
|
||||||
|
// starts with "opengl"
|
||||||
|
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||||
|
if (screen->use_opengl) {
|
||||||
|
struct sc_opengl *gl = &screen->gl;
|
||||||
|
sc_opengl_init(gl);
|
||||||
|
|
||||||
|
LOGI("OpenGL version: %s", gl->version);
|
||||||
|
|
||||||
|
if (mipmaps) {
|
||||||
|
bool supports_mipmaps =
|
||||||
|
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
|
||||||
|
2, 0 /* OpenGL ES 2.0+ */);
|
||||||
|
if (supports_mipmaps) {
|
||||||
|
LOGI("Trilinear filtering enabled");
|
||||||
|
screen->mipmaps = true;
|
||||||
|
} else {
|
||||||
|
LOGW("Trilinear filtering disabled "
|
||||||
|
"(OpenGL 3.0+ or ES 2.0+ required)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGI("Trilinear filtering disabled");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Surface *icon = read_xpm(icon_xpm);
|
SDL_Surface *icon = read_xpm(icon_xpm);
|
||||||
@ -221,14 +318,19 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
|
|
||||||
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
|
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
|
||||||
frame_size.height);
|
frame_size.height);
|
||||||
screen->texture = create_texture(screen->renderer, frame_size);
|
screen->texture = create_texture(screen);
|
||||||
if (!screen->texture) {
|
if (!screen->texture) {
|
||||||
LOGC("Could not create texture: %s", SDL_GetError());
|
LOGC("Could not create texture: %s", SDL_GetError());
|
||||||
screen_destroy(screen);
|
screen_destroy(screen);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -251,35 +353,82 @@ 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
|
||||||
|
screen_set_rotation(struct screen *screen, unsigned rotation) {
|
||||||
|
assert(rotation < 4);
|
||||||
|
if (rotation == screen->rotation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct size new_content_size =
|
||||||
|
get_rotated_size(screen->frame_size, rotation);
|
||||||
|
|
||||||
|
set_content_size(screen, new_content_size);
|
||||||
|
|
||||||
|
screen->rotation = rotation;
|
||||||
|
LOGI("Display rotation set to %u", rotation);
|
||||||
|
|
||||||
|
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
|
||||||
static bool
|
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) {
|
||||||
if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width,
|
|
||||||
new_frame_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 windowed_size = get_windowed_window_size(screen);
|
|
||||||
struct size target_size = {
|
|
||||||
(uint32_t) windowed_size.width * new_frame_size.width
|
|
||||||
/ screen->frame_size.width,
|
|
||||||
(uint32_t) windowed_size.height * new_frame_size.height
|
|
||||||
/ screen->frame_size.height,
|
|
||||||
};
|
|
||||||
target_size = get_optimal_size(target_size, new_frame_size);
|
|
||||||
set_window_size(screen, target_size);
|
|
||||||
|
|
||||||
screen->frame_size = new_frame_size;
|
screen->frame_size = new_frame_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);
|
||||||
screen->texture = create_texture(screen->renderer, new_frame_size);
|
screen->texture = create_texture(screen);
|
||||||
if (!screen->texture) {
|
if (!screen->texture) {
|
||||||
LOGC("Could not create texture: %s", SDL_GetError());
|
LOGC("Could not create texture: %s", SDL_GetError());
|
||||||
return false;
|
return false;
|
||||||
@ -296,6 +445,13 @@ update_texture(struct screen *screen, const AVFrame *frame) {
|
|||||||
frame->data[0], frame->linesize[0],
|
frame->data[0], frame->linesize[0],
|
||||||
frame->data[1], frame->linesize[1],
|
frame->data[1], frame->linesize[1],
|
||||||
frame->data[2], frame->linesize[2]);
|
frame->data[2], frame->linesize[2]);
|
||||||
|
|
||||||
|
if (screen->mipmaps) {
|
||||||
|
assert(screen->use_opengl);
|
||||||
|
SDL_GL_BindTexture(screen->texture, NULL, NULL);
|
||||||
|
screen->gl.GenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
SDL_GL_UnbindTexture(screen->texture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -310,14 +466,41 @@ 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);
|
||||||
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
|
if (screen->rotation == 0) {
|
||||||
|
SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
|
||||||
|
} else {
|
||||||
|
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
||||||
|
// counterclockwise (to be consistent with --lock-video-orientation)
|
||||||
|
int cw_rotation = (4 - screen->rotation) % 4;
|
||||||
|
double angle = 90 * cw_rotation;
|
||||||
|
|
||||||
|
SDL_Rect *dstrect = NULL;
|
||||||
|
SDL_Rect rect;
|
||||||
|
if (screen->rotation & 1) {
|
||||||
|
rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
|
||||||
|
rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
|
||||||
|
rect.w = screen->rect.h;
|
||||||
|
rect.h = screen->rect.w;
|
||||||
|
dstrect = ▭
|
||||||
|
} else {
|
||||||
|
assert(screen->rotation == 2);
|
||||||
|
dstrect = &screen->rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
|
||||||
|
angle, NULL, 0);
|
||||||
|
}
|
||||||
SDL_RenderPresent(screen->renderer);
|
SDL_RenderPresent(screen->renderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,27 +513,25 @@ 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->frame_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);
|
||||||
LOGD("Resized to optimal size");
|
LOGD("Resized to optimal size: %ux%u", optimal_size.width,
|
||||||
|
optimal_size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -364,9 +545,10 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
|
|||||||
screen->maximized = false;
|
screen->maximized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetWindowSize(screen->window, screen->frame_size.width,
|
struct size content_size = screen->content_size;
|
||||||
screen->frame_size.height);
|
SDL_SetWindowSize(screen->window, content_size.width, content_size.height);
|
||||||
LOGD("Resized to pixel-perfect");
|
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
|
||||||
|
content_size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -374,39 +556,73 @@ 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
|
||||||
|
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
|
||||||
|
unsigned rotation = screen->rotation;
|
||||||
|
assert(rotation < 4);
|
||||||
|
|
||||||
|
int32_t w = screen->content_size.width;
|
||||||
|
int32_t h = screen->content_size.height;
|
||||||
|
|
||||||
|
screen_hidpi_scale_coords(screen, &x, &y);
|
||||||
|
|
||||||
|
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;
|
||||||
|
switch (rotation) {
|
||||||
|
case 0:
|
||||||
|
result.x = x;
|
||||||
|
result.y = y;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
result.x = h - y;
|
||||||
|
result.y = x;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
result.x = w - x;
|
||||||
|
result.y = h - y;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert(rotation == 3);
|
||||||
|
result.x = y;
|
||||||
|
result.y = w - x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "opengl.h"
|
||||||
|
|
||||||
|
#define WINDOW_POSITION_UNDEFINED (-0x8000)
|
||||||
|
|
||||||
struct video_buffer;
|
struct video_buffer;
|
||||||
|
|
||||||
@ -14,38 +17,58 @@ struct screen {
|
|||||||
SDL_Window *window;
|
SDL_Window *window;
|
||||||
SDL_Renderer *renderer;
|
SDL_Renderer *renderer;
|
||||||
SDL_Texture *texture;
|
SDL_Texture *texture;
|
||||||
|
bool use_opengl;
|
||||||
|
struct sc_opengl gl;
|
||||||
struct size frame_size;
|
struct size frame_size;
|
||||||
// The window size the last time it was not maximized or fullscreen.
|
struct size content_size; // rotated frame_size
|
||||||
struct size windowed_window_size;
|
|
||||||
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
|
bool resize_pending; // resize requested while fullscreen or maximized
|
||||||
// able to revert the size to its non-maximized value.
|
// The content size the last time the window was not maximized or
|
||||||
struct size windowed_window_size_backup;
|
// fullscreen (meaningful only when resize_pending is true)
|
||||||
|
struct size windowed_content_size;
|
||||||
|
|
||||||
|
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
|
||||||
|
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;
|
||||||
bool no_window;
|
bool no_window;
|
||||||
|
bool mipmaps;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCREEN_INITIALIZER { \
|
#define SCREEN_INITIALIZER { \
|
||||||
.window = NULL, \
|
.window = NULL, \
|
||||||
.renderer = NULL, \
|
.renderer = NULL, \
|
||||||
.texture = NULL, \
|
.texture = NULL, \
|
||||||
|
.use_opengl = false, \
|
||||||
|
.gl = {0}, \
|
||||||
.frame_size = { \
|
.frame_size = { \
|
||||||
.width = 0, \
|
|
||||||
.height = 0, \
|
|
||||||
}, \
|
|
||||||
.windowed_window_size = { \
|
|
||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.windowed_window_size_backup = { \
|
.content_size = { \
|
||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
|
.resize_pending = false, \
|
||||||
|
.windowed_content_size = { \
|
||||||
|
.width = 0, \
|
||||||
|
.height = 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, \
|
||||||
.no_window = false, \
|
.no_window = false, \
|
||||||
|
.mipmaps = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize default values
|
// initialize default values
|
||||||
@ -53,11 +76,13 @@ 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
|
||||||
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,
|
||||||
int16_t window_x, int16_t window_y, uint16_t window_width,
|
int16_t window_x, int16_t window_y, uint16_t window_width,
|
||||||
uint16_t window_height, bool window_borderless);
|
uint16_t window_height, bool window_borderless,
|
||||||
|
uint8_t rotation, bool mipmaps);
|
||||||
|
|
||||||
// show the window
|
// show the window
|
||||||
void
|
void
|
||||||
@ -72,8 +97,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
|
||||||
@ -87,8 +115,24 @@ screen_resize_to_fit(struct screen *screen);
|
|||||||
void
|
void
|
||||||
screen_resize_to_pixel_perfect(struct screen *screen);
|
screen_resize_to_pixel_perfect(struct screen *screen);
|
||||||
|
|
||||||
|
// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise)
|
||||||
|
void
|
||||||
|
screen_set_rotation(struct screen *screen, unsigned rotation);
|
||||||
|
|
||||||
// react to window events
|
// react to window events
|
||||||
void
|
void
|
||||||
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
|
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
|
||||||
|
|
||||||
|
// convert point from window coordinates to frame coordinates
|
||||||
|
// x and y are expressed in pixels
|
||||||
|
struct point
|
||||||
|
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
267
app/src/server.c
267
app/src/server.c
@ -5,6 +5,7 @@
|
|||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <libgen.h>
|
#include <libgen.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
#include <SDL2/SDL_timer.h>
|
#include <SDL2/SDL_timer.h>
|
||||||
#include <SDL2/SDL_platform.h>
|
#include <SDL2/SDL_platform.h>
|
||||||
|
|
||||||
@ -126,17 +127,6 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
|
|||||||
return process_check_success(process, "adb forward --remove");
|
return process_check_success(process, "adb forward --remove");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
enable_tunnel(struct server *server) {
|
|
||||||
if (enable_tunnel_reverse(server->serial, server->local_port)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
|
||||||
server->tunnel_forward = true;
|
|
||||||
return enable_tunnel_forward(server->serial, server->local_port);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
disable_tunnel(struct server *server) {
|
disable_tunnel(struct server *server) {
|
||||||
if (server->tunnel_forward) {
|
if (server->tunnel_forward) {
|
||||||
@ -145,33 +135,165 @@ disable_tunnel(struct server *server) {
|
|||||||
return disable_tunnel_reverse(server->serial);
|
return disable_tunnel_reverse(server->serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static socket_t
|
||||||
|
listen_on_port(uint16_t port) {
|
||||||
|
#define IPV4_LOCALHOST 0x7F000001
|
||||||
|
return net_listen(IPV4_LOCALHOST, port, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
enable_tunnel_reverse_any_port(struct server *server,
|
||||||
|
struct port_range port_range) {
|
||||||
|
uint16_t port = port_range.first;
|
||||||
|
for (;;) {
|
||||||
|
if (!enable_tunnel_reverse(server->serial, port)) {
|
||||||
|
// the command itself failed, it will fail on any port
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At the application level, the device part is "the server" because it
|
||||||
|
// serves video stream and control. However, at the network level, the
|
||||||
|
// client listens and the server connects to the client. That way, the
|
||||||
|
// client can listen before starting the server app, so there is no
|
||||||
|
// need to try to connect until the server socket is listening on the
|
||||||
|
// device.
|
||||||
|
server->server_socket = listen_on_port(port);
|
||||||
|
if (server->server_socket != INVALID_SOCKET) {
|
||||||
|
// success
|
||||||
|
server->local_port = port;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// failure, disable tunnel and try another port
|
||||||
|
if (!disable_tunnel_reverse(server->serial)) {
|
||||||
|
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check before incrementing to avoid overflow on port 65535
|
||||||
|
if (port < port_range.last) {
|
||||||
|
LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
|
||||||
|
port, (uint16_t) (port + 1));
|
||||||
|
port++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port_range.first == port_range.last) {
|
||||||
|
LOGE("Could not listen on port %" PRIu16, port_range.first);
|
||||||
|
} else {
|
||||||
|
LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
|
||||||
|
port_range.first, port_range.last);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
enable_tunnel_forward_any_port(struct server *server,
|
||||||
|
struct port_range port_range) {
|
||||||
|
server->tunnel_forward = true;
|
||||||
|
uint16_t port = port_range.first;
|
||||||
|
for (;;) {
|
||||||
|
if (enable_tunnel_forward(server->serial, port)) {
|
||||||
|
// success
|
||||||
|
server->local_port = port;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port < port_range.last) {
|
||||||
|
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
|
||||||
|
port, port + 1);
|
||||||
|
port++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port_range.first == port_range.last) {
|
||||||
|
LOGE("Could not forward port %" PRIu16, port_range.first);
|
||||||
|
} else {
|
||||||
|
LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
|
||||||
|
port_range.first, port_range.last);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
enable_tunnel_any_port(struct server *server, struct port_range port_range,
|
||||||
|
bool force_adb_forward) {
|
||||||
|
if (!force_adb_forward) {
|
||||||
|
// Attempt to use "adb reverse"
|
||||||
|
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if "adb reverse" does not work (e.g. over "adb connect"), it
|
||||||
|
// fallbacks to "adb forward", so the app socket is the client
|
||||||
|
|
||||||
|
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return enable_tunnel_forward_any_port(server, port_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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[5];
|
||||||
|
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);
|
||||||
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
|
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
|
||||||
|
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
|
||||||
|
sprintf(display_id_string, "%"PRIu16, params->display_id);
|
||||||
const char *const cmd[] = {
|
const char *const cmd[] = {
|
||||||
"shell",
|
"shell",
|
||||||
"CLASSPATH=" DEVICE_SERVER_PATH,
|
"CLASSPATH=" DEVICE_SERVER_PATH,
|
||||||
"app_process",
|
"app_process",
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
# define SERVER_DEBUGGER_PORT "5005"
|
# define SERVER_DEBUGGER_PORT "5005"
|
||||||
|
# ifdef SERVER_DEBUGGER_METHOD_NEW
|
||||||
|
/* Android 9 and above */
|
||||||
|
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
|
||||||
|
# else
|
||||||
|
/* Android 8 and below */
|
||||||
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
|
||||||
|
# endif
|
||||||
SERVER_DEBUGGER_PORT,
|
SERVER_DEBUGGER_PORT,
|
||||||
#endif
|
#endif
|
||||||
"/", // 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,
|
||||||
|
lock_video_orientation_string,
|
||||||
server->tunnel_forward ? "true" : "false",
|
server->tunnel_forward ? "true" : "false",
|
||||||
params->crop ? params->crop : "-",
|
params->crop ? params->crop : "-",
|
||||||
"true", // always send frame meta (packet boundaries + timestamp)
|
"true", // always send frame meta (packet boundaries + timestamp)
|
||||||
params->control ? "true" : "false",
|
params->control ? "true" : "false",
|
||||||
|
display_id_string,
|
||||||
|
params->show_touches ? "true" : "false",
|
||||||
|
params->stay_awake ? "true" : "false",
|
||||||
|
params->codec_options ? params->codec_options : "-",
|
||||||
};
|
};
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
LOGI("Server debugger waiting for a client on device port "
|
LOGI("Server debugger waiting for a client on device port "
|
||||||
@ -187,13 +309,6 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
#define IPV4_LOCALHOST 0x7F000001
|
|
||||||
|
|
||||||
static socket_t
|
|
||||||
listen_on_port(uint16_t port) {
|
|
||||||
return net_listen(IPV4_LOCALHOST, port, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static socket_t
|
static socket_t
|
||||||
connect_and_read_byte(uint16_t port) {
|
connect_and_read_byte(uint16_t port) {
|
||||||
socket_t socket = net_connect(IPV4_LOCALHOST, port);
|
socket_t socket = net_connect(IPV4_LOCALHOST, port);
|
||||||
@ -229,14 +344,12 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
close_socket(socket_t *socket) {
|
close_socket(socket_t socket) {
|
||||||
assert(*socket != INVALID_SOCKET);
|
assert(socket != INVALID_SOCKET);
|
||||||
net_shutdown(*socket, SHUT_RDWR);
|
net_shutdown(socket, SHUT_RDWR);
|
||||||
if (!net_close(*socket)) {
|
if (!net_close(socket)) {
|
||||||
LOGW("Could not close socket");
|
LOGW("Could not close socket");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
*socket = INVALID_SOCKET;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -244,10 +357,26 @@ server_init(struct server *server) {
|
|||||||
*server = (struct server) SERVER_INITIALIZER;
|
*server = (struct server) SERVER_INITIALIZER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
run_wait_server(void *data) {
|
||||||
|
struct server *server = data;
|
||||||
|
cmd_simple_wait(server->process, NULL); // ignore exit code
|
||||||
|
// no need for synchronization, server_socket is initialized before this
|
||||||
|
// thread was created
|
||||||
|
if (server->server_socket != INVALID_SOCKET
|
||||||
|
&& !atomic_flag_test_and_set(&server->server_socket_closed)) {
|
||||||
|
// On Linux, accept() is unblocked by shutdown(), but on Windows, it is
|
||||||
|
// unblocked by closesocket(). Therefore, call both (close_socket()).
|
||||||
|
close_socket(server->server_socket);
|
||||||
|
}
|
||||||
|
LOGD("Server terminated");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
server_start(struct server *server, const char *serial,
|
server_start(struct server *server, const char *serial,
|
||||||
const struct server_params *params) {
|
const struct server_params *params) {
|
||||||
server->local_port = params->local_port;
|
server->port_range = params->port_range;
|
||||||
|
|
||||||
if (serial) {
|
if (serial) {
|
||||||
server->serial = SDL_strdup(serial);
|
server->serial = SDL_strdup(serial);
|
||||||
@ -257,49 +386,51 @@ server_start(struct server *server, const char *serial,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!push_server(serial)) {
|
if (!push_server(serial)) {
|
||||||
SDL_free(server->serial);
|
goto error1;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enable_tunnel(server)) {
|
if (!enable_tunnel_any_port(server, params->port_range,
|
||||||
SDL_free(server->serial);
|
params->force_adb_forward)) {
|
||||||
return false;
|
goto error1;
|
||||||
}
|
|
||||||
|
|
||||||
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
|
|
||||||
// "adb forward", so the app socket is the client
|
|
||||||
if (!server->tunnel_forward) {
|
|
||||||
// At the application level, the device part is "the server" because it
|
|
||||||
// serves video stream and control. However, at the network level, the
|
|
||||||
// client listens and the server connects to the client. That way, the
|
|
||||||
// client can listen before starting the server app, so there is no
|
|
||||||
// need to try to connect until the server socket is listening on the
|
|
||||||
// device.
|
|
||||||
|
|
||||||
server->server_socket = listen_on_port(params->local_port);
|
|
||||||
if (server->server_socket == INVALID_SOCKET) {
|
|
||||||
LOGE("Could not listen on port %" PRIu16, params->local_port);
|
|
||||||
disable_tunnel(server);
|
|
||||||
SDL_free(server->serial);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// server will connect to our server socket
|
// server will connect to our server socket
|
||||||
server->process = execute_server(server, params);
|
server->process = execute_server(server, params);
|
||||||
|
|
||||||
if (server->process == PROCESS_NONE) {
|
if (server->process == PROCESS_NONE) {
|
||||||
if (!server->tunnel_forward) {
|
goto error2;
|
||||||
close_socket(&server->server_socket);
|
}
|
||||||
}
|
|
||||||
disable_tunnel(server);
|
// If the server process dies before connecting to the server socket, then
|
||||||
SDL_free(server->serial);
|
// the client will be stuck forever on accept(). To avoid the problem, we
|
||||||
return false;
|
// must be able to wake up the accept() call when the server dies. To keep
|
||||||
|
// things simple and multiplatform, just spawn a new thread waiting for the
|
||||||
|
// server process and calling shutdown()/close() on the server socket if
|
||||||
|
// necessary to wake up any accept() blocking call.
|
||||||
|
server->wait_server_thread =
|
||||||
|
SDL_CreateThread(run_wait_server, "wait-server", server);
|
||||||
|
if (!server->wait_server_thread) {
|
||||||
|
cmd_terminate(server->process);
|
||||||
|
cmd_simple_wait(server->process, NULL); // ignore exit code
|
||||||
|
goto error2;
|
||||||
}
|
}
|
||||||
|
|
||||||
server->tunnel_enabled = true;
|
server->tunnel_enabled = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
error2:
|
||||||
|
if (!server->tunnel_forward) {
|
||||||
|
bool was_closed =
|
||||||
|
atomic_flag_test_and_set(&server->server_socket_closed);
|
||||||
|
// the thread is not started, the flag could not be already set
|
||||||
|
assert(!was_closed);
|
||||||
|
(void) was_closed;
|
||||||
|
close_socket(server->server_socket);
|
||||||
|
}
|
||||||
|
disable_tunnel(server);
|
||||||
|
error1:
|
||||||
|
SDL_free(server->serial);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -317,7 +448,11 @@ server_connect_to(struct server *server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we don't need the server socket anymore
|
// we don't need the server socket anymore
|
||||||
close_socket(&server->server_socket);
|
if (!atomic_flag_test_and_set(&server->server_socket_closed)) {
|
||||||
|
// close it from here
|
||||||
|
close_socket(server->server_socket);
|
||||||
|
// otherwise, it is closed by run_wait_server()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
uint32_t attempts = 100;
|
uint32_t attempts = 100;
|
||||||
uint32_t delay = 100; // ms
|
uint32_t delay = 100; // ms
|
||||||
@ -344,29 +479,27 @@ server_connect_to(struct server *server) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
server_stop(struct server *server) {
|
server_stop(struct server *server) {
|
||||||
if (server->server_socket != INVALID_SOCKET) {
|
if (server->server_socket != INVALID_SOCKET
|
||||||
close_socket(&server->server_socket);
|
&& !atomic_flag_test_and_set(&server->server_socket_closed)) {
|
||||||
|
close_socket(server->server_socket);
|
||||||
}
|
}
|
||||||
if (server->video_socket != INVALID_SOCKET) {
|
if (server->video_socket != INVALID_SOCKET) {
|
||||||
close_socket(&server->video_socket);
|
close_socket(server->video_socket);
|
||||||
}
|
}
|
||||||
if (server->control_socket != INVALID_SOCKET) {
|
if (server->control_socket != INVALID_SOCKET) {
|
||||||
close_socket(&server->control_socket);
|
close_socket(server->control_socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(server->process != PROCESS_NONE);
|
assert(server->process != PROCESS_NONE);
|
||||||
|
|
||||||
if (!cmd_terminate(server->process)) {
|
cmd_terminate(server->process);
|
||||||
LOGW("Could not terminate server");
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_simple_wait(server->process, NULL); // ignore exit code
|
|
||||||
LOGD("Server terminated");
|
|
||||||
|
|
||||||
if (server->tunnel_enabled) {
|
if (server->tunnel_enabled) {
|
||||||
// ignore failure
|
// ignore failure
|
||||||
disable_tunnel(server);
|
disable_tunnel(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_WaitThread(server->wait_server_thread, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -1,42 +1,62 @@
|
|||||||
#ifndef SERVER_H
|
#ifndef SERVER_H
|
||||||
#define SERVER_H
|
#define SERVER_H
|
||||||
|
|
||||||
|
#include <stdatomic.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
|
#include "common.h"
|
||||||
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
struct server {
|
struct server {
|
||||||
char *serial;
|
char *serial;
|
||||||
process_t process;
|
process_t process;
|
||||||
|
SDL_Thread *wait_server_thread;
|
||||||
|
atomic_flag server_socket_closed;
|
||||||
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;
|
||||||
uint16_t local_port;
|
struct port_range 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"
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SERVER_INITIALIZER { \
|
#define SERVER_INITIALIZER { \
|
||||||
.serial = NULL, \
|
.serial = NULL, \
|
||||||
.process = PROCESS_NONE, \
|
.process = PROCESS_NONE, \
|
||||||
.server_socket = INVALID_SOCKET, \
|
.wait_server_thread = NULL, \
|
||||||
.video_socket = INVALID_SOCKET, \
|
.server_socket_closed = ATOMIC_FLAG_INIT, \
|
||||||
|
.server_socket = INVALID_SOCKET, \
|
||||||
|
.video_socket = INVALID_SOCKET, \
|
||||||
.control_socket = INVALID_SOCKET, \
|
.control_socket = INVALID_SOCKET, \
|
||||||
.local_port = 0, \
|
.port_range = { \
|
||||||
.tunnel_enabled = false, \
|
.first = 0, \
|
||||||
.tunnel_forward = false, \
|
.last = 0, \
|
||||||
|
}, \
|
||||||
|
.local_port = 0, \
|
||||||
|
.tunnel_enabled = false, \
|
||||||
|
.tunnel_forward = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
struct server_params {
|
struct server_params {
|
||||||
|
enum sc_log_level log_level;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
uint16_t local_port;
|
const char *codec_options;
|
||||||
|
struct 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;
|
||||||
|
int8_t lock_video_orientation;
|
||||||
bool control;
|
bool control;
|
||||||
|
uint16_t display_id;
|
||||||
|
bool show_touches;
|
||||||
|
bool stay_awake;
|
||||||
|
bool force_adb_forward;
|
||||||
};
|
};
|
||||||
|
|
||||||
// init default values
|
// init default values
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
@ -21,6 +23,42 @@
|
|||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
|
bool
|
||||||
|
cmd_search(const char *file) {
|
||||||
|
char *path = getenv("PATH");
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
path = strdup(path);
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool ret = false;
|
||||||
|
size_t file_len = strlen(file);
|
||||||
|
char *saveptr;
|
||||||
|
for (char *dir = strtok_r(path, ":", &saveptr); dir;
|
||||||
|
dir = strtok_r(NULL, ":", &saveptr)) {
|
||||||
|
size_t dir_len = strlen(dir);
|
||||||
|
char *fullpath = malloc(dir_len + file_len + 2);
|
||||||
|
if (!fullpath)
|
||||||
|
continue;
|
||||||
|
memcpy(fullpath, dir, dir_len);
|
||||||
|
fullpath[dir_len] = '/';
|
||||||
|
memcpy(fullpath + dir_len + 1, file, file_len + 1);
|
||||||
|
|
||||||
|
struct stat sb;
|
||||||
|
bool fullpath_executable = stat(fullpath, &sb) == 0 &&
|
||||||
|
sb.st_mode & S_IXUSR;
|
||||||
|
free(fullpath);
|
||||||
|
if (fullpath_executable) {
|
||||||
|
ret = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(path);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
enum process_result
|
enum process_result
|
||||||
cmd_execute(const char *const argv[], pid_t *pid) {
|
cmd_execute(const char *const argv[], pid_t *pid) {
|
||||||
int fd[2];
|
int fd[2];
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
#include "command.h"
|
#include "command.h"
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/str_util.h"
|
#include "util/str_util.h"
|
||||||
|
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
build_cmd(char *cmd, size_t len, const char *const argv[]) {
|
build_cmd(char *cmd, size_t len, const char *const argv[]) {
|
||||||
// Windows command-line parsing is WTF:
|
// Windows command-line parsing is WTF:
|
||||||
|
@ -3,6 +3,13 @@
|
|||||||
|
|
||||||
#include <SDL2/SDL_log.h>
|
#include <SDL2/SDL_log.h>
|
||||||
|
|
||||||
|
enum sc_log_level {
|
||||||
|
SC_LOG_LEVEL_DEBUG,
|
||||||
|
SC_LOG_LEVEL_INFO,
|
||||||
|
SC_LOG_LEVEL_WARN,
|
||||||
|
SC_LOG_LEVEL_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
|
@ -81,6 +81,35 @@ parse_integer(const char *s, long *out) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
parse_integers(const char *s, const char sep, size_t max_items, long *out) {
|
||||||
|
size_t count = 0;
|
||||||
|
char *endptr;
|
||||||
|
do {
|
||||||
|
errno = 0;
|
||||||
|
long value = strtol(s, &endptr, 0);
|
||||||
|
if (errno == ERANGE) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endptr == s || (*endptr != sep && *endptr != '\0')) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
out[count++] = value;
|
||||||
|
if (*endptr == sep) {
|
||||||
|
if (count >= max_items) {
|
||||||
|
// max items already reached, could not accept a new item
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// parse the next token during the next iteration
|
||||||
|
s = endptr + 1;
|
||||||
|
}
|
||||||
|
} while (*endptr != '\0');
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
parse_integer_with_suffix(const char *s, long *out) {
|
parse_integer_with_suffix(const char *s, long *out) {
|
||||||
char *endptr;
|
char *endptr;
|
||||||
|
@ -31,6 +31,11 @@ strquote(const char *src);
|
|||||||
bool
|
bool
|
||||||
parse_integer(const char *s, long *out);
|
parse_integer(const char *s, long *out);
|
||||||
|
|
||||||
|
// parse s as integers separated by sep (for example '1234:2000')
|
||||||
|
// returns the number of integers on success, 0 on failure
|
||||||
|
size_t
|
||||||
|
parse_integers(const char *s, const char sep, size_t max_items, long *out);
|
||||||
|
|
||||||
// parse s as an integer into value
|
// parse s as an integer into value
|
||||||
// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as
|
// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as
|
||||||
// suffix
|
// suffix
|
||||||
|
@ -48,9 +48,10 @@ static void test_options(void) {
|
|||||||
"--fullscreen",
|
"--fullscreen",
|
||||||
"--max-fps", "30",
|
"--max-fps", "30",
|
||||||
"--max-size", "1024",
|
"--max-size", "1024",
|
||||||
|
"--lock-video-orientation", "2",
|
||||||
// "--no-control" is not compatible with "--turn-screen-off"
|
// "--no-control" is not compatible with "--turn-screen-off"
|
||||||
// "--no-display" is not compatible with "--fulscreen"
|
// "--no-display" is not compatible with "--fulscreen"
|
||||||
"--port", "1234",
|
"--port", "1234:1236",
|
||||||
"--push-target", "/sdcard/Movies",
|
"--push-target", "/sdcard/Movies",
|
||||||
"--record", "file",
|
"--record", "file",
|
||||||
"--record-format", "mkv",
|
"--record-format", "mkv",
|
||||||
@ -78,7 +79,9 @@ static void test_options(void) {
|
|||||||
assert(opts->fullscreen);
|
assert(opts->fullscreen);
|
||||||
assert(opts->max_fps == 30);
|
assert(opts->max_fps == 30);
|
||||||
assert(opts->max_size == 1024);
|
assert(opts->max_size == 1024);
|
||||||
assert(opts->port == 1234);
|
assert(opts->lock_video_orientation == 2);
|
||||||
|
assert(opts->port_range.first == 1234);
|
||||||
|
assert(opts->port_range.last == 1236);
|
||||||
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
|
assert(!strcmp(opts->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 == RECORDER_FORMAT_MKV);
|
||||||
|
@ -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)));
|
||||||
@ -49,20 +51,22 @@ static void test_serialize_inject_text(void) {
|
|||||||
static void test_serialize_inject_text_long(void) {
|
static void test_serialize_inject_text_long(void) {
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
||||||
char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1];
|
char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1];
|
||||||
memset(text, 'a', sizeof(text));
|
memset(text, 'a', sizeof(text));
|
||||||
text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0';
|
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
|
||||||
msg.inject_text.text = text;
|
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_TEXT_MAX_LENGTH);
|
assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||||
|
|
||||||
unsigned char expected[3 + CONTROL_MSG_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_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);
|
||||||
|
|
||||||
|
@ -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,30 @@ static void test_deserialize_clipboard(void) {
|
|||||||
device_msg_destroy(&msg);
|
device_msg_destroy(&msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(void) {
|
int main(void) {
|
||||||
test_deserialize_clipboard();
|
test_deserialize_clipboard();
|
||||||
|
test_deserialize_clipboard_big();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -187,6 +187,55 @@ static void test_parse_integer(void) {
|
|||||||
assert(!ok); // out-of-range
|
assert(!ok); // out-of-range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_parse_integers(void) {
|
||||||
|
long values[5];
|
||||||
|
|
||||||
|
size_t count = parse_integers("1234", ':', 5, values);
|
||||||
|
assert(count == 1);
|
||||||
|
assert(values[0] == 1234);
|
||||||
|
|
||||||
|
count = parse_integers("1234:5678", ':', 5, values);
|
||||||
|
assert(count == 2);
|
||||||
|
assert(values[0] == 1234);
|
||||||
|
assert(values[1] == 5678);
|
||||||
|
|
||||||
|
count = parse_integers("1234:5678", ':', 2, values);
|
||||||
|
assert(count == 2);
|
||||||
|
assert(values[0] == 1234);
|
||||||
|
assert(values[1] == 5678);
|
||||||
|
|
||||||
|
count = parse_integers("1234:-5678", ':', 2, values);
|
||||||
|
assert(count == 2);
|
||||||
|
assert(values[0] == 1234);
|
||||||
|
assert(values[1] == -5678);
|
||||||
|
|
||||||
|
count = parse_integers("1:2:3:4:5", ':', 5, values);
|
||||||
|
assert(count == 5);
|
||||||
|
assert(values[0] == 1);
|
||||||
|
assert(values[1] == 2);
|
||||||
|
assert(values[2] == 3);
|
||||||
|
assert(values[3] == 4);
|
||||||
|
assert(values[4] == 5);
|
||||||
|
|
||||||
|
count = parse_integers("1234:5678", ':', 1, values);
|
||||||
|
assert(count == 0); // max_items == 1
|
||||||
|
|
||||||
|
count = parse_integers("1:2:3:4:5", ':', 3, values);
|
||||||
|
assert(count == 0); // max_items == 3
|
||||||
|
|
||||||
|
count = parse_integers(":1234", ':', 5, values);
|
||||||
|
assert(count == 0); // invalid
|
||||||
|
|
||||||
|
count = parse_integers("1234:", ':', 5, values);
|
||||||
|
assert(count == 0); // invalid
|
||||||
|
|
||||||
|
count = parse_integers("1234:", ':', 1, values);
|
||||||
|
assert(count == 0); // invalid, even when max_items == 1
|
||||||
|
|
||||||
|
count = parse_integers("1234::5678", ':', 5, values);
|
||||||
|
assert(count == 0); // invalid
|
||||||
|
}
|
||||||
|
|
||||||
static void test_parse_integer_with_suffix(void) {
|
static void test_parse_integer_with_suffix(void) {
|
||||||
long value;
|
long value;
|
||||||
bool ok = parse_integer_with_suffix("1234", &value);
|
bool ok = parse_integer_with_suffix("1234", &value);
|
||||||
@ -249,6 +298,7 @@ int main(void) {
|
|||||||
test_strquote();
|
test_strquote();
|
||||||
test_utf8_truncate();
|
test_utf8_truncate();
|
||||||
test_parse_integer();
|
test_parse_integer();
|
||||||
|
test_parse_integers();
|
||||||
test_parse_integer_with_suffix();
|
test_parse_integer_with_suffix();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.4.2'
|
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
@ -129,11 +129,6 @@ page at http://checkstyle.sourceforge.net/config.html -->
|
|||||||
</module>
|
</module>
|
||||||
<module name="IllegalInstantiation" />
|
<module name="IllegalInstantiation" />
|
||||||
<module name="InnerAssignment" />
|
<module name="InnerAssignment" />
|
||||||
<module name="MagicNumber">
|
|
||||||
<property name="severity" value="info" />
|
|
||||||
<property name="ignoreHashCodeMethod" value="true" />
|
|
||||||
<property name="ignoreAnnotation" value="true" />
|
|
||||||
</module>
|
|
||||||
<module name="MissingSwitchDefault" />
|
<module name="MissingSwitchDefault" />
|
||||||
<module name="SimplifyBooleanExpression" />
|
<module name="SimplifyBooleanExpression" />
|
||||||
<module name="SimplifyBooleanReturn" />
|
<module name="SimplifyBooleanReturn" />
|
||||||
|
@ -15,6 +15,6 @@ cpu = 'i686'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared'
|
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared'
|
||||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev'
|
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev'
|
||||||
prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32'
|
||||||
|
@ -15,6 +15,6 @@ cpu = 'x86_64'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared'
|
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared'
|
||||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev'
|
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev'
|
||||||
prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32'
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
33
gradlew
vendored
33
gradlew
vendored
@ -125,8 +125,8 @@ if $darwin; then
|
|||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
@ -154,19 +154,19 @@ if $cygwin ; then
|
|||||||
else
|
else
|
||||||
eval `echo args$i`="\"$arg\""
|
eval `echo args$i`="\"$arg\""
|
||||||
fi
|
fi
|
||||||
i=$((i+1))
|
i=`expr $i + 1`
|
||||||
done
|
done
|
||||||
case $i in
|
case $i in
|
||||||
(0) set -- ;;
|
0) set -- ;;
|
||||||
(1) set -- "$args0" ;;
|
1) set -- "$args0" ;;
|
||||||
(2) set -- "$args0" "$args1" ;;
|
2) set -- "$args0" "$args1" ;;
|
||||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -175,14 +175,9 @@ save () {
|
|||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
echo " "
|
echo " "
|
||||||
}
|
}
|
||||||
APP_ARGS=$(save "$@")
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
|
||||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
3
gradlew.bat
vendored
3
gradlew.bat
vendored
@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
project('scrcpy', 'c',
|
project('scrcpy', 'c',
|
||||||
version: '1.12.1',
|
version: '1.14',
|
||||||
meson_version: '>= 0.37',
|
meson_version: '>= 0.48',
|
||||||
default_options: [
|
default_options: [
|
||||||
'c_std=c11',
|
'c_std=c11',
|
||||||
'warning_level=2',
|
'warning_level=2',
|
||||||
|
@ -6,3 +6,4 @@ option('prebuilt_server', type: 'string', description: 'Path of the prebuilt ser
|
|||||||
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
||||||
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
||||||
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
|
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
|
||||||
|
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
|
||||||
|
@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
|
|||||||
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
|
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
|
||||||
|
|
||||||
prepare-ffmpeg-shared-win32:
|
prepare-ffmpeg-shared-win32:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \
|
||||||
9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \
|
ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \
|
||||||
ffmpeg-4.2.1-win32-shared
|
ffmpeg-4.2.2-win32-shared
|
||||||
|
|
||||||
prepare-ffmpeg-dev-win32:
|
prepare-ffmpeg-dev-win32:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \
|
||||||
c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \
|
8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \
|
||||||
ffmpeg-4.2.1-win32-dev
|
ffmpeg-4.2.2-win32-dev
|
||||||
|
|
||||||
prepare-ffmpeg-shared-win64:
|
prepare-ffmpeg-shared-win64:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \
|
||||||
55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \
|
5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \
|
||||||
ffmpeg-4.2.1-win64-shared
|
ffmpeg-4.2.2-win64-shared
|
||||||
|
|
||||||
prepare-ffmpeg-dev-win64:
|
prepare-ffmpeg-dev-win64:
|
||||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \
|
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \
|
||||||
5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \
|
f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \
|
||||||
ffmpeg-4.2.1-win64-dev
|
ffmpeg-4.2.2-win64-dev
|
||||||
|
|
||||||
prepare-sdl2:
|
prepare-sdl2:
|
||||||
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
|
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \
|
||||||
a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \
|
e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \
|
||||||
SDL2-2.0.10
|
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.0-windows.zip \
|
||||||
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
|
854305f9a702f5ea2c3de73edde402bd26afa0ee944c9b0c4380420f5a862e0d \
|
||||||
platform-tools
|
platform-tools
|
||||||
|
@ -6,8 +6,8 @@ android {
|
|||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 14
|
versionCode 16
|
||||||
versionName "1.12.1"
|
versionName "1.14"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRCPY_DEBUG=false
|
SCRCPY_DEBUG=false
|
||||||
SCRCPY_VERSION_NAME=1.12.1
|
SCRCPY_VERSION_NAME=1.14
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-29}
|
PLATFORM=${ANDROID_PLATFORM:-29}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
||||||
|
@ -3,7 +3,9 @@
|
|||||||
prebuilt_server = get_option('prebuilt_server')
|
prebuilt_server = get_option('prebuilt_server')
|
||||||
if prebuilt_server == ''
|
if prebuilt_server == ''
|
||||||
custom_target('scrcpy-server',
|
custom_target('scrcpy-server',
|
||||||
build_always: true, # gradle is responsible for tracking source changes
|
# gradle is responsible for tracking source changes
|
||||||
|
build_by_default: true,
|
||||||
|
build_always_stale: true,
|
||||||
output: 'scrcpy-server',
|
output: 'scrcpy-server',
|
||||||
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
|
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
|
||||||
console: true,
|
console: true,
|
||||||
|
@ -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();
|
||||||
|
}
|
77
server/src/main/java/com/genymobile/scrcpy/CleanUp.java
Normal file
77
server/src/main/java/com/genymobile/scrcpy/CleanUp.java
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the cleanup of scrcpy, even if the main process is killed.
|
||||||
|
* <p>
|
||||||
|
* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process).
|
||||||
|
*/
|
||||||
|
public final class CleanUp {
|
||||||
|
|
||||||
|
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
||||||
|
|
||||||
|
private CleanUp() {
|
||||||
|
// not instantiable
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException {
|
||||||
|
boolean needProcess = disableShowTouches || restoreStayOn != -1;
|
||||||
|
if (needProcess) {
|
||||||
|
startProcess(disableShowTouches, restoreStayOn);
|
||||||
|
} else {
|
||||||
|
// There is no additional clean up to do when scrcpy dies
|
||||||
|
unlinkSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException {
|
||||||
|
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)};
|
||||||
|
|
||||||
|
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||||
|
builder.environment().put("CLASSPATH", SERVER_PATH);
|
||||||
|
builder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void unlinkSelf() {
|
||||||
|
try {
|
||||||
|
new File(SERVER_PATH).delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Ln.e("Could not unlink server", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String... args) {
|
||||||
|
unlinkSelf();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Wait for the server to die
|
||||||
|
System.in.read();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Expected when the server is dead
|
||||||
|
}
|
||||||
|
|
||||||
|
Ln.i("Cleaning up");
|
||||||
|
|
||||||
|
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
|
||||||
|
int restoreStayOn = Integer.parseInt(args[1]);
|
||||||
|
|
||||||
|
if (disableShowTouches || restoreStayOn != -1) {
|
||||||
|
ServiceManager serviceManager = new ServiceManager();
|
||||||
|
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||||
|
if (disableShowTouches) {
|
||||||
|
Ln.i("Disabling \"show touches\"");
|
||||||
|
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
|
||||||
|
}
|
||||||
|
if (restoreStayOn != -1) {
|
||||||
|
Ln.i("Restoring \"stay awake\"");
|
||||||
|
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,8 @@ public final class ControlMessage {
|
|||||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
||||||
public static final int TYPE_ROTATE_DEVICE = 10;
|
public static final int TYPE_ROTATE_DEVICE = 10;
|
||||||
|
|
||||||
|
public static final int FLAGS_PASTE = 1;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
private int metaState; // KeyEvent.META_*
|
private int metaState; // KeyEvent.META_*
|
||||||
@ -28,15 +30,18 @@ public final class ControlMessage {
|
|||||||
private Position position;
|
private Position position;
|
||||||
private int hScroll;
|
private int hScroll;
|
||||||
private int vScroll;
|
private int vScroll;
|
||||||
|
private int flags;
|
||||||
|
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 +73,13 @@ 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;
|
||||||
|
if (paste) {
|
||||||
|
msg.flags = FLAGS_PASTE;
|
||||||
|
}
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,4 +142,12 @@ public final class ControlMessage {
|
|||||||
public int getVScroll() {
|
public int getVScroll() {
|
||||||
return vScroll;
|
return vScroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFlags() {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRepeat() {
|
||||||
|
return repeat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,19 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
public class ControlMessageReader {
|
public class ControlMessageReader {
|
||||||
|
|
||||||
private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
|
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||||
private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17;
|
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||||
private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21;
|
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||||
private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||||
private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
|
||||||
|
|
||||||
public static final int TEXT_MAX_LENGTH = 300;
|
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
|
||||||
private static final int RAW_BUFFER_SIZE = 1024;
|
|
||||||
|
|
||||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
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;
|
||||||
|
|
||||||
|
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
|
||||||
@ -98,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() {
|
||||||
@ -122,7 +125,6 @@ public class ControlMessageReader {
|
|||||||
return ControlMessage.createInjectText(text);
|
return ControlMessage.createInjectText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private ControlMessage parseInjectTouchEvent() {
|
private ControlMessage parseInjectTouchEvent() {
|
||||||
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
|
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
|
||||||
return null;
|
return null;
|
||||||
@ -149,11 +151,15 @@ public class ControlMessageReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetClipboard() {
|
private ControlMessage parseSetClipboard() {
|
||||||
|
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean parse = 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, parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetScreenPowerMode() {
|
private ControlMessage parseSetScreenPowerMode() {
|
||||||
@ -172,12 +178,10 @@ public class ControlMessageReader {
|
|||||||
return new Position(x, y, screenWidth, screenHeight);
|
return new Position(x, y, screenWidth, screenHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private static int toUnsigned(short value) {
|
private static int toUnsigned(short value) {
|
||||||
return value & 0xffff;
|
return value & 0xffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private static int toUnsigned(byte value) {
|
private static int toUnsigned(byte value) {
|
||||||
return value & 0xff;
|
return value & 0xff;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
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;
|
||||||
@ -47,11 +45,10 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
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_POWER);
|
||||||
|
|
||||||
// dirty hack
|
// dirty hack
|
||||||
// After POWER is injected, the device is powered on asynchronously.
|
// After POWER is injected, the device is powered on asynchronously.
|
||||||
@ -76,19 +73,29 @@ public class Controller {
|
|||||||
ControlMessage msg = connection.receiveControlMessage();
|
ControlMessage msg = connection.receiveControlMessage();
|
||||||
switch (msg.getType()) {
|
switch (msg.getType()) {
|
||||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
|
if (device.supportsInputEvents()) {
|
||||||
|
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_TEXT:
|
case ControlMessage.TYPE_INJECT_TEXT:
|
||||||
injectText(msg.getText());
|
if (device.supportsInputEvents()) {
|
||||||
|
injectText(msg.getText());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
if (device.supportsInputEvents()) {
|
||||||
|
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
|
if (device.supportsInputEvents()) {
|
||||||
|
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||||
pressBackOrTurnScreenOn();
|
if (device.supportsInputEvents()) {
|
||||||
|
pressBackOrTurnScreenOn();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
device.expandNotificationPanel();
|
device.expandNotificationPanel();
|
||||||
@ -98,13 +105,22 @@ public class Controller {
|
|||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
String clipboardText = device.getClipboardText();
|
String clipboardText = device.getClipboardText();
|
||||||
sender.pushClipboardText(clipboardText);
|
if (clipboardText != null) {
|
||||||
|
sender.pushClipboardText(clipboardText);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
device.setClipboardText(msg.getText());
|
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
||||||
|
setClipboard(msg.getText(), paste);
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
device.setScreenPowerMode(msg.getAction());
|
if (device.supportsInputEvents()) {
|
||||||
|
int mode = msg.getAction();
|
||||||
|
boolean setPowerModeOk = device.setScreenPowerMode(mode);
|
||||||
|
if (setPowerModeOk) {
|
||||||
|
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
device.rotateDevice();
|
device.rotateDevice();
|
||||||
@ -114,8 +130,8 @@ 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);
|
return device.injectKeyEvent(action, keycode, repeat, metaState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectChar(char c) {
|
private boolean injectChar(char c) {
|
||||||
@ -126,7 +142,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,7 +198,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) {
|
||||||
@ -204,27 +220,26 @@ 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_MOUSE, 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();
|
|
||||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
|
||||||
InputDevice.SOURCE_KEYBOARD);
|
|
||||||
return injectEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
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_POWER;
|
||||||
return injectKeycode(keycode);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable {
|
|||||||
controlSocket.close();
|
controlSocket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private void send(String deviceName, int width, int height) throws IOException {
|
private void send(String deviceName, int width, int height) throws IOException {
|
||||||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||||
|
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||||
|
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.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 {
|
||||||
|
|
||||||
@ -20,18 +28,47 @@ 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
|
||||||
|
*/
|
||||||
|
private final int displayId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The surface flinger layer stack associated with this logical display
|
||||||
|
*/
|
||||||
|
private final int layerStack;
|
||||||
|
|
||||||
|
private final boolean supportsInputEvents;
|
||||||
|
|
||||||
public Device(Options options) {
|
public Device(Options options) {
|
||||||
screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize());
|
displayId = options.getDisplayId();
|
||||||
registerRotationWatcher(new IRotationWatcher.Stub() {
|
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||||
|
if (displayInfo == null) {
|
||||||
|
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
|
||||||
|
throw new InvalidDisplayIdException(displayId, displayIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
int displayInfoFlags = displayInfo.getFlags();
|
||||||
|
|
||||||
|
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
|
||||||
|
layerStack = displayInfo.getLayerStack();
|
||||||
|
|
||||||
|
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.withRotation(rotation);
|
screenInfo = screenInfo.withDeviceRotation(rotation);
|
||||||
|
|
||||||
// notify
|
// notify
|
||||||
if (rotationListener != null) {
|
if (rotationListener != null) {
|
||||||
@ -39,104 +76,120 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}, displayId);
|
||||||
|
|
||||||
|
if (options.getControl()) {
|
||||||
|
// If control is enabled, synchronize Android clipboard to the computer automatically
|
||||||
|
serviceManager.getClipboardManager().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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||||
|
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||||
|
}
|
||||||
|
|
||||||
|
// main display or any display on Android >= Q
|
||||||
|
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||||
|
if (!supportsInputEvents) {
|
||||||
|
Ln.w("Input events are not supported for secondary displays before Android 10");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized ScreenInfo getScreenInfo() {
|
public synchronized ScreenInfo getScreenInfo() {
|
||||||
return screenInfo;
|
return screenInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ScreenInfo computeScreenInfo(Rect crop, int maxSize) {
|
public int getLayerStack() {
|
||||||
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
|
return layerStack;
|
||||||
boolean rotated = (displayInfo.getRotation() & 1) != 0;
|
|
||||||
Size deviceSize = displayInfo.getSize();
|
|
||||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
|
||||||
if (crop != null) {
|
|
||||||
if (rotated) {
|
|
||||||
// the crop (provided by the user) is expressed in the natural orientation
|
|
||||||
crop = flipRect(crop);
|
|
||||||
}
|
|
||||||
if (!contentRect.intersect(crop)) {
|
|
||||||
// intersect() changes contentRect so that it is intersected with crop
|
|
||||||
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
|
|
||||||
contentRect = new Rect(); // empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
|
|
||||||
return new ScreenInfo(contentRect, videoSize, rotated);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatCrop(Rect rect) {
|
|
||||||
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private static Size computeVideoSize(int w, int h, int maxSize) {
|
|
||||||
// Compute the video size and the padding of the content inside this video.
|
|
||||||
// Principle:
|
|
||||||
// - scale down the great side of the screen to maxSize (if necessary);
|
|
||||||
// - scale down the other side so that the aspect ratio is preserved;
|
|
||||||
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
|
|
||||||
w &= ~7; // in case it's not a multiple of 8
|
|
||||||
h &= ~7;
|
|
||||||
if (maxSize > 0) {
|
|
||||||
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
|
||||||
throw new AssertionError("Max size must be a multiple of 8");
|
|
||||||
}
|
|
||||||
boolean portrait = h > w;
|
|
||||||
int major = portrait ? h : w;
|
|
||||||
int minor = portrait ? w : h;
|
|
||||||
if (major > maxSize) {
|
|
||||||
int minorExact = minor * maxSize / major;
|
|
||||||
// +4 to round the value to the nearest multiple of 8
|
|
||||||
minor = (minorExact + 4) & ~7;
|
|
||||||
major = maxSize;
|
|
||||||
}
|
|
||||||
w = portrait ? minor : major;
|
|
||||||
h = portrait ? major : minor;
|
|
||||||
}
|
|
||||||
return new Size(w, h);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Point getPhysicalPoint(Position position) {
|
public Point getPhysicalPoint(Position position) {
|
||||||
// it hides the field on purpose, to read it with a lock
|
// it hides the field on purpose, to read it with a lock
|
||||||
@SuppressWarnings("checkstyle:HiddenField")
|
@SuppressWarnings("checkstyle:HiddenField")
|
||||||
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
|
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
|
||||||
Size videoSize = screenInfo.getVideoSize();
|
|
||||||
Size clientVideoSize = position.getScreenSize();
|
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
|
||||||
if (!videoSize.equals(clientVideoSize)) {
|
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
|
||||||
|
|
||||||
|
int reverseVideoRotation = screenInfo.getReverseVideoRotation();
|
||||||
|
// reverse the video rotation to apply the events
|
||||||
|
Position devicePosition = position.rotate(reverseVideoRotation);
|
||||||
|
|
||||||
|
Size clientVideoSize = devicePosition.getScreenSize();
|
||||||
|
if (!unlockedVideoSize.equals(clientVideoSize)) {
|
||||||
// The client sends a click relative to a video with wrong dimensions,
|
// The client sends a click relative to a video with wrong dimensions,
|
||||||
// the device may have been rotated since the event was generated, so ignore the event
|
// the device may have been rotated since the event was generated, so ignore the event
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Rect contentRect = screenInfo.getContentRect();
|
Rect contentRect = screenInfo.getContentRect();
|
||||||
Point point = position.getPoint();
|
Point point = devicePosition.getPoint();
|
||||||
int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth();
|
int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth();
|
||||||
int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight();
|
int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight();
|
||||||
return new Point(scaledX, scaledY);
|
return new Point(convertedX, convertedY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getDeviceName() {
|
public static String getDeviceName() {
|
||||||
return Build.MODEL;
|
return Build.MODEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
public boolean supportsInputEvents() {
|
||||||
|
return supportsInputEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectEvent(InputEvent inputEvent, int mode) {
|
||||||
|
if (!supportsInputEvents()) {
|
||||||
|
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
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) {
|
|
||||||
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
@ -153,22 +206,23 @@ public final class Device {
|
|||||||
return s.toString();
|
return s.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClipboardText(String text) {
|
public boolean setClipboardText(String text) {
|
||||||
serviceManager.getClipboardManager().setText(text);
|
isSettingClipboard.set(true);
|
||||||
Ln.i("Device clipboard set");
|
boolean ok = serviceManager.getClipboardManager().setText(text);
|
||||||
|
isSettingClipboard.set(false);
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public void setScreenPowerMode(int mode) {
|
public 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;
|
||||||
}
|
}
|
||||||
SurfaceControl.setDisplayPowerMode(d, mode);
|
return SurfaceControl.setDisplayPowerMode(d, mode);
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -192,7 +246,7 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Rect flipRect(Rect crop) {
|
public ContentProvider createSettingsProvider() {
|
||||||
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
return serviceManager.getActivityManager().createSettingsProvider();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,12 @@ 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);
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);
|
buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD);
|
||||||
@ -22,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;
|
||||||
|
@ -1,12 +1,24 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
public final class DisplayInfo {
|
public final class DisplayInfo {
|
||||||
|
private final int displayId;
|
||||||
private final Size size;
|
private final Size size;
|
||||||
private final int rotation;
|
private final int rotation;
|
||||||
|
private final int layerStack;
|
||||||
|
private final int flags;
|
||||||
|
|
||||||
public DisplayInfo(Size size, int rotation) {
|
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
|
||||||
|
|
||||||
|
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
|
||||||
|
this.displayId = displayId;
|
||||||
this.size = size;
|
this.size = size;
|
||||||
this.rotation = rotation;
|
this.rotation = rotation;
|
||||||
|
this.layerStack = layerStack;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDisplayId() {
|
||||||
|
return displayId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Size getSize() {
|
public Size getSize() {
|
||||||
@ -16,5 +28,13 @@ public final class DisplayInfo {
|
|||||||
public int getRotation() {
|
public int getRotation() {
|
||||||
return rotation;
|
return rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getLayerStack() {
|
||||||
|
return layerStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFlags() {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
public class InvalidDisplayIdException extends RuntimeException {
|
||||||
|
|
||||||
|
private final int displayId;
|
||||||
|
private final int[] availableDisplayIds;
|
||||||
|
|
||||||
|
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
|
||||||
|
super("There is no display having id " + displayId);
|
||||||
|
this.displayId = displayId;
|
||||||
|
this.availableDisplayIds = availableDisplayIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDisplayId() {
|
||||||
|
return displayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getAvailableDisplayIds() {
|
||||||
|
return availableDisplayIds;
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -3,13 +3,27 @@ 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;
|
||||||
|
private int lockedVideoOrientation;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
private Rect crop;
|
private Rect crop;
|
||||||
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
||||||
private boolean control;
|
private boolean control;
|
||||||
|
private int displayId;
|
||||||
|
private boolean showTouches;
|
||||||
|
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;
|
||||||
@ -35,6 +49,14 @@ public class Options {
|
|||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getLockedVideoOrientation() {
|
||||||
|
return lockedVideoOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLockedVideoOrientation(int lockedVideoOrientation) {
|
||||||
|
this.lockedVideoOrientation = lockedVideoOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTunnelForward() {
|
public boolean isTunnelForward() {
|
||||||
return tunnelForward;
|
return tunnelForward;
|
||||||
}
|
}
|
||||||
@ -66,4 +88,36 @@ public class Options {
|
|||||||
public void setControl(boolean control) {
|
public void setControl(boolean control) {
|
||||||
this.control = control;
|
this.control = control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getDisplayId() {
|
||||||
|
return displayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayId(int displayId) {
|
||||||
|
this.displayId = displayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getShowTouches() {
|
||||||
|
return showTouches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShowTouches(boolean showTouches) {
|
||||||
|
this.showTouches = showTouches;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getStayAwake() {
|
||||||
|
return stayAwake;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStayAwake(boolean stayAwake) {
|
||||||
|
this.stayAwake = stayAwake;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCodecOptions() {
|
||||||
|
return codecOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCodecOptions(String codecOptions) {
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,19 @@ public class Position {
|
|||||||
return screenSize;
|
return screenSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Position rotate(int rotation) {
|
||||||
|
switch (rotation) {
|
||||||
|
case 1:
|
||||||
|
return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate());
|
||||||
|
case 2:
|
||||||
|
return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize);
|
||||||
|
case 3:
|
||||||
|
return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate());
|
||||||
|
default:
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) {
|
if (this == o) {
|
||||||
|
@ -6,13 +6,13 @@ import android.graphics.Rect;
|
|||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.Surface;
|
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 {
|
||||||
@ -26,21 +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 int iFrameInterval;
|
|
||||||
private boolean sendFrameMeta;
|
private boolean sendFrameMeta;
|
||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
|
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.iFrameInterval = iFrameInterval;
|
this.codecOptions = codecOptions;
|
||||||
}
|
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
|
||||||
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -54,21 +50,40 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
|
|
||||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
Workarounds.prepareMainLooper();
|
Workarounds.prepareMainLooper();
|
||||||
Workarounds.fillAppInfo();
|
|
||||||
|
|
||||||
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
|
try {
|
||||||
|
internalStreamScreen(device, fd);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
// Retry with workarounds enabled:
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/365>
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/940>
|
||||||
|
Ln.d("Applying workarounds to avoid NullPointerException");
|
||||||
|
Workarounds.fillAppInfo();
|
||||||
|
internalStreamScreen(device, fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
|
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
MediaCodec codec = createCodec();
|
MediaCodec codec = createCodec();
|
||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
Rect contentRect = device.getScreenInfo().getContentRect();
|
ScreenInfo screenInfo = device.getScreenInfo();
|
||||||
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
|
Rect contentRect = screenInfo.getContentRect();
|
||||||
|
// include the locked video orientation
|
||||||
|
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||||
|
// does not include the locked video orientation
|
||||||
|
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||||
|
int videoRotation = screenInfo.getVideoRotation();
|
||||||
|
int layerStack = device.getLayerStack();
|
||||||
|
|
||||||
setSize(format, videoRect.width(), videoRect.height());
|
setSize(format, videoRect.width(), videoRect.height());
|
||||||
configure(codec, format);
|
configure(codec, format);
|
||||||
Surface surface = codec.createInputSurface();
|
Surface surface = codec.createInputSurface();
|
||||||
setDisplaySurface(display, surface, contentRect, videoRect);
|
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||||
codec.start();
|
codec.start();
|
||||||
try {
|
try {
|
||||||
alive = encode(codec, fd);
|
alive = encode(codec, fd);
|
||||||
@ -136,18 +151,34 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static MediaCodec createCodec() throws IOException {
|
private static MediaCodec createCodec() throws IOException {
|
||||||
return MediaCodec.createEncoderByType("video/avc");
|
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||||
private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
|
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, "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);
|
||||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
|
||||||
// display the very first frame, and recover from bad quality when no new frames
|
// display the very first frame, and recover from bad quality when no new frames
|
||||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
||||||
if (maxFps > 0) {
|
if (maxFps > 0) {
|
||||||
@ -156,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,12 +210,12 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
format.setInteger(MediaFormat.KEY_HEIGHT, height);
|
format.setInteger(MediaFormat.KEY_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) {
|
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
|
||||||
SurfaceControl.openTransaction();
|
SurfaceControl.openTransaction();
|
||||||
try {
|
try {
|
||||||
SurfaceControl.setDisplaySurface(display, surface);
|
SurfaceControl.setDisplaySurface(display, surface);
|
||||||
SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
|
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
|
||||||
SurfaceControl.setDisplayLayerStack(display, 0);
|
SurfaceControl.setDisplayLayerStack(display, layerStack);
|
||||||
} finally {
|
} finally {
|
||||||
SurfaceControl.closeTransaction();
|
SurfaceControl.closeTransaction();
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,161 @@ package com.genymobile.scrcpy;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
|
||||||
public final class ScreenInfo {
|
public final class ScreenInfo {
|
||||||
|
/**
|
||||||
|
* Device (physical) size, possibly cropped
|
||||||
|
*/
|
||||||
private final Rect contentRect; // device size, possibly cropped
|
private final Rect contentRect; // device size, possibly cropped
|
||||||
private final Size videoSize;
|
|
||||||
private final boolean rotated;
|
|
||||||
|
|
||||||
public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) {
|
/**
|
||||||
|
* Video size, possibly smaller than the device size, already taking the device rotation and crop into account.
|
||||||
|
* <p>
|
||||||
|
* However, it does not include the locked video orientation.
|
||||||
|
*/
|
||||||
|
private final Size unlockedVideoSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device rotation, related to the natural device orientation (0, 1, 2 or 3)
|
||||||
|
*/
|
||||||
|
private final int deviceRotation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW)
|
||||||
|
*/
|
||||||
|
private final int lockedVideoOrientation;
|
||||||
|
|
||||||
|
public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) {
|
||||||
this.contentRect = contentRect;
|
this.contentRect = contentRect;
|
||||||
this.videoSize = videoSize;
|
this.unlockedVideoSize = unlockedVideoSize;
|
||||||
this.rotated = rotated;
|
this.deviceRotation = deviceRotation;
|
||||||
|
this.lockedVideoOrientation = lockedVideoOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Rect getContentRect() {
|
public Rect getContentRect() {
|
||||||
return contentRect;
|
return contentRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Size getVideoSize() {
|
/**
|
||||||
return videoSize;
|
* Return the video size as if locked video orientation was not set.
|
||||||
|
*
|
||||||
|
* @return the unlocked video size
|
||||||
|
*/
|
||||||
|
public Size getUnlockedVideoSize() {
|
||||||
|
return unlockedVideoSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScreenInfo withRotation(int rotation) {
|
/**
|
||||||
boolean newRotated = (rotation & 1) != 0;
|
* Return the actual video size if locked video orientation is set.
|
||||||
if (rotated == newRotated) {
|
*
|
||||||
|
* @return the actual video size
|
||||||
|
*/
|
||||||
|
public Size getVideoSize() {
|
||||||
|
if (getVideoRotation() % 2 == 0) {
|
||||||
|
return unlockedVideoSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockedVideoSize.rotate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDeviceRotation() {
|
||||||
|
return deviceRotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScreenInfo withDeviceRotation(int newDeviceRotation) {
|
||||||
|
if (newDeviceRotation == deviceRotation) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated);
|
// true if changed between portrait and landscape
|
||||||
|
boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0;
|
||||||
|
Rect newContentRect;
|
||||||
|
Size newUnlockedVideoSize;
|
||||||
|
if (orientationChanged) {
|
||||||
|
newContentRect = flipRect(contentRect);
|
||||||
|
newUnlockedVideoSize = unlockedVideoSize.rotate();
|
||||||
|
} else {
|
||||||
|
newContentRect = contentRect;
|
||||||
|
newUnlockedVideoSize = unlockedVideoSize;
|
||||||
|
}
|
||||||
|
return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||||
|
int rotation = displayInfo.getRotation();
|
||||||
|
Size deviceSize = displayInfo.getSize();
|
||||||
|
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||||
|
if (crop != null) {
|
||||||
|
if (rotation % 2 != 0) { // 180s preserve dimensions
|
||||||
|
// the crop (provided by the user) is expressed in the natural orientation
|
||||||
|
crop = flipRect(crop);
|
||||||
|
}
|
||||||
|
if (!contentRect.intersect(crop)) {
|
||||||
|
// intersect() changes contentRect so that it is intersected with crop
|
||||||
|
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
|
||||||
|
contentRect = new Rect(); // empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
|
||||||
|
return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatCrop(Rect rect) {
|
||||||
|
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Size computeVideoSize(int w, int h, int maxSize) {
|
||||||
|
// Compute the video size and the padding of the content inside this video.
|
||||||
|
// Principle:
|
||||||
|
// - scale down the great side of the screen to maxSize (if necessary);
|
||||||
|
// - scale down the other side so that the aspect ratio is preserved;
|
||||||
|
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
|
||||||
|
w &= ~7; // in case it's not a multiple of 8
|
||||||
|
h &= ~7;
|
||||||
|
if (maxSize > 0) {
|
||||||
|
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
|
||||||
|
throw new AssertionError("Max size must be a multiple of 8");
|
||||||
|
}
|
||||||
|
boolean portrait = h > w;
|
||||||
|
int major = portrait ? h : w;
|
||||||
|
int minor = portrait ? w : h;
|
||||||
|
if (major > maxSize) {
|
||||||
|
int minorExact = minor * maxSize / major;
|
||||||
|
// +4 to round the value to the nearest multiple of 8
|
||||||
|
minor = (minorExact + 4) & ~7;
|
||||||
|
major = maxSize;
|
||||||
|
}
|
||||||
|
w = portrait ? minor : major;
|
||||||
|
h = portrait ? major : minor;
|
||||||
|
}
|
||||||
|
return new Size(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rect flipRect(Rect crop) {
|
||||||
|
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the rotation to apply to the device rotation to get the requested locked video orientation
|
||||||
|
*
|
||||||
|
* @return the rotation offset
|
||||||
|
*/
|
||||||
|
public int getVideoRotation() {
|
||||||
|
if (lockedVideoOrientation == -1) {
|
||||||
|
// no offset
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the rotation to apply to the requested locked video orientation to get the device rotation
|
||||||
|
*
|
||||||
|
* @return the (reverse) rotation offset
|
||||||
|
*/
|
||||||
|
public int getReverseVideoRotation() {
|
||||||
|
if (lockedVideoOrientation == -1) {
|
||||||
|
// no offset
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,74 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
|
import android.os.BatteryManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
|
|
||||||
private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
|
|
||||||
|
|
||||||
private Server() {
|
private Server() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
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 + ")");
|
||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
|
||||||
|
|
||||||
|
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||||
|
int restoreStayOn = -1;
|
||||||
|
if (options.getShowTouches() || options.getStayAwake()) {
|
||||||
|
try (ContentProvider settings = device.createSettingsProvider()) {
|
||||||
|
if (options.getShowTouches()) {
|
||||||
|
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
|
||||||
|
// If "show touches" was disabled, it must be disabled back on clean up
|
||||||
|
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.getStayAwake()) {
|
||||||
|
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||||
|
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||||
|
try {
|
||||||
|
restoreStayOn = Integer.parseInt(oldValue);
|
||||||
|
if (restoreStayOn == stayOn) {
|
||||||
|
// No need to restore
|
||||||
|
restoreStayOn = -1;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
restoreStayOn = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
||||||
|
|
||||||
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 {
|
||||||
@ -67,7 +109,6 @@ public final class Server {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private static Options createOptions(String... args) {
|
private static Options createOptions(String... args) {
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
throw new IllegalArgumentException("Missing client version");
|
throw new IllegalArgumentException("Missing client version");
|
||||||
@ -76,41 +117,59 @@ public final class Server {
|
|||||||
String clientVersion = args[0];
|
String clientVersion = args[0];
|
||||||
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
|
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.length != 8) {
|
final int expectedParameters = 14;
|
||||||
throw new IllegalArgumentException("Expecting 8 parameters");
|
if (args.length != expectedParameters) {
|
||||||
|
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[5]);
|
||||||
|
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[4]);
|
boolean tunnelForward = Boolean.parseBoolean(args[6]);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
|
||||||
Rect crop = parseCrop(args[5]);
|
Rect crop = parseCrop(args[7]);
|
||||||
options.setCrop(crop);
|
options.setCrop(crop);
|
||||||
|
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[6]);
|
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
|
|
||||||
boolean control = Boolean.parseBoolean(args[7]);
|
boolean control = Boolean.parseBoolean(args[9]);
|
||||||
options.setControl(control);
|
options.setControl(control);
|
||||||
|
|
||||||
|
int displayId = Integer.parseInt(args[10]);
|
||||||
|
options.setDisplayId(displayId);
|
||||||
|
|
||||||
|
boolean showTouches = Boolean.parseBoolean(args[11]);
|
||||||
|
options.setShowTouches(showTouches);
|
||||||
|
|
||||||
|
boolean stayAwake = Boolean.parseBoolean(args[12]);
|
||||||
|
options.setStayAwake(stayAwake);
|
||||||
|
|
||||||
|
String codecOptions = args[13];
|
||||||
|
options.setCodecOptions(codecOptions);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private static Rect parseCrop(String crop) {
|
private static Rect parseCrop(String crop) {
|
||||||
if ("-".equals(crop)) {
|
if ("-".equals(crop)) {
|
||||||
return null;
|
return null;
|
||||||
@ -127,15 +186,6 @@ public final class Server {
|
|||||||
return new Rect(x, y, x + width, y + height);
|
return new Rect(x, y, x + width, y + height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void unlinkSelf() {
|
|
||||||
try {
|
|
||||||
new File(SERVER_PATH).delete();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Ln.e("Could not unlink server", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
private static void suggestFix(Throwable e) {
|
private static void suggestFix(Throwable e) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
if (e instanceof MediaCodec.CodecException) {
|
if (e instanceof MediaCodec.CodecException) {
|
||||||
@ -147,6 +197,16 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (e instanceof InvalidDisplayIdException) {
|
||||||
|
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
|
||||||
|
int[] displayIds = idie.getAvailableDisplayIds();
|
||||||
|
if (displayIds != null && displayIds.length > 0) {
|
||||||
|
Ln.e("Try to use one of the available display ids:");
|
||||||
|
for (int id : displayIds) {
|
||||||
|
Ln.e(" scrcpy --display " + id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String... args) throws Exception {
|
public static void main(String... args) throws Exception {
|
||||||
@ -158,8 +218,10 @@ public final class Server {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
unlinkSelf();
|
|
||||||
Options options = createOptions(args);
|
Options options = createOptions(args);
|
||||||
|
|
||||||
|
Ln.initLogLevel(options.getLogLevel());
|
||||||
|
|
||||||
scrcpy(options);
|
scrcpy(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ public final class StringUtils {
|
|||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
|
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
|
||||||
int len = utf8.length;
|
int len = utf8.length;
|
||||||
if (len <= maxLength) {
|
if (len <= maxLength) {
|
||||||
|
@ -28,7 +28,7 @@ public final class Workarounds {
|
|||||||
Looper.prepareMainLooper();
|
Looper.prepareMainLooper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public static void fillAppInfo() {
|
public static void fillAppInfo() {
|
||||||
try {
|
try {
|
||||||
// ActivityThread activityThread = new ActivityThread();
|
// ActivityThread activityThread = new ActivityThread();
|
||||||
@ -73,7 +73,7 @@ public final class Workarounds {
|
|||||||
mInitialApplicationField.set(activityThread, app);
|
mInitialApplicationField.set(activityThread, app);
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
// this is a workaround, so failing is not an error
|
// this is a workaround, so failing is not an error
|
||||||
Ln.w("Could not fill app info: " + throwable.getMessage());
|
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class ActivityManager {
|
||||||
|
|
||||||
|
private final IInterface manager;
|
||||||
|
private Method getContentProviderExternalMethod;
|
||||||
|
private boolean getContentProviderExternalMethodLegacy;
|
||||||
|
private Method removeContentProviderExternalMethod;
|
||||||
|
|
||||||
|
public ActivityManager(IInterface manager) {
|
||||||
|
this.manager = manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getGetContentProviderExternalMethod() throws NoSuchMethodException {
|
||||||
|
if (getContentProviderExternalMethod == null) {
|
||||||
|
try {
|
||||||
|
getContentProviderExternalMethod = manager.getClass()
|
||||||
|
.getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// old version
|
||||||
|
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
|
||||||
|
getContentProviderExternalMethodLegacy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getContentProviderExternalMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException {
|
||||||
|
if (removeContentProviderExternalMethod == null) {
|
||||||
|
removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class);
|
||||||
|
}
|
||||||
|
return removeContentProviderExternalMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContentProvider getContentProviderExternal(String name, IBinder token) {
|
||||||
|
try {
|
||||||
|
Method method = getGetContentProviderExternalMethod();
|
||||||
|
Object[] args;
|
||||||
|
if (!getContentProviderExternalMethodLegacy) {
|
||||||
|
// new version
|
||||||
|
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
||||||
|
} else {
|
||||||
|
// old version
|
||||||
|
args = new Object[]{name, ServiceManager.USER_ID, token};
|
||||||
|
}
|
||||||
|
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
||||||
|
Object providerHolder = method.invoke(manager, args);
|
||||||
|
if (providerHolder == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// IContentProvider provider = providerHolder.provider;
|
||||||
|
Field providerField = providerHolder.getClass().getDeclaredField("provider");
|
||||||
|
providerField.setAccessible(true);
|
||||||
|
Object provider = providerField.get(providerHolder);
|
||||||
|
if (provider == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new ContentProvider(this, provider, name, token);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeContentProviderExternal(String name, IBinder token) {
|
||||||
|
try {
|
||||||
|
Method method = getRemoveContentProviderExternalMethod();
|
||||||
|
method.invoke(manager, name, token);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentProvider createSettingsProvider() {
|
||||||
|
return getContentProviderExternal("settings", new Binder());
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
@ -10,13 +11,10 @@ import java.lang.reflect.InvocationTargetException;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
public class ClipboardManager {
|
public class ClipboardManager {
|
||||||
|
|
||||||
private static final String PACKAGE_NAME = "com.android.shell";
|
|
||||||
private static final int USER_ID = 0;
|
|
||||||
|
|
||||||
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;
|
||||||
@ -46,17 +44,17 @@ public class ClipboardManager {
|
|||||||
|
|
||||||
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
|
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
return (ClipData) method.invoke(manager, PACKAGE_NAME);
|
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
|
||||||
}
|
}
|
||||||
return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID);
|
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
|
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
|
||||||
throws InvocationTargetException, IllegalAccessException {
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
method.invoke(manager, clipData, PACKAGE_NAME);
|
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
|
||||||
} else {
|
} else {
|
||||||
method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
|
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,13 +72,48 @@ public class ClipboardManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setText(CharSequence text) {
|
public boolean setText(CharSequence text) {
|
||||||
try {
|
try {
|
||||||
Method method = getSetPrimaryClipMethod();
|
Method method = getSetPrimaryClipMethod();
|
||||||
ClipData clipData = ClipData.newPlainText(null, text);
|
ClipData clipData = ClipData.newPlainText(null, text);
|
||||||
setPrimaryClip(method, manager, clipData);
|
setPrimaryClip(method, manager, clipData);
|
||||||
|
return true;
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class ContentProvider implements Closeable {
|
||||||
|
|
||||||
|
public static final String TABLE_SYSTEM = "system";
|
||||||
|
public static final String TABLE_SECURE = "secure";
|
||||||
|
public static final String TABLE_GLOBAL = "global";
|
||||||
|
|
||||||
|
// See android/providerHolder/Settings.java
|
||||||
|
private static final String CALL_METHOD_GET_SYSTEM = "GET_system";
|
||||||
|
private static final String CALL_METHOD_GET_SECURE = "GET_secure";
|
||||||
|
private static final String CALL_METHOD_GET_GLOBAL = "GET_global";
|
||||||
|
|
||||||
|
private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
|
||||||
|
private static final String CALL_METHOD_PUT_SECURE = "PUT_secure";
|
||||||
|
private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global";
|
||||||
|
|
||||||
|
private static final String CALL_METHOD_USER_KEY = "_user";
|
||||||
|
|
||||||
|
private static final String NAME_VALUE_TABLE_VALUE = "value";
|
||||||
|
|
||||||
|
private final ActivityManager manager;
|
||||||
|
// android.content.IContentProvider
|
||||||
|
private final Object provider;
|
||||||
|
private final String name;
|
||||||
|
private final IBinder token;
|
||||||
|
|
||||||
|
private Method callMethod;
|
||||||
|
private boolean callMethodLegacy;
|
||||||
|
|
||||||
|
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
|
||||||
|
this.manager = manager;
|
||||||
|
this.provider = provider;
|
||||||
|
this.name = name;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getCallMethod() throws NoSuchMethodException {
|
||||||
|
if (callMethod == null) {
|
||||||
|
try {
|
||||||
|
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
// old version
|
||||||
|
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
|
||||||
|
callMethodLegacy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bundle call(String callMethod, String arg, Bundle extras) {
|
||||||
|
try {
|
||||||
|
Method method = getCallMethod();
|
||||||
|
Object[] args;
|
||||||
|
if (!callMethodLegacy) {
|
||||||
|
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||||
|
} else {
|
||||||
|
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
||||||
|
}
|
||||||
|
return (Bundle) method.invoke(provider, args);
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
manager.removeContentProviderExternal(name, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getGetMethod(String table) {
|
||||||
|
switch (table) {
|
||||||
|
case TABLE_SECURE:
|
||||||
|
return CALL_METHOD_GET_SECURE;
|
||||||
|
case TABLE_SYSTEM:
|
||||||
|
return CALL_METHOD_GET_SYSTEM;
|
||||||
|
case TABLE_GLOBAL:
|
||||||
|
return CALL_METHOD_GET_GLOBAL;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid table: " + table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPutMethod(String table) {
|
||||||
|
switch (table) {
|
||||||
|
case TABLE_SECURE:
|
||||||
|
return CALL_METHOD_PUT_SECURE;
|
||||||
|
case TABLE_SYSTEM:
|
||||||
|
return CALL_METHOD_PUT_SYSTEM;
|
||||||
|
case TABLE_GLOBAL:
|
||||||
|
return CALL_METHOD_PUT_GLOBAL;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid table: " + table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue(String table, String key) {
|
||||||
|
String method = getGetMethod(table);
|
||||||
|
Bundle arg = new Bundle();
|
||||||
|
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||||
|
Bundle bundle = call(method, key, arg);
|
||||||
|
if (bundle == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return bundle.getString("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putValue(String table, String key, String value) {
|
||||||
|
String method = getPutMethod(table);
|
||||||
|
Bundle arg = new Bundle();
|
||||||
|
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||||
|
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||||
|
call(method, key, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAndPutValue(String table, String key, String value) {
|
||||||
|
String oldValue = getValue(table, key);
|
||||||
|
if (!value.equals(oldValue)) {
|
||||||
|
putValue(table, key, value);
|
||||||
|
}
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
}
|
@ -12,15 +12,28 @@ public final class DisplayManager {
|
|||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DisplayInfo getDisplayInfo() {
|
public DisplayInfo getDisplayInfo(int displayId) {
|
||||||
try {
|
try {
|
||||||
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0);
|
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
|
||||||
|
if (displayInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Class<?> cls = displayInfo.getClass();
|
Class<?> cls = displayInfo.getClass();
|
||||||
// width and height already take the rotation into account
|
// width and height already take the rotation into account
|
||||||
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
|
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
|
||||||
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
|
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
|
||||||
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
|
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
|
||||||
return new DisplayInfo(new Size(width, height), rotation);
|
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
|
||||||
|
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
|
||||||
|
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getDisplayIds() {
|
||||||
|
try {
|
||||||
|
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ public final class InputManager {
|
|||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method injectInputEventMethod;
|
private Method injectInputEventMethod;
|
||||||
|
|
||||||
|
private static Method setDisplayIdMethod;
|
||||||
|
|
||||||
public InputManager(IInterface manager) {
|
public InputManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
@ -37,4 +39,22 @@ public final class InputManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Method getSetDisplayIdMethod() throws NoSuchMethodException {
|
||||||
|
if (setDisplayIdMethod == null) {
|
||||||
|
setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class);
|
||||||
|
}
|
||||||
|
return setDisplayIdMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean setDisplayId(InputEvent inputEvent, int displayId) {
|
||||||
|
try {
|
||||||
|
Method method = getSetDisplayIdMethod();
|
||||||
|
method.invoke(inputEvent, displayId);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Cannot associate a display id to the input event", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,12 @@ import android.os.IInterface;
|
|||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public final class ServiceManager {
|
public final class ServiceManager {
|
||||||
|
|
||||||
|
public static final String PACKAGE_NAME = "com.android.shell";
|
||||||
|
public static final int USER_ID = 0;
|
||||||
|
|
||||||
private final Method getServiceMethod;
|
private final Method getServiceMethod;
|
||||||
|
|
||||||
private WindowManager windowManager;
|
private WindowManager windowManager;
|
||||||
@ -16,6 +20,7 @@ public final class ServiceManager {
|
|||||||
private PowerManager powerManager;
|
private PowerManager powerManager;
|
||||||
private StatusBarManager statusBarManager;
|
private StatusBarManager statusBarManager;
|
||||||
private ClipboardManager clipboardManager;
|
private ClipboardManager clipboardManager;
|
||||||
|
private ActivityManager activityManager;
|
||||||
|
|
||||||
public ServiceManager() {
|
public ServiceManager() {
|
||||||
try {
|
try {
|
||||||
@ -76,4 +81,21 @@ public final class ServiceManager {
|
|||||||
}
|
}
|
||||||
return clipboardManager;
|
return clipboardManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ActivityManager getActivityManager() {
|
||||||
|
if (activityManager == null) {
|
||||||
|
try {
|
||||||
|
// On old Android versions, the ActivityManager is not exposed via AIDL,
|
||||||
|
// so use ActivityManagerNative.getDefault()
|
||||||
|
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
|
||||||
|
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
|
||||||
|
IInterface am = (IInterface) getDefaultMethod.invoke(null);
|
||||||
|
activityManager = new ActivityManager(am);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activityManager;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,12 +121,14 @@ public final class SurfaceControl {
|
|||||||
return setDisplayPowerModeMethod;
|
return setDisplayPowerModeMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setDisplayPowerMode(IBinder displayToken, int mode) {
|
public static boolean setDisplayPowerMode(IBinder displayToken, int mode) {
|
||||||
try {
|
try {
|
||||||
Method method = getSetDisplayPowerModeMethod();
|
Method method = getSetDisplayPowerModeMethod();
|
||||||
method.invoke(null, displayToken, mode);
|
method.invoke(null, displayToken, mode);
|
||||||
|
return true;
|
||||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
Ln.e("Could not invoke method", e);
|
Ln.e("Could not invoke method", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,13 +93,13 @@ public final class WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
|
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
|
||||||
try {
|
try {
|
||||||
Class<?> cls = manager.getClass();
|
Class<?> cls = manager.getClass();
|
||||||
try {
|
try {
|
||||||
// display parameter added since this commit:
|
// display parameter added since this commit:
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
|
||||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
|
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
// old version
|
// old version
|
||||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||||
|
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -25,15 +25,20 @@ 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();
|
||||||
|
|
||||||
|
// The message type (1 byte) does not count
|
||||||
|
Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1);
|
||||||
|
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,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();
|
||||||
|
|
||||||
@ -63,9 +68,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_INJECT_TEXT);
|
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||||
byte[] text = new byte[ControlMessageReader.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();
|
||||||
|
|
||||||
@ -77,7 +82,6 @@ public class ControlMessageReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
public void testParseTouchEvent() throws IOException {
|
public void testParseTouchEvent() throws IOException {
|
||||||
ControlMessageReader reader = new ControlMessageReader();
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
||||||
@ -95,6 +99,9 @@ public class ControlMessageReaderTest {
|
|||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
// The message type (1 byte) does not count
|
||||||
|
Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1);
|
||||||
|
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
@ -110,7 +117,6 @@ public class ControlMessageReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
public void testParseScrollEvent() throws IOException {
|
public void testParseScrollEvent() throws IOException {
|
||||||
ControlMessageReader reader = new ControlMessageReader();
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
||||||
@ -126,6 +132,9 @@ public class ControlMessageReaderTest {
|
|||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
// The message type (1 byte) does not count
|
||||||
|
Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1);
|
||||||
|
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
@ -209,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();
|
||||||
@ -220,6 +230,37 @@ 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());
|
||||||
|
|
||||||
|
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
||||||
|
Assert.assertTrue(parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
|
||||||
|
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
||||||
|
Assert.assertTrue(parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -233,6 +274,9 @@ public class ControlMessageReaderTest {
|
|||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
// The message type (1 byte) does not count
|
||||||
|
Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1);
|
||||||
|
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
@ -266,11 +310,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();
|
||||||
@ -280,12 +326,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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +347,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);
|
||||||
@ -311,6 +360,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();
|
||||||
@ -318,6 +368,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));
|
||||||
@ -327,6 +378,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -8,7 +8,6 @@ import java.nio.charset.StandardCharsets;
|
|||||||
public class StringUtilsTest {
|
public class StringUtilsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SuppressWarnings("checkstyle:MagicNumber")
|
|
||||||
public void testUtf8Truncate() {
|
public void testUtf8Truncate() {
|
||||||
String s = "aÉbÔc";
|
String s = "aÉbÔc";
|
||||||
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
|
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
|
||||||
|
Reference in New Issue
Block a user