Compare commits

..

5 Commits

Author SHA1 Message Date
d76c7c3029 Do not warn on terminating the server
If the server is already dead, terminating it fails. This is expected.
2020-03-29 21:31:12 +02:00
f2f032a494 Do not block on accept() if server died
The server may die before connecting to the client. In that case, the
client was blocked indefinitely (until Ctrl+C) on accept().

To avoid the problem, close the server socket once the server process is
dead.
2020-03-29 21:31:09 +02:00
728b976aae Wait server from a separate thread
Create a thread just to wait for the server process exit.

This paves the way to simply wake up a blocking accept() in a portable
way.
2020-03-29 21:24:58 +02:00
ff583bdde8 Refactor server_start() error handling
This avoids cleanup duplication.
2020-03-29 21:24:31 +02:00
429153abfb Add display id parameter
Add --display command line parameter to specify a display id.
2020-03-28 23:56:37 +01:00
86 changed files with 880 additions and 4718 deletions

View File

@ -1,29 +0,0 @@
---
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.

View File

@ -1,22 +0,0 @@
---
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.

5
.gitignore vendored
View File

@ -1,9 +1,4 @@
build/ build/
/dist/ /dist/
/build-*/
/build_*/
/release-*/
.idea/ .idea/
.gradle/ .gradle/
/x/
local.properties

View File

@ -8,22 +8,6 @@ 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
@ -176,8 +160,8 @@ Additionally, if you want to build the server, install Java 8 from Caskroom, and
make it avaliable from the `PATH`: make it avaliable from the `PATH`:
```bash ```bash
brew tap homebrew/cask-versions brew tap caskroom/versions
brew cask install adoptopenjdk/openjdk/adoptopenjdk8 brew cask install java8
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
@ -190,17 +174,12 @@ See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
## Common steps ## Common steps
If you want to build the server, install the [Android SDK] (_Android Studio_), If you want to build the server, install the [Android SDK] (_Android Studio_),
and set `ANDROID_SDK_ROOT` to its directory. For example: and set `ANDROID_HOME` to its directory. For example:
[Android SDK]: https://developer.android.com/studio/index.html [Android SDK]: https://developer.android.com/studio/index.html
```bash ```bash
# Linux export ANDROID_HOME=~/android/sdk
export ANDROID_SDK_ROOT=~/Android/Sdk
# Mac
export ANDROID_SDK_ROOT=~/Library/Android/sdk
# Windows
set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk
``` ```
If you don't want to build the server, use the [prebuilt server]. If you don't want to build the server, use the [prebuilt server].
@ -254,10 +233,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.16`][direct-scrcpy-server] - [`scrcpy-server-v1.12.1`][direct-scrcpy-server]
_(SHA-256: 94a79e05b4498d0460ab7bd9d12cbf05156e3a47bf0c5d1420cee1d4493b3832)_ _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-server-v1.16 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1
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:
@ -268,6 +247,3 @@ 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).

167
FAQ.md
View File

@ -3,104 +3,19 @@
Here are the common reported problems and their status. Here are the common reported problems and their status.
## `adb` issues ### On Windows, my device is not detected
`scrcpy` execute `adb` commands to initialize the connection with the device. If The most common is your device not being detected by `adb`, or is unauthorized.
`adb` fails, then scrcpy will not work. Check everything is ok by calling:
In that case, it will print this error:
> ERROR: "adb push" returned with value 1
This is typically not a bug in _scrcpy_, but a problem in your environment.
To find out the cause, execute:
```bash
adb devices adb devices
```
### `adb` not found Windows may need some [drivers] to detect your device.
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
> adb: error: failed to get feature set: no devices/emulators found
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
### Several devices connected ### I can only mirror, I cannot interact with the device
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:
@ -114,43 +29,22 @@ 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 [#15]. scaled. See [issue 15].
[#15]: https://github.com/Genymobile/scrcpy/issues/15 [issue 15]: https://github.com/Genymobile/scrcpy/issues/15
Open _scrcpy_ directly on the monitor you use it. A workaround is to build with HiDPI support disabled:
```bash
### Special characters do not work meson x --buildtype release -Dhidpi_support=false
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
``` ```
You may also need to configure the [scaling behavior]: However, the video will be displayed at lower resolution.
### 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_.
@ -158,7 +52,6 @@ You may also 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.
@ -168,29 +61,19 @@ 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
## Crashes ### I get an error "Could not open video stream"
### 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:

View File

@ -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.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/SDL2-2.0.10/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.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/SDL2-2.0.10/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)"; \

View File

@ -1,5 +1,3 @@
_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

307
README.md
View File

@ -1,6 +1,4 @@
# scrcpy (v1.16) # scrcpy (v1.12.1)
[Read in another language](#translations)
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.
@ -36,11 +34,10 @@ control it using keyboard and mouse.
## Get the app ## Get the app
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
### Linux ### Linux
On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): In Debian (_testing_ and _sid_ for now):
``` ```
apt install scrcpy apt install scrcpy
@ -52,11 +49,6 @@ A [Snap] package is available: [`scrcpy`][snap-link].
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) [snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
For Fedora, a [COPR] package is available: [`scrcpy`][copr-link].
[COPR]: https://fedoraproject.org/wiki/Category:Copr
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
@ -74,13 +66,16 @@ hard).
### Windows ### Windows
For Windows, for simplicity, a prebuilt archive with all the dependencies For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) is available: (including `adb`) are available:
- [`scrcpy-win64-v1.16.zip`][direct-win64] - [`scrcpy-win32-v1.12.1.zip`][direct-win32]
_(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ _(SHA-256: 0f4b3b063536b50a2df05dc42c760f9cc0093a9a26dbdf02d8232c74dab43480)_
- [`scrcpy-win64-v1.12.1.zip`][direct-win64]
_(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip [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.12.1/scrcpy-win64-v1.12.1.zip
It is also available in [Chocolatey]: It is also available in [Chocolatey]:
@ -88,18 +83,14 @@ It is also available in [Chocolatey]:
```bash ```bash
choco install scrcpy choco install scrcpy
choco install adb # if you don't have it yet
``` ```
And in [Scoop]: You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash ```bash
scoop install scrcpy choco install adb
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].
@ -203,22 +194,6 @@ scrcpy --lock-video-orientation 3 # 90° clockwise
This affects recording orientation. This affects recording orientation.
#### Encoder
Some devices have more than one encoder, and some of them may cause issues or
crash. It is possible to select a different encoder:
```bash
scrcpy --encoder OMX.qcom.video.encoder.avc
```
To list the available encoders, you could pass an invalid encoder name, the
error will give the available encoders:
```bash
scrcpy --encoder _
```
### Recording ### Recording
It is possible to record the screen while mirroring: It is possible to record the screen while mirroring:
@ -234,6 +209,7 @@ To disable mirroring while recording:
scrcpy --no-display --record file.mp4 scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C # interrupt recording with Ctrl+C
# Ctrl+C does not terminate properly on Windows, so disconnect the device
``` ```
"Skipped frames" are recorded, even if they are not displayed in real time (for "Skipped frames" are recorded, even if they are not displayed in real time (for
@ -293,7 +269,7 @@ You could use [AutoAdb]:
autoadb scrcpy -s '{}' autoadb scrcpy -s '{}'
``` ```
[AutoAdb]: https://github.com/rom1v/autoadb [AutoAdb]: https://github.com/rom1v/usbaudio
#### SSH tunnel #### SSH tunnel
@ -313,22 +289,6 @@ From another terminal:
scrcpy scrcpy
``` ```
To avoid enabling remote port forwarding, you could force a forward connection
instead (notice the `-L` instead of `-R`):
```bash
adb kill-server # kill the local adb server on 5037
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal:
```bash
scrcpy --force-adb-forward
```
Like for wireless connections, it may be useful to reduce quality: Like for wireless connections, it may be useful to reduce quality:
``` ```
@ -378,35 +338,7 @@ scrcpy --fullscreen
scrcpy -f # short version scrcpy -f # short version
``` ```
Fullscreen can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>. 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 <kbd>MOD</kbd>+<kbd></kbd>
_(left)_ and <kbd>MOD</kbd>+<kbd></kbd> _(right)_.
Note that _scrcpy_ manages 3 different rotations:
- <kbd>MOD</kbd>+<kbd>r</kbd> 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 <kbd>MOD</kbd>+<kbd></kbd>/<kbd>MOD</kbd>+<kbd></kbd>)
rotates only the window content. This affects only the display, not the
recording.
### Other mirroring options ### Other mirroring options
@ -436,22 +368,6 @@ The list of display ids can be retrieved by:
adb shell dumpsys display # search "mDisplayId=" in the output adb shell dumpsys display # search "mDisplayId=" in the output
``` ```
The secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored in read-only).
#### Stay awake
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
@ -462,22 +378,9 @@ scrcpy --turn-screen-off
scrcpy -S scrcpy -S
``` ```
Or by pressing <kbd>MOD</kbd>+<kbd>o</kbd> at any time. Or by pressing `Ctrl`+`o` at any time.
To turn it back on, press <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
On Android, the `POWER` button always turns the screen on. For convenience, if
`POWER` is sent via scrcpy (via right-click or <kbd>MOD</kbd>+<kbd>p</kbd>), it
will force to turn the screen off after a small delay (on a best effort basis).
The physical `POWER` button will still cause the screen to be turned on.
It can also be useful to prevent the device from sleeping:
```bash
scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
To turn it back on, press `POWER` (or `Ctrl`+`p`).
#### Render expired frames #### Render expired frames
@ -498,8 +401,7 @@ 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 restore the _Scrcpy_ provides an option to enable this feature on start and disable on exit:
initial value on exit:
```bash ```bash
scrcpy --show-touches scrcpy --show-touches
@ -509,78 +411,24 @@ scrcpy -t
Note that it only shows _physical_ touches (with the finger on the device). Note that it only shows _physical_ touches (with the finger on the device).
#### Disable screensaver
By default, scrcpy does not prevent the screensaver to run on the computer.
To disable it:
```bash
scrcpy --disable-screensaver
```
### Input control ### Input control
#### Rotate device screen #### Rotate device screen
Press <kbd>MOD</kbd>+<kbd>r</kbd> to switch between portrait and landscape Press `Ctrl`+`r` to switch between portrait and landscape modes.
modes.
Note that it rotates only if the application in foreground supports the Note that it rotates only if the application in foreground supports the
requested orientation. requested orientation.
#### Copy-paste #### Copy-paste
Any time the Android clipboard changes, it is automatically synchronized to the It is possible to synchronize clipboards between the computer and the device, in
computer clipboard. both directions:
Any <kbd>Ctrl</kbd> shortcut is forwarded to the device. In particular:
- <kbd>Ctrl</kbd>+<kbd>c</kbd> typically copies
- <kbd>Ctrl</kbd>+<kbd>x</kbd> typically cuts
- <kbd>Ctrl</kbd>+<kbd>v</kbd> typically pastes (after computer-to-device
clipboard synchronization)
This typically works as you expect.
The actual behavior depends on the active application though. For example,
_Termux_ sends SIGINT on <kbd>Ctrl</kbd>+<kbd>c</kbd> instead, and _K-9 Mail_
composes a new message.
To copy, cut and paste in such cases (but only supported on Android >= 7):
- <kbd>MOD</kbd>+<kbd>c</kbd> injects `COPY`
- <kbd>MOD</kbd>+<kbd>x</kbd> injects `CUT`
- <kbd>MOD</kbd>+<kbd>v</kbd> injects `PASTE` (after computer-to-device
clipboard synchronization)
In addition, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> allows to inject the
computer clipboard text as a sequence of key events. This is useful when the
component does not accept text pasting (for example in _Termux_), but it can
break non-ASCII content.
**WARNING:** Pasting the computer clipboard to the device (either via
<kbd>Ctrl</kbd>+<kbd>v</kbd> or <kbd>MOD</kbd>+<kbd>v</kbd>) copies the content
into the device clipboard. As a consequence, any Android application could read
its content. You should avoid to paste sensitive content (like passwords) that
way.
Some devices do not behave as expected when setting the device clipboard
programmatically. An option `--legacy-paste` is provided to change the behavior
of <kbd>Ctrl</kbd>+<kbd>v</kbd> and <kbd>MOD</kbd>+<kbd>v</kbd> so that they
also inject the computer clipboard text as a sequence of key events (the same
way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
#### Pinch-to-zoom
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
More precisely, hold <kbd>Ctrl</kbd> while pressing the left-click button. Until
the left-click button is released, all mouse movements scale and rotate the
content (if supported by the app) relative to the center of the screen.
Concretely, scrcpy generates additional touch events from a "virtual finger" at
a location inverted through the center of the screen.
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
breaks non-ASCII characters).
#### Text injection preference #### Text injection preference
@ -604,28 +452,6 @@ scrcpy --prefer-text
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
#### Key repeat
By default, holding a key down generates repeated key events. This can cause
performance problems in some games, where these events are useless anyway.
To avoid forwarding repeated key events:
```bash
scrcpy --no-key-repeat
```
#### Right-click and middle-click
By default, right-click triggers BACK (or POWER on) and middle-click triggers
HOME. To disable these shortcuts and forward the clicks to the device instead:
```bash
scrcpy --forward-all-clicks
```
### File drop ### File drop
#### Install APK #### Install APK
@ -652,67 +478,40 @@ scrcpy --push-target /sdcard/foo/bar/
### Audio forwarding ### Audio forwarding
Audio is not forwarded by _scrcpy_. Use [sndcpy]. Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
Also see [issue #14]. Also see [issue #14].
[sndcpy]: https://github.com/rom1v/sndcpy [USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 [issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts ## Shortcuts
In the following list, <kbd>MOD</kbd> is the shortcut modifier. By default, it's | Action | Shortcut | Shortcut (macOS)
(left) <kbd>Alt</kbd> or (left) <kbd>Super</kbd>. | -------------------------------------- |:----------------------------- |:-----------------------------
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
`lalt`, `ralt`, `lsuper` and `rsuper`. For example: | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
```bash | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
# use RCtrl for shortcuts | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
scrcpy --shortcut-mod=rctrl | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
# use either LCtrl+LAlt or LSuper for shortcuts | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
scrcpy --shortcut-mod=lctrl+lalt,lsuper | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
``` | Power on | _Right-click²_ | _Right-click²_
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
_<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._ | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
| Action | Shortcut | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
| ------------------------------------------- |:----------------------------- | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd> | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
| Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_
| Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd>
| Click on `MENU` (unlock screen) | <kbd>MOD</kbd>+<kbd>m</kbd>
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd></kbd> _(up)_
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd></kbd> _(down)_
| Click on `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
| Power on | _Right-click²_
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd>
| Collapse notification panel | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard³ | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard³ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste³ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._ _²Right-click turns the screen on if it was off, presses BACK otherwise._
_³Only on Android >= 7._
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
handled by the active application.
## Custom paths ## Custom paths
@ -781,13 +580,3 @@ Read the [developers page].
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ [article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
## Translations
This README is available in other languages:
- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md)
- [한국어 (Korean, `ko`) - v1.11](README.ko.md)
- [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.12.1](README.pt-br.md)
Only this README file is guaranteed to be up-to-date.

View File

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

View File

@ -1,705 +0,0 @@
_Only the original [README](README.md) is guaranteed to be up-to-date._
_只有原版的 [README](README.md)是保證最新的。_
本文件翻譯時點: [521f2fe](https://github.com/Genymobile/scrcpy/commit/521f2fe994019065e938aa1a54b56b4f10a4ac4a#diff-04c6e90faac2675aa89e2176d2eec7d8)
# scrcpy (v1.15)
Scrcpy 可以透過 USB、或是 [TCP/IP][article-tcpip] 來顯示或控制 Android 裝置。且 scrcpy 不需要 _root_ 權限。
Scrcpy 目前支援 _GNU/Linux_、_Windows_ 和 _macOS_
![screenshot](assets/screenshot-debian-600.jpg)
特色:
- **輕量** (只顯示裝置螢幕)
- **效能** (30~60fps)
- **品質** (1920×1080 或更高)
- **低延遲** ([35~70ms][lowlatency])
- **快速啟動** (~1 秒就可以顯示第一個畫面)
- **非侵入性** (不安裝、留下任何東西在裝置上)
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
## 需求
Android 裝置必須是 API 21+ (Android 5.0+)。
請確認裝置上的 [adb 偵錯 (通常位於開發者模式內)][enable-adb] 已啟用。
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
在部分的裝置上,你也必須啟用[特定的額外選項][control]才能使用鍵盤和滑鼠控制。
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
## 下載/獲取軟體
### Linux
Debian (目前支援 _testing__sid_) 和 Ubuntu (20.04):
```
apt install scrcpy
```
[Snap] 上也可以下載: [`scrcpy`][snap-link].
[snap-link]: https://snapstats.org/snaps/scrcpy
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
在 Fedora 上也可以使用 [COPR] 下載: [`scrcpy`][copr-link].
[COPR]: https://fedoraproject.org/wiki/Category:Copr
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
在 Arch Linux 上也可以使用 [AUR] 下載: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
在 Gentoo 上也可以使用 [Ebuild] 下載: [`scrcpy/`][ebuild-link].
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
你也可以自己[編譯 _Scrcpy_][BUILD]。別擔心,並沒有想像中的難。
### Windows
為了保持簡單Windows 用戶可以下載一個包含所有必需軟體 (包含 `adb`) 的壓縮包:
- [`scrcpy-win64-v1.15.zip`][direct-win64]
_(SHA-256: dd514bb591e63ef4cd52a53c30f1153a28f59722d64690eb07bd017849edcba2)_
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.15/scrcpy-win64-v1.15.zip
[Chocolatey] 上也可以下載:
[Chocolatey]: https://chocolatey.org/
```bash
choco install scrcpy
choco install adb # 如果你還沒有安裝的話
```
[Scoop] 上也可以下載:
```bash
scoop install scrcpy
scoop install adb # 如果你還沒有安裝的話
```
[Scoop]: https://scoop.sh
你也可以自己[編譯 _Scrcpy_][BUILD]。
### macOS
_Scrcpy_ 可以在 [Homebrew] 上直接安裝:
[Homebrew]: https://brew.sh/
```bash
brew install scrcpy
```
由於執行期間需要可以藉由 `PATH` 存取 `adb` 。如果還沒有安裝 `adb` 可以使用下列方式安裝:
```bash
brew cask install android-platform-tools
```
你也可以自己[編譯 _Scrcpy_][BUILD]。
## 執行
將電腦和你的 Android 裝置連線,然後執行:
```bash
scrcpy
```
_Scrcpy_ 可以接受命令列參數。輸入下列指令就可以瀏覽可以使用的命令列參數:
```bash
scrcpy --help
```
## 功能
> 以下說明中,有關快捷鍵的說明可能會出現 <kbd>MOD</kbd> 按鈕。相關說明請參見[快捷鍵]內的說明。
[快捷鍵]: #快捷鍵
### 畫面擷取
#### 縮小尺寸
使用比較低的解析度來投放 Android 裝置在某些情況可以提升效能。
限制寬和高的最大值(例如: 1024):
```bash
scrcpy --max-size 1024
scrcpy -m 1024 # 縮短版本
```
比較小的參數會根據螢幕比例重新計算。
根據上面的範例1920x1080 會被縮小成 1024x576。
#### 更改 bit-rate
預設的 bit-rate 是 8 Mbps。如果要更改 bit-rate (例如: 2 Mbps):
```bash
scrcpy --bit-rate 2M
scrcpy -b 2M # 縮短版本
```
#### 限制 FPS
限制畫面最高的 FPS:
```bash
scrcpy --max-fps 15
```
僅在 Android 10 後正式支援,不過也有可能可以在 Android 10 以前的版本使用。
#### 裁切
裝置的螢幕可以裁切。如此一來,鏡像出來的螢幕就只會是原本的一部份。
假如只要鏡像 Oculus Go 的其中一隻眼睛:
```bash
scrcpy --crop 1224:1440:0:0 # 位於 (0,0)大小1224x1440
```
如果 `--max-size` 也有指定的話,裁切後才會縮放。
#### 鎖定影像方向
如果要鎖定鏡像影像方向:
```bash
scrcpy --lock-video-orientation 0 # 原本的方向
scrcpy --lock-video-orientation 1 # 逆轉 90°
scrcpy --lock-video-orientation 2 # 180°
scrcpy --lock-video-orientation 3 # 順轉 90°
```
這會影響錄影結果的影像方向。
### 錄影
鏡像投放螢幕的同時也可以錄影:
```bash
scrcpy --record file.mp4
scrcpy -r file.mkv
```
如果只要錄影,不要投放螢幕鏡像的話:
```bash
scrcpy --no-display --record file.mp4
scrcpy -Nr file.mkv
# 用 Ctrl+C 停止錄影
```
就算有些幀為了效能而被跳過,它們還是一樣會被錄製。
裝置上的每一幀都有時間戳記,所以 [封包延遲 (Packet Delay Variation, PDV)][packet delay variation] 並不會影響錄影的檔案。
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
### 連線
#### 無線
_Scrcpy_ 利用 `adb` 和裝置通訊,而 `adb` 可以[透過 TCP/IP 連結][connect]:
1. 讓電腦和裝置連到同一個 Wi-Fi。
2. 獲取手機的 IP 位址(設定 → 關於手機 → 狀態).
3. 啟用裝置上的 `adb over TCP/IP`: `adb tcpip 5555`.
4. 拔掉裝置上的線。
5. 透過 TCP/IP 連接裝置: `adb connect DEVICE_IP:5555` _(把 `DEVICE_IP` 換成裝置的IP位址)_.
6. 和平常一樣執行 `scrcpy`
如果效能太差,可以降低 bit-rate 和解析度:
```bash
scrcpy --bit-rate 2M --max-size 800
scrcpy -b2M -m800 # 縮短版本
```
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
#### 多裝置
如果 `adb devices` 內有多個裝置,則必須附上 _serial_:
```bash
scrcpy --serial 0123456789abcdef
scrcpy -s 0123456789abcdef # 縮短版本
```
如果裝置是透過 TCP/IP 連線:
```bash
scrcpy --serial 192.168.0.1:5555
scrcpy -s 192.168.0.1:5555 # 縮短版本
```
你可以啟用復數個對應不同裝置的 _scrcpy_
#### 裝置連結後自動啟動
你可以使用 [AutoAdb]:
```bash
autoadb scrcpy -s '{}'
```
[AutoAdb]: https://github.com/rom1v/autoadb
#### SSH tunnel
本地的 `adb` 可以連接到遠端的 `adb` 伺服器(假設兩者使用相同版本的 _adb_ 通訊協定),以連接到遠端裝置:
```bash
adb kill-server # 停止在 Port 5037 的本地 adb 伺服
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
# 保持開啟
```
從另外一個終端機:
```bash
scrcpy
```
如果要避免啟用 remote port forwarding你可以強制它使用 forward connection (注意 `-L``-R` 的差別):
```bash
adb kill-server # 停止在 Port 5037 的本地 adb 伺服
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
# 保持開啟
```
從另外一個終端機:
```bash
scrcpy --force-adb-forward
```
和無線連接一樣,有時候降低品質會比較好:
```
scrcpy -b2M -m800 --max-fps 15
```
### 視窗調整
#### 標題
預設標題是裝置的型號,不過可以透過以下方式修改:
```bash
scrcpy --window-title 'My device'
```
#### 位置 & 大小
初始的視窗位置和大小也可以指定:
```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
```
#### 無邊框
如果要停用視窗裝飾:
```bash
scrcpy --window-borderless
```
#### 保持最上層
如果要保持 `scrcpy` 的視窗在最上層:
```bash
scrcpy --always-on-top
```
#### 全螢幕
這個軟體可以直接在全螢幕模式下起動:
```bash
scrcpy --fullscreen
scrcpy -f # 縮短版本
```
全螢幕可以使用 <kbd>MOD</kbd>+<kbd>f</kbd> 開關。
#### 旋轉
視窗可以旋轉:
```bash
scrcpy --rotation 1
```
可用的數值:
- `0`: 不旋轉
- `1`: 90 度**逆**轉
- `2`: 180 度
- `3`: 90 度**順**轉
旋轉方向也可以使用 <kbd>MOD</kbd>+<kbd></kbd> _(左方向鍵)_<kbd>MOD</kbd>+<kbd></kbd> _(右方向鍵)_ 調整。
_scrcpy_ 有 3 種不同的旋轉:
- <kbd>MOD</kbd>+<kbd>r</kbd> 要求裝置在垂直、水平之間旋轉 (目前運行中的 App 有可能會因為不支援而拒絕)。
- `--lock-video-orientation` 修改鏡像的方向 (裝置傳給電腦的影像)。這會影響錄影結果的影像方向。
- `--rotation` (或是 <kbd>MOD</kbd>+<kbd></kbd> / <kbd>MOD</kbd>+<kbd></kbd>) 只旋轉視窗的內容。這只會影響鏡像結果,不會影響錄影結果。
### 其他鏡像選項
#### 唯讀
停用所有控制,包含鍵盤輸入、滑鼠事件、拖放檔案:
```bash
scrcpy --no-control
scrcpy -n
```
#### 顯示螢幕
如果裝置有複數個螢幕,可以指定要鏡像哪個螢幕:
```bash
scrcpy --display 1
```
可以透過下列指令獲取螢幕 ID:
```
adb shell dumpsys display # 找輸出結果中的 "mDisplayId="
```
第二螢幕只有在 Android 10+ 時可以控制。如果不是 Android 10+,螢幕就會在唯讀狀態下投放。
#### 保持清醒
如果要避免裝置在連接狀態下進入睡眠:
```bash
scrcpy --stay-awake
scrcpy -w
```
_scrcpy_ 關閉後就會回復成原本的設定。
#### 關閉螢幕
鏡像開始時,可以要求裝置關閉螢幕:
```bash
scrcpy --turn-screen-off
scrcpy -S
```
或是在任何時候輸入 <kbd>MOD</kbd>+<kbd>o</kbd>
如果要開啟螢幕,輸入 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
在 Android 上,`POWER` 按鈕總是開啟螢幕。
為了方便,如果 `POWER` 是透過 scrcpy 轉送 (右鍵 或 <kbd>MOD</kbd>+<kbd>p</kbd>)的話,螢幕將會在短暫的延遲後關閉。
實際在手機上的 `POWER` 還是會開啟螢幕。
防止裝置進入睡眠狀態:
```bash
scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
#### 顯示過期的幀
為了降低延遲, _scrcpy_ 預設只會顯示最後解碼的幀,並且拋棄所有在這之前的幀。
如果要強制顯示所有的幀 (有可能會拉高延遲),輸入:
```bash
scrcpy --render-expired-frames
```
#### 顯示觸控點
對於要報告的人來說,顯示裝置上的實際觸控點有時候是有幫助的。
Android 在_開發者選項_中有提供這個功能。
_Scrcpy_ 可以在啟動時啟用這個功能,並且在停止後恢復成原本的設定:
```bash
scrcpy --show-touches
scrcpy -t
```
這個選項只會顯示**實際觸碰在裝置上的觸碰點**。
### 輸入控制
#### 旋轉裝置螢幕
輸入 <kbd>MOD</kbd>+<kbd>r</kbd> 以在垂直、水平之間切換。
如果使用中的程式不支援,則不會切換。
#### 複製/貼上
如果 Android 剪貼簿上的內容有任何更動,電腦的剪貼簿也會一起更動。
任何與 <kbd>Ctrl</kbd> 相關的快捷鍵事件都會轉送到裝置上。特別來說:
- <kbd>Ctrl</kbd>+<kbd>c</kbd> 通常是複製
- <kbd>Ctrl</kbd>+<kbd>x</kbd> 通常是剪下
- <kbd>Ctrl</kbd>+<kbd>v</kbd> 通常是貼上 (在電腦的剪貼簿與裝置上的剪貼簿同步之後)
這些跟你通常預期的行為一樣。
但是,實際上的行為是根據目前運行中的應用程式而定。
舉例來說, _Termux_ 在收到 <kbd>Ctrl</kbd>+<kbd>c</kbd> 後,會傳送 SIGINT_K-9 Mail_ 則是建立新訊息。
如果在這情況下,要剪下、複製或貼上 (只有在Android 7+時才支援):
- <kbd>MOD</kbd>+<kbd>c</kbd> 注入 `複製`
- <kbd>MOD</kbd>+<kbd>x</kbd> 注入 `剪下`
- <kbd>MOD</kbd>+<kbd>v</kbd> 注入 `貼上` (在電腦的剪貼簿與裝置上的剪貼簿同步之後)
另外,<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> 則是以一連串的按鍵事件貼上電腦剪貼簿中的內容。當元件不允許文字貼上 (例如 _Termux_) 時,這就很有用。不過,這在非 ASCII 內容上就無法使用。
**警告:** 貼上電腦的剪貼簿內容 (無論是從 <kbd>Ctrl</kbd>+<kbd>v</kbd><kbd>MOD</kbd>+<kbd>v</kbd>) 時,會複製剪貼簿中的內容至裝置的剪貼簿上。這會讓所有 Android 程式讀取剪貼簿的內容。請避免貼上任何敏感內容 (像是密碼)。
#### 文字輸入偏好
輸入文字時,有兩種[事件][textevents]會被觸發:
- _鍵盤事件 (key events)_代表有一個按鍵被按下或放開
- _文字事件 (text events)_代表有一個文字被輸入
預設上,文字是被以鍵盤事件 (key events) 輸入的,所以鍵盤和遊戲內所預期的一樣 (通常是指 WASD)。
但是這可能造成[一些問題][prefertext]。如果在這輸入這方面遇到了問題,你可以試試:
```bash
scrcpy --prefer-text
```
(不過遊戲內鍵盤就會不可用)
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
#### 重複輸入
通常來說,長時間按住一個按鍵會重複觸發按鍵事件。這會在一些遊戲中造成效能問題,而且這個重複的按鍵事件是沒有意義的。
如果不要轉送這些重複的按鍵事件:
```bash
scrcpy --no-key-repeat
```
### 檔案
#### 安裝 APK
如果要安裝 APK ,拖放一個 APK 檔案 (以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。
視窗上不會有任何反饋;結果會顯示在命令列中。
#### 推送檔案至裝置
如果要推送檔案到裝置上的 `/sdcard/` ,拖放一個非 APK 檔案 (**不**以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。
視窗上不會有任何反饋;結果會顯示在命令列中。
推送檔案的目標路徑可以在啟動時指定:
```bash
scrcpy --push-target /sdcard/foo/bar/
```
### 音訊轉送
_scrcpy_ **不**轉送音訊。請使用 [sndcpy]。另外,參見 [issue #14]。
[sndcpy]: https://github.com/rom1v/sndcpy
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
## 快捷鍵
在以下的清單中,<kbd>MOD</kbd> 是快捷鍵的特殊按鍵。通常來說,這個按鍵是 (左) <kbd>Alt</kbd> 或是 (左) <kbd>Super</kbd>
這個是可以使用 `--shortcut-mod` 更改的。可以用的選項有:
- `lctrl`: 左邊的 <kbd>Ctrl</kbd>
- `rctrl`: 右邊的 <kbd>Ctrl</kbd>
- `lalt`: 左邊的 <kbd>Alt</kbd>
- `ralt`: 右邊的 <kbd>Alt</kbd>
- `lsuper`: 左邊的 <kbd>Super</kbd>
- `rsuper`: 右邊的 <kbd>Super</kbd>
```bash
# 以 右邊的 Ctrl 為快捷鍵特殊按鍵
scrcpy --shortcut-mod=rctrl
# 以 左邊的 Ctrl 和左邊的 Alt 或是 左邊的 Super 為快捷鍵特殊按鍵
scrcpy --shortcut-mod=lctrl+lalt,lsuper
```
_<kbd>[Super]</kbd> 通常是 <kbd>Windows</kbd> 或 <kbd>Cmd</kbd> 鍵。_
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
| Action | Shortcut
| ------------------------------------------------- |:-----------------------------
| 切換至全螢幕 | <kbd>MOD</kbd>+<kbd>f</kbd>
| 左旋顯示螢幕 | <kbd>MOD</kbd>+<kbd></kbd> _(左)_
| 右旋顯示螢幕 | <kbd>MOD</kbd>+<kbd></kbd> _(右)_
| 縮放視窗成 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| 縮放視窗到沒有黑邊框為止 | <kbd>MOD</kbd>+<kbd>w</kbd> \| _雙擊¹_
| 按下 `首頁` 鍵 | <kbd>MOD</kbd>+<kbd>h</kbd> \| _中鍵_
| 按下 `返回` 鍵 | <kbd>MOD</kbd>+<kbd>b</kbd> \| _右鍵²_
| 按下 `切換 APP` 鍵 | <kbd>MOD</kbd>+<kbd>s</kbd>
| 按下 `選單` 鍵 (或解鎖螢幕) | <kbd>MOD</kbd>+<kbd>m</kbd>
| 按下 `音量+` 鍵 | <kbd>MOD</kbd>+<kbd></kbd> _(上)_
| 按下 `音量-` 鍵 | <kbd>MOD</kbd>+<kbd></kbd> _(下)_
| 按下 `電源` 鍵 | <kbd>MOD</kbd>+<kbd>p</kbd>
| 開啟 | _右鍵²_
| 關閉裝置螢幕(持續鏡像) | <kbd>MOD</kbd>+<kbd>o</kbd>
| 開啟裝置螢幕 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| 旋轉裝置螢幕 | <kbd>MOD</kbd>+<kbd>r</kbd>
| 開啟通知列 | <kbd>MOD</kbd>+<kbd>n</kbd>
| 關閉通知列 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| 複製至剪貼簿³ | <kbd>MOD</kbd>+<kbd>c</kbd>
| 剪下至剪貼簿³ | <kbd>MOD</kbd>+<kbd>x</kbd>
| 同步剪貼簿並貼上³ | <kbd>MOD</kbd>+<kbd>v</kbd>
| 複製電腦剪貼簿中的文字至裝置並貼上 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| 啟用/停用 FPS 計數器(顯示於 stdout - 通常是命令列) | <kbd>MOD</kbd>+<kbd>i</kbd>
_¹在黑邊框上雙擊以移除它們。_
_²右鍵會返回。如果螢幕是關閉狀態則會打開螢幕。_
_³只支援 Android 7+。_
所有 <kbd>Ctrl</kbd>+_按鍵_ 快捷鍵都會傳送到裝置上,所以它們是由目前運作的應用程式處理的。
## 自訂路徑
如果要使用特定的 _adb_ ,將它設定到環境變數中的 `ADB`:
ADB=/path/to/adb scrcpy
如果要覆寫 `scrcpy-server` 檔案的路徑,則將路徑設定到環境變數中的 `SCRCPY_SERVER_PATH`
[相關連結][useful]
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
## 為何叫 _scrcpy_
有一個同事要我找一個跟 [gnirehtet] 一樣難念的名字。
[`strcpy`] 複製一個字串 (**str**ing)`scrcpy` 複製一個螢幕 (**scr**een)。
[gnirehtet]: https://github.com/Genymobile/gnirehtet
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
## 如何編譯?
請看[這份文件 (英文)][BUILD]。
[BUILD]: BUILD.md
## 常見問題
請看[這份文件 (英文)][FAQ]。
[FAQ]: FAQ.md
## 開發者文件
請看[這個頁面 (英文)][developers page].
[developers page]: DEVELOP.md
## Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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.
## 相關文章
- [Scrcpy 簡介 (英文)][article-intro]
- [Scrcpy 可以無線連線了 (英文)][article-tcpip]
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/

View File

@ -11,7 +11,6 @@ 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',
@ -164,12 +163,12 @@ if get_option('buildtype') == 'debug'
'src/cli.c', 'src/cli.c',
'src/util/str_util.c', 'src/util/str_util.c',
]], ]],
['test_control_msg_serialize', [ ['test_control_event_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_msg_deserialize', [ ['test_device_event_deserialize', [
'tests/test_device_msg_deserialize.c', 'tests/test_device_msg_deserialize.c',
'src/device_msg.c', 'src/device_msg.c',
]], ]],
@ -186,7 +185,7 @@ if get_option('buildtype') == 'debug'
exe = executable(t[0], t[1], exe = executable(t[0], t[1],
include_directories: src_dir, include_directories: src_dir,
dependencies: dependencies, dependencies: dependencies,
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) c_args: ['-DSDL_MAIN_HANDLED'])
test(t[0], exe) test(t[0], exe)
endforeach endforeach
endif endif

View File

@ -25,16 +25,6 @@ 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.
@ -43,10 +33,6 @@ The values are expressed in the device natural orientation (typically, portrait
.B \-\-max\-size .B \-\-max\-size
value is computed on the cropped size. value is computed on the cropped size.
.TP
.BI "\-\-disable-screensaver"
Disable screensaver while scrcpy is running.
.TP .TP
.BI "\-\-display " id .BI "\-\-display " id
Specify the display id to mirror. Specify the display id to mirror.
@ -56,18 +42,6 @@ The list of possible display ids can be listed by "adb shell dumpsys display"
Default is 0. Default is 0.
.TP
.BI "\-\-encoder " name
Use a specific MediaCodec encoder (must be a H.264 encoder).
.TP
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
.TP
.B \-\-forward\-all\-clicks
By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead.
.TP .TP
.B \-f, \-\-fullscreen .B \-f, \-\-fullscreen
Start in fullscreen. Start in fullscreen.
@ -76,15 +50,9 @@ Start in fullscreen.
.B \-h, \-\-help .B \-h, \-\-help
Print this help. Print this help.
.TP
.B \-\-legacy\-paste
Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v).
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP .TP
.BI "\-\-lock\-video\-orientation " value .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. Lock video orientation to \fIvalue\fR. Values are integers in the range [-1..3]. Natural device orientation is 0 and each increment adds 90 degrees counterclockwise.
Default is -1 (unlocked). Default is -1 (unlocked).
@ -106,14 +74,6 @@ Disable device control (mirror the device in read\-only).
.B \-N, \-\-no\-display .B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled). Do not display device (only when screen recording is enabled).
.TP
.B \-\-no\-key\-repeat
Do not forward repeated key events when a key is held down.
.TP
.B \-\-no\-mipmaps
If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps.
.TP .TP
.BI "\-p, \-\-port " port[:port] .BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen. Set the TCP port (range) used by the client to listen.
@ -146,44 +106,21 @@ 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.
.TP
.BI "\-\-shortcut\-mod " key[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper".
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP .TP
.B \-S, \-\-turn\-screen\-off .B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately. Turn the device screen off immediately.
.TP .TP
.B \-t, \-\-show\-touches .B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit. Enable "show touches" on start, disable on quit.
It only shows physical touches (not clicks from scrcpy). It only shows physical touches (not clicks from scrcpy).
@ -191,16 +128,6 @@ 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).
@ -235,108 +162,85 @@ Default is 0 (automatic).\n
.SH SHORTCUTS .SH SHORTCUTS
In the following list, MOD is the shortcut modifier. By default, it's (left) .TP
Alt or (left) Super, but it can be configured by \-\-shortcut-mod (see above). .B Ctrl+f
switch fullscreen mode
.TP .TP
.B MOD+f .B Ctrl+g
Switch fullscreen mode resize window to 1:1 (pixel\-perfect)
.TP .TP
.B MOD+Left .B Ctrl+x, Double\-click on black borders
Rotate display left resize window to remove black borders
.TP .TP
.B MOD+Right .B Ctrl+h, Home, Middle\-click
Rotate display right
.TP
.B MOD+g
Resize window to 1:1 (pixel\-perfect)
.TP
.B MOD+w, Double\-click on black borders
Resize window to remove black borders
.TP
.B MOD+h, Home, Middle\-click
Click on HOME Click on HOME
.TP .TP
.B MOD+b, MOD+Backspace, Right\-click (when screen is on) .B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on)
Click on BACK Click on BACK
.TP .TP
.B MOD+s .B Ctrl+s
Click on APP_SWITCH Click on APP_SWITCH
.TP .TP
.B MOD+m .B Ctrl+m
Click on MENU Click on MENU
.TP .TP
.B MOD+Up .B Ctrl+Up
Click on VOLUME_UP Click on VOLUME_UP
.TP .TP
.B MOD+Down .B Ctrl+Down
Click on VOLUME_DOWN Click on VOLUME_DOWN
.TP .TP
.B MOD+p .B Ctrl+p
Click on POWER (turn screen on/off) 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 MOD+o .B Ctrl+o
Turn device screen off (keep mirroring) turn device screen off (keep mirroring)
.TP .TP
.B MOD+Shift+o .B Ctrl+r
Turn device screen on rotate device screen
.TP .TP
.B MOD+r .B Ctrl+n
Rotate device screen expand notification panel
.TP .TP
.B MOD+n .B Ctrl+Shift+n
Expand notification panel collapse notification panel
.TP .TP
.B MOD+Shift+n .B Ctrl+c
Collapse notification panel copy device clipboard to computer
.TP .TP
.B Mod+c .B Ctrl+v
Copy to clipboard (inject COPY keycode, Android >= 7 only) paste computer clipboard to device
.TP .TP
.B Mod+x .B Ctrl+Shift+v
Cut to clipboard (inject CUT keycode, Android >= 7 only) copy computer clipboard to device
.TP .TP
.B MOD+v .B Ctrl+i
Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) enable/disable FPS counter (print frames/second in logs)
.TP
.B MOD+Shift+v
Inject computer clipboard text as a sequence of key events
.TP
.B MOD+i
Enable/disable FPS counter (print frames/second in logs)
.TP
.B Ctrl+click-and-move
Pinch-to-zoom from the center of the screen
.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

View File

@ -3,16 +3,20 @@
#include <assert.h> #include <assert.h>
#include <getopt.h> #include <getopt.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h>
#include <unistd.h> #include <unistd.h>
#include "config.h" #include "config.h"
#include "scrcpy.h" #include "recorder.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
void void
scrcpy_print_usage(const char *arg0) { scrcpy_print_usage(const char *arg0) {
#ifdef __APPLE__
# define CTRL_OR_CMD "Cmd"
#else
# define CTRL_OR_CMD "Ctrl"
#endif
fprintf(stderr, fprintf(stderr,
"Usage: %s [options]\n" "Usage: %s [options]\n"
"\n" "\n"
@ -26,24 +30,12 @@ scrcpy_print_usage(const char *arg0) {
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n" " Default is %d.\n"
"\n" "\n"
" --codec-options key[:type]=value[,...]\n"
" Set a list of comma-separated key:type=value options for the\n"
" device encoder.\n"
" The possible values for 'type' are 'int' (default), 'long',\n"
" 'float' and 'string'.\n"
" The list of possible codec options is available in the\n"
" Android documentation:\n"
" <https://d.android.com/reference/android/media/MediaFormat>\n"
"\n"
" --crop width:height:x:y\n" " --crop width:height:x:y\n"
" Crop the device screen on the server.\n" " Crop the device screen on the server.\n"
" The values are expressed in the device natural orientation\n" " The values are expressed in the device natural orientation\n"
" (typically, portrait for a phone, landscape for a tablet).\n" " (typically, portrait for a phone, landscape for a tablet).\n"
" Any --max-size value is computed on the cropped size.\n" " Any --max-size value is computed on the cropped size.\n"
"\n" "\n"
" --disable-screensaver\n"
" Disable screensaver while scrcpy is running.\n"
"\n"
" --display id\n" " --display id\n"
" Specify the display id to mirror.\n" " Specify the display id to mirror.\n"
"\n" "\n"
@ -53,35 +45,16 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
" Default is 0.\n" " Default is 0.\n"
"\n" "\n"
" --encoder name\n"
" Use a specific MediaCodec encoder (must be a H.264 encoder).\n"
"\n"
" --force-adb-forward\n"
" Do not attempt to use \"adb reverse\" to connect to the\n"
" the device.\n"
"\n"
" --forward-all-clicks\n"
" By default, right-click triggers BACK (or POWER on) and\n"
" middle-click triggers HOME. This option disables these\n"
" shortcuts and forward the clicks to the device instead.\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"
" --legacy-paste\n"
" Inject computer clipboard text as a sequence of key events\n"
" on Ctrl+v (like MOD+Shift+v).\n"
" This is a workaround for some devices not behaving as\n"
" expected when setting the device clipboard programmatically.\n"
"\n"
" --lock-video-orientation value\n" " --lock-video-orientation value\n"
" Lock video orientation to value.\n" " Lock video orientation to value. Values are integers in the\n"
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" " range [-1..3]. Natural device orientation is 0 and each\n"
" Natural device orientation is 0, and each increment adds a\n" " increment adds 90 degrees counterclockwise.\n"
" 90 degrees rotation counterclockwise.\n"
" Default is %d%s.\n" " Default is %d%s.\n"
"\n" "\n"
" --max-fps value\n" " --max-fps value\n"
@ -101,14 +74,6 @@ 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"
" --no-key-repeat\n"
" Do not forward repeated key events when a key is held down.\n"
"\n"
" --no-mipmaps\n"
" If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n"
" mipmaps are automatically generated to improve downscaling\n"
" quality. This option disables the generation of mipmaps.\n"
"\n"
" -p, --port port[:port]\n" " -p, --port port[:port]\n"
" Set the TCP port (range) used by the client to listen.\n" " Set the TCP port (range) used by the client to listen.\n"
" Default is %d:%d.\n" " Default is %d:%d.\n"
@ -133,64 +98,26 @@ 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"
"\n" "\n"
" --shortcut-mod key[+...]][,...]\n"
" Specify the modifiers to use for scrcpy shortcuts.\n"
" Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n"
" \"lsuper\" and \"rsuper\".\n"
"\n"
" A shortcut can consist in several keys, separated by '+'.\n"
" Several shortcuts can be specified, separated by ','.\n"
"\n"
" For example, to use either LCtrl+LAlt or LSuper for scrcpy\n"
" shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"\n"
" Default is \"lalt,lsuper\" (left-Alt or left-Super).\n"
"\n"
" -S, --turn-screen-off\n" " -S, --turn-screen-off\n"
" Turn the device screen off immediately.\n" " Turn the device screen off immediately.\n"
"\n" "\n"
" -t, --show-touches\n" " -t, --show-touches\n"
" Enable \"show touches\" on start, restore the initial value\n" " Enable \"show touches\" on start, disable on quit.\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"
@ -215,89 +142,69 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
"Shortcuts:\n" "Shortcuts:\n"
"\n" "\n"
" In the following list, MOD is the shortcut modifier. By default,\n" " " CTRL_OR_CMD "+f\n"
" it's (left) Alt or (left) Super, but it can be configured by\n" " switch fullscreen mode\n"
" --shortcut-mod (see above).\n"
"\n" "\n"
" MOD+f\n" " " CTRL_OR_CMD "+g\n"
" Switch fullscreen mode\n" " resize window to 1:1 (pixel-perfect)\n"
"\n" "\n"
" MOD+Left\n" " " CTRL_OR_CMD "+x\n"
" Rotate display left\n"
"\n"
" MOD+Right\n"
" Rotate display right\n"
"\n"
" MOD+g\n"
" Resize window to 1:1 (pixel-perfect)\n"
"\n"
" MOD+w\n"
" Double-click on black borders\n" " Double-click on black borders\n"
" Resize window to remove black borders\n" " resize window to remove black borders\n"
"\n" "\n"
" MOD+h\n" " Ctrl+h\n"
" Middle-click\n" " Middle-click\n"
" Click on HOME\n" " click on HOME\n"
"\n" "\n"
" MOD+b\n" " " CTRL_OR_CMD "+b\n"
" MOD+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"
" MOD+s\n" " " CTRL_OR_CMD "+s\n"
" Click on APP_SWITCH\n" " click on APP_SWITCH\n"
"\n" "\n"
" MOD+m\n" " Ctrl+m\n"
" Click on MENU\n" " click on MENU\n"
"\n" "\n"
" MOD+Up\n" " " CTRL_OR_CMD "+Up\n"
" Click on VOLUME_UP\n" " click on VOLUME_UP\n"
"\n" "\n"
" MOD+Down\n" " " CTRL_OR_CMD "+Down\n"
" Click on VOLUME_DOWN\n" " click on VOLUME_DOWN\n"
"\n" "\n"
" MOD+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"
" MOD+o\n" " " CTRL_OR_CMD "+o\n"
" Turn device screen off (keep mirroring)\n" " turn device screen off (keep mirroring)\n"
"\n" "\n"
" MOD+Shift+o\n" " " CTRL_OR_CMD "+r\n"
" Turn device screen on\n" " rotate device screen\n"
"\n" "\n"
" MOD+r\n" " " CTRL_OR_CMD "+n\n"
" Rotate device screen\n" " expand notification panel\n"
"\n" "\n"
" MOD+n\n" " " CTRL_OR_CMD "+Shift+n\n"
" Expand notification panel\n" " collapse notification panel\n"
"\n" "\n"
" MOD+Shift+n\n" " " CTRL_OR_CMD "+c\n"
" Collapse notification panel\n" " copy device clipboard to computer\n"
"\n" "\n"
" MOD+c\n" " " CTRL_OR_CMD "+v\n"
" Copy to clipboard (inject COPY keycode, Android >= 7 only)\n" " paste computer clipboard to device\n"
"\n" "\n"
" MOD+x\n" " " CTRL_OR_CMD "+Shift+v\n"
" Cut to clipboard (inject CUT keycode, Android >= 7 only)\n" " copy computer clipboard to device\n"
"\n" "\n"
" MOD+v\n" " " CTRL_OR_CMD "+i\n"
" Copy computer clipboard to device, then paste (inject PASTE\n" " enable/disable FPS counter (print frames/second in logs)\n"
" keycode, Android >= 7 only)\n"
"\n"
" MOD+Shift+v\n"
" Inject computer clipboard text as a sequence of key events\n"
"\n"
" MOD+i\n"
" Enable/disable FPS counter (print frames/second in logs)\n"
"\n"
" Ctrl+click-and-move\n"
" Pinch-to-zoom from the center of the screen\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,
@ -403,25 +310,13 @@ parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
return true; 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 static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); static_assert(WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value");
if (!strcmp(s, "auto")) { if (!strcmp(s, "auto")) {
*position = SC_WINDOW_POSITION_UNDEFINED; *position = WINDOW_POSITION_UNDEFINED;
return true; return true;
} }
@ -450,7 +345,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
} }
static bool static bool
parse_port_range(const char *s, struct sc_port_range *port_range) { parse_port_range(const char *s, struct port_range *port_range) {
long values[2]; long values[2];
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
if (!count) { if (!count) {
@ -490,143 +385,20 @@ parse_display_id(const char *s, uint16_t *display_id) {
} }
static bool static bool
parse_log_level(const char *s, enum sc_log_level *log_level) { parse_record_format(const char *optarg, enum recorder_format *format) {
if (!strcmp(s, "debug")) {
*log_level = SC_LOG_LEVEL_DEBUG;
return true;
}
if (!strcmp(s, "info")) {
*log_level = SC_LOG_LEVEL_INFO;
return true;
}
if (!strcmp(s, "warn")) {
*log_level = SC_LOG_LEVEL_WARN;
return true;
}
if (!strcmp(s, "error")) {
*log_level = SC_LOG_LEVEL_ERROR;
return true;
}
LOGE("Could not parse log level: %s", s);
return false;
}
// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt")
// returns a bitwise-or of SC_MOD_* constants (or 0 on error)
static unsigned
parse_shortcut_mods_item(const char *item, size_t len) {
unsigned mod = 0;
for (;;) {
char *plus = strchr(item, '+');
// strchr() does not consider the "len" parameter, to it could find an
// occurrence too far in the string (there is no strnchr())
bool has_plus = plus && plus < item + len;
assert(!has_plus || plus > item);
size_t key_len = has_plus ? (size_t) (plus - item) : len;
#define STREQ(literal, s, len) \
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
if (STREQ("lctrl", item, key_len)) {
mod |= SC_MOD_LCTRL;
} else if (STREQ("rctrl", item, key_len)) {
mod |= SC_MOD_RCTRL;
} else if (STREQ("lalt", item, key_len)) {
mod |= SC_MOD_LALT;
} else if (STREQ("ralt", item, key_len)) {
mod |= SC_MOD_RALT;
} else if (STREQ("lsuper", item, key_len)) {
mod |= SC_MOD_LSUPER;
} else if (STREQ("rsuper", item, key_len)) {
mod |= SC_MOD_RSUPER;
} else {
LOGE("Unknown modifier key: %.*s "
"(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)",
(int) key_len, item);
return 0;
}
#undef STREQ
if (!has_plus) {
break;
}
item = plus + 1;
assert(len >= key_len + 1);
len -= key_len + 1;
}
return mod;
}
static bool
parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
unsigned count = 0;
unsigned current = 0;
// LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper"
for (;;) {
char *comma = strchr(s, ',');
if (comma && count == SC_MAX_SHORTCUT_MODS - 1) {
assert(count < SC_MAX_SHORTCUT_MODS);
LOGW("Too many shortcut modifiers alternatives");
return false;
}
assert(!comma || comma > s);
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
unsigned mod = parse_shortcut_mods_item(s, limit);
if (!mod) {
LOGE("Invalid modifier keys: %.*s", (int) limit, s);
return false;
}
mods->data[current++] = mod;
++count;
if (!comma) {
break;
}
s = comma + 1;
}
mods->count = count;
return true;
}
#ifdef SC_TEST
// expose the function to unit-tests
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
return parse_shortcut_mods(s, mods);
}
#endif
static bool
parse_record_format(const char *optarg, enum sc_record_format *format) {
if (!strcmp(optarg, "mp4")) { if (!strcmp(optarg, "mp4")) {
*format = SC_RECORD_FORMAT_MP4; *format = RECORDER_FORMAT_MP4;
return true; return true;
} }
if (!strcmp(optarg, "mkv")) { if (!strcmp(optarg, "mkv")) {
*format = SC_RECORD_FORMAT_MKV; *format = RECORDER_FORMAT_MKV;
return true; return true;
} }
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false; return false;
} }
static enum sc_record_format static enum recorder_format
guess_record_format(const char *filename) { guess_record_format(const char *filename) {
size_t len = strlen(filename); size_t len = strlen(filename);
if (len < 4) { if (len < 4) {
@ -634,10 +406,10 @@ guess_record_format(const char *filename) {
} }
const char *ext = &filename[len - 4]; const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) { if (!strcmp(ext, ".mp4")) {
return SC_RECORD_FORMAT_MP4; return RECORDER_FORMAT_MP4;
} }
if (!strcmp(ext, ".mkv")) { if (!strcmp(ext, ".mkv")) {
return SC_RECORD_FORMAT_MKV; return RECORDER_FORMAT_MKV;
} }
return 0; return 0;
} }
@ -657,59 +429,32 @@ guess_record_format(const char *filename) {
#define OPT_MAX_FPS 1012 #define OPT_MAX_FPS 1012
#define OPT_LOCK_VIDEO_ORIENTATION 1013 #define OPT_LOCK_VIDEO_ORIENTATION 1013
#define OPT_DISPLAY_ID 1014 #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
#define OPT_SHORTCUT_MOD 1021
#define OPT_NO_KEY_REPEAT 1022
#define OPT_FORWARD_ALL_CLICKS 1023
#define OPT_LEGACY_PASTE 1024
#define OPT_ENCODER_NAME 1025
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
static const struct option long_options[] = { static const struct option long_options[] = {
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
{"bit-rate", required_argument, NULL, 'b'}, {"bit-rate", required_argument, NULL, 'b'},
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
{"crop", required_argument, NULL, OPT_CROP}, {"crop", required_argument, NULL, OPT_CROP},
{"disable-screensaver", no_argument, NULL,
OPT_DISABLE_SCREENSAVER},
{"display", required_argument, NULL, OPT_DISPLAY_ID}, {"display", required_argument, NULL, OPT_DISPLAY_ID},
{"encoder", required_argument, NULL, OPT_ENCODER_NAME},
{"force-adb-forward", no_argument, NULL,
OPT_FORCE_ADB_FORWARD},
{"forward-all-clicks", no_argument, NULL,
OPT_FORWARD_ALL_CLICKS},
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
{"lock-video-orientation", required_argument, NULL, {"lock-video-orientation", required_argument, NULL,
OPT_LOCK_VIDEO_ORIENTATION}, OPT_LOCK_VIDEO_ORIENTATION},
{"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
{"no-control", no_argument, NULL, 'n'}, {"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'}, {"no-display", no_argument, NULL, 'N'},
{"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT},
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
{"port", required_argument, NULL, 'p'}, {"port", required_argument, NULL, 'p'},
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"push-target", required_argument, NULL, OPT_PUSH_TARGET}, {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'}, {"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, {"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
{"render-driver", required_argument, NULL, OPT_RENDER_DRIVER},
{"render-expired-frames", no_argument, NULL, {"render-expired-frames", no_argument, NULL,
OPT_RENDER_EXPIRED_FRAMES}, OPT_RENDER_EXPIRED_FRAMES},
{"rotation", required_argument, NULL, OPT_ROTATION},
{"serial", required_argument, NULL, 's'}, {"serial", required_argument, NULL, 's'},
{"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD},
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
{"verbosity", required_argument, NULL, 'V'}, {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
{"window-x", required_argument, NULL, OPT_WINDOW_X}, {"window-x", required_argument, NULL, OPT_WINDOW_X},
@ -726,8 +471,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:StTvV:w", while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTv", long_options,
long_options, NULL)) != -1) { 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)) {
@ -806,14 +551,6 @@ 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;
@ -849,43 +586,6 @@ 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_NO_KEY_REPEAT:
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS:
opts->codec_options = optarg;
break;
case OPT_ENCODER_NAME:
opts->encoder_name = optarg;
break;
case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true;
break;
case OPT_DISABLE_SCREENSAVER:
opts->disable_screensaver = true;
break;
case OPT_SHORTCUT_MOD:
if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) {
return false;
}
break;
case OPT_FORWARD_ALL_CLICKS:
opts->forward_all_clicks = true;
break;
case OPT_LEGACY_PASTE:
opts->legacy_paste = true;
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@ -897,6 +597,11 @@ 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]);
@ -922,10 +627,5 @@ 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;
} }

View File

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

View File

@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) {
static size_t static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) { write_string(const char *utf8, size_t max_len, unsigned char *buf) {
size_t len = utf8_truncation_index(utf8, max_len); size_t len = utf8_truncation_index(utf8, max_len);
buffer_write32be(buf, len); buffer_write16be(buf, (uint16_t) len);
memcpy(&buf[4], utf8, len); memcpy(&buf[2], utf8, len);
return 4 + len; return 2 + len;
} }
static uint16_t static uint16_t
@ -42,13 +42,11 @@ 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.repeat); buffer_write32be(&buf[6], msg->inject_keycode.metastate);
buffer_write32be(&buf[10], msg->inject_keycode.metastate); return 10;
return 14;
case CONTROL_MSG_TYPE_INJECT_TEXT: { case CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len = size_t len = write_string(msg->inject_text.text,
write_string(msg->inject_text.text, CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]);
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:
@ -68,11 +66,10 @@ 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: {
buf[1] = !!msg->set_clipboard.paste; size_t len = write_string(msg->inject_text.text,
size_t len = write_string(msg->set_clipboard.text,
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[2]); &buf[1]);
return 2 + len; return 1 + len;
} }
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;

View File

@ -10,14 +10,12 @@
#include "android/keycodes.h" #include "android/keycodes.h"
#include "common.h" #include "common.h"
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define CONTROL_MSG_TEXT_MAX_LENGTH 300
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 #define CONTROL_MSG_SERIALIZED_MAX_SIZE \
// type: 1 byte; paste flag: 1 byte; length: 4 bytes (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
#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);
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2);
enum control_msg_type { enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_KEYCODE, CONTROL_MSG_TYPE_INJECT_KEYCODE,
@ -45,7 +43,6 @@ 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 {
@ -65,7 +62,6 @@ 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;
@ -73,7 +69,7 @@ struct control_msg {
}; };
}; };
// buf size must be at least CONTROL_MSG_MAX_SIZE // buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE
// return the number of bytes written // return the number of bytes written
size_t size_t
control_msg_serialize(const struct control_msg *msg, unsigned char *buf); control_msg_serialize(const struct control_msg *msg, unsigned char *buf);

View File

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

View File

@ -9,7 +9,7 @@
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, device_msg_deserialize(const unsigned char *buf, size_t len,
struct device_msg *msg) { struct device_msg *msg) {
if (len < 5) { if (len < 3) {
// 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: {
size_t clipboard_len = buffer_read32be(&buf[1]); uint16_t clipboard_len = buffer_read16be(&buf[1]);
if (clipboard_len > len - 5) { if (clipboard_len > len - 3) {
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[5], clipboard_len); memcpy(text, &buf[3], clipboard_len);
} }
text[clipboard_len] = '\0'; text[clipboard_len] = '\0';
msg->clipboard.text = text; msg->clipboard.text = text;
return 5 + clipboard_len; return 3 + clipboard_len;
} }
default: default:
LOGW("Unknown device message type: %d", (int) msg->type); LOGW("Unknown device message type: %d", (int) msg->type);

View File

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

View File

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

View File

@ -23,7 +23,7 @@ fps_counter_init(struct fps_counter *counter) {
} }
counter->thread = NULL; counter->thread = NULL;
atomic_init(&counter->started, 0); SDL_AtomicSet(&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,16 +35,6 @@ 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) {
@ -80,10 +70,10 @@ run_fps_counter(void *data) {
mutex_lock(counter->mutex); mutex_lock(counter->mutex);
while (!counter->interrupted) { while (!counter->interrupted) {
while (!counter->interrupted && !is_started(counter)) { while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) {
cond_wait(counter->state_cond, counter->mutex); cond_wait(counter->state_cond, counter->mutex);
} }
while (!counter->interrupted && is_started(counter)) { while (!counter->interrupted && SDL_AtomicGet(&counter->started)) {
uint32_t now = SDL_GetTicks(); uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now); check_interval_expired(counter, now);
@ -106,7 +96,7 @@ fps_counter_start(struct fps_counter *counter) {
counter->nr_skipped = 0; counter->nr_skipped = 0;
mutex_unlock(counter->mutex); mutex_unlock(counter->mutex);
set_started(counter, true); SDL_AtomicSet(&counter->started, 1);
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
@ -124,13 +114,13 @@ fps_counter_start(struct fps_counter *counter) {
void void
fps_counter_stop(struct fps_counter *counter) { fps_counter_stop(struct fps_counter *counter) {
set_started(counter, false); SDL_AtomicSet(&counter->started, 0);
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 is_started(counter); return SDL_AtomicGet(&counter->started);
} }
void void
@ -155,7 +145,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 (!is_started(counter)) { if (!SDL_AtomicGet(&counter->started)) {
return; return;
} }
@ -168,7 +158,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 (!is_started(counter)) { if (!SDL_AtomicGet(&counter->started)) {
return; return;
} }

View File

@ -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
atomic_bool started; SDL_atomic_t started;
// the following fields are protected by the mutex // the following fields are protected by the mutex
bool interrupted; bool interrupted;

View File

@ -1,81 +1,42 @@
#include "input_manager.h" #include "input_manager.h"
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_keycode.h>
#include "config.h" #include "config.h"
#include "event_converter.h" #include "event_converter.h"
#include "util/lock.h" #include "util/lock.h"
#include "util/log.h" #include "util/log.h"
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
// coordinates (as provided in SDL mouse events)
//
// See my question:
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
static void
convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
SDL_Rect viewport;
float scale_x, scale_y;
SDL_RenderGetViewport(renderer, &viewport);
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
*x = (int) (*x / scale_x) - viewport.x;
*y = (int) (*y / scale_y) - viewport.y;
}
static struct point
get_mouse_point(struct screen *screen) {
int x;
int y;
SDL_GetMouseState(&x, &y);
convert_to_renderer_coordinates(screen->renderer, &x, &y);
return (struct point) {
.x = x,
.y = y,
};
}
static const int ACTION_DOWN = 1; static const int ACTION_DOWN = 1;
static const int ACTION_UP = 1 << 1; static const int ACTION_UP = 1 << 1;
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(unsigned mod) {
uint16_t sdl_mod = 0;
if (mod & SC_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (mod & SC_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (mod & SC_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (mod & SC_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (mod & SC_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (mod & SC_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static bool
is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
assert(im->sdl_shortcut_mods.count);
assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) {
if (im->sdl_shortcut_mods.data[i] == sdl_mod) {
return true;
}
}
return false;
}
void
input_manager_init(struct input_manager *im,
const struct scrcpy_options *options)
{
im->control = options->control;
im->forward_key_repeat = options->forward_key_repeat;
im->prefer_text = options->prefer_text;
im->forward_all_clicks = options->forward_all_clicks;
im->legacy_paste = options->legacy_paste;
const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods;
assert(shortcut_mods->count);
assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < shortcut_mods->count; ++i) {
uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]);
assert(sdl_mod);
im->sdl_shortcut_mods.data[i] = sdl_mod;
}
im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false;
}
static void static void
send_keycode(struct controller *controller, enum android_keycode keycode, send_keycode(struct controller *controller, enum android_keycode keycode,
int actions, const char *name) { int actions, const char *name) {
@ -84,7 +45,6 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
msg.inject_keycode.keycode = keycode; msg.inject_keycode.keycode = keycode;
msg.inject_keycode.metastate = 0; msg.inject_keycode.metastate = 0;
msg.inject_keycode.repeat = 0;
if (actions & ACTION_DOWN) { if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
@ -137,16 +97,6 @@ action_menu(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); send_keycode(controller, AKEYCODE_MENU, actions, "MENU");
} }
static inline void
action_copy(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_COPY, actions, "COPY");
}
static inline void
action_cut(struct controller *controller, int actions) {
send_keycode(controller, AKEYCODE_CUT, actions, "CUT");
}
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
static void static void
press_back_or_turn_screen_on(struct controller *controller) { press_back_or_turn_screen_on(struct controller *controller) {
@ -179,7 +129,17 @@ collapse_notification_panel(struct controller *controller) {
} }
static void static void
set_device_clipboard(struct controller *controller, bool paste) { request_device_clipboard(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device clipboard");
}
}
static void
set_device_clipboard(struct controller *controller) {
char *text = SDL_GetClipboardText(); 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());
@ -194,7 +154,6 @@ set_device_clipboard(struct controller *controller, bool paste) {
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);
@ -262,25 +221,9 @@ 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) {
if (is_shortcut_mod(im, SDL_GetModState())) {
// A shortcut must never generate text events
return;
}
if (!im->prefer_text) { if (!im->prefer_text) {
char c = event->text[0]; char c = event->text[0];
if (isalpha(c) || c == ' ') { if (isalpha(c) || c == ' ') {
@ -303,39 +246,9 @@ input_manager_process_text_input(struct input_manager *im,
} }
} }
static bool
simulate_virtual_finger(struct input_manager *im,
enum android_motionevent_action action,
struct point point) {
bool up = action == AMOTION_EVENT_ACTION_UP;
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
msg.inject_touch_event.action = action;
msg.inject_touch_event.position.screen_size = im->screen->frame_size;
msg.inject_touch_event.position.point = point;
msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.buttons = 0;
if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject virtual finger event'");
return false;
}
return true;
}
static struct point
inverse_point(struct point point, struct size size) {
point.x = size.width - point.x;
point.y = size.height - point.y;
return point;
}
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, uint32_t repeat) { bool prefer_text) {
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)) {
@ -348,7 +261,6 @@ 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;
@ -356,125 +268,129 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
void void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event) { const SDL_KeyboardEvent *event,
bool control) {
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
bool control = im->control; // ctrl: the Ctrl key
bool smod = is_shortcut_mod(im, event->keysym.mod); bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL);
bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT);
bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI);
// use Cmd on macOS, Ctrl on other platforms
#ifdef __APPLE__
bool cmd = !ctrl && meta;
#else
if (meta) {
// no shortcuts involve Meta on platforms other than macOS, and it must
// not be forwarded to the device
return;
}
bool cmd = ctrl; // && !meta, already guaranteed
#endif
if (alt) {
// no shortcuts involve Alt, and it must not be forwarded to the device
return;
}
struct controller *controller = im->controller; struct controller *controller = im->controller;
// capture all Ctrl events
if (ctrl || cmd) {
SDL_Keycode keycode = event->keysym.sym; SDL_Keycode keycode = event->keysym.sym;
bool down = event->type == SDL_KEYDOWN; bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat;
// The shortcut modifier is pressed
if (smod) {
int action = down ? ACTION_DOWN : ACTION_UP; int action = down ? ACTION_DOWN : ACTION_UP;
bool repeat = event->repeat;
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
switch (keycode) { switch (keycode) {
case SDLK_h: case SDLK_h:
if (control && !shift && !repeat) { // Ctrl+h on all platform, since Cmd+h is already captured by
// the system on macOS to hide the window
if (control && ctrl && !meta && !shift && !repeat) {
action_home(controller, action); action_home(controller, action);
} }
return; return;
case SDLK_b: // fall-through case SDLK_b: // fall-through
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
if (control && !shift && !repeat) { if (control && cmd && !shift && !repeat) {
action_back(controller, action); action_back(controller, action);
} }
return; return;
case SDLK_s: case SDLK_s:
if (control && !shift && !repeat) { if (control && cmd && !shift && !repeat) {
action_app_switch(controller, action); action_app_switch(controller, action);
} }
return; return;
case SDLK_m: case SDLK_m:
if (control && !shift && !repeat) { // Ctrl+m on all platform, since Cmd+m is already captured by
// the system on macOS to minimize the window
if (control && ctrl && !meta && !shift && !repeat) {
action_menu(controller, action); action_menu(controller, action);
} }
return; return;
case SDLK_p: case SDLK_p:
if (control && !shift && !repeat) { if (control && cmd && !shift && !repeat) {
action_power(controller, action); action_power(controller, action);
} }
return; return;
case SDLK_o: case SDLK_o:
if (control && !repeat && down) { if (control && cmd && !shift && down) {
enum screen_power_mode mode = shift set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
? SCREEN_POWER_MODE_NORMAL
: SCREEN_POWER_MODE_OFF;
set_screen_power_mode(controller, mode);
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (control && !shift) { if (control && cmd && !shift) {
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(controller, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (control && !shift) { if (control && cmd && !shift) {
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(controller, action);
} }
return; return;
case SDLK_LEFT:
if (!shift && !repeat && down) {
rotate_client_left(im->screen);
}
return;
case SDLK_RIGHT:
if (!shift && !repeat && down) {
rotate_client_right(im->screen);
}
return;
case SDLK_c: case SDLK_c:
if (control && !shift && !repeat) { if (control && cmd && !shift && !repeat && down) {
action_copy(controller, action); request_device_clipboard(controller);
}
return;
case SDLK_x:
if (control && !shift && !repeat) {
action_cut(controller, action);
} }
return; return;
case SDLK_v: case SDLK_v:
if (control && !repeat && down) { if (control && cmd && !repeat && down) {
if (shift || im->legacy_paste) { if (shift) {
// store the text in the device clipboard
set_device_clipboard(controller);
} else {
// inject the text as input events // inject the text as input events
clipboard_paste(controller); clipboard_paste(controller);
} else {
// store the text in the device clipboard and paste
set_device_clipboard(controller, true);
} }
} }
return; return;
case SDLK_f: case SDLK_f:
if (!shift && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_switch_fullscreen(im->screen); screen_switch_fullscreen(im->screen);
} }
return; return;
case SDLK_w: case SDLK_x:
if (!shift && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(im->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && !repeat && down) { if (!shift && cmd && !repeat && down) {
screen_resize_to_pixel_perfect(im->screen); screen_resize_to_pixel_perfect(im->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && !repeat && down) { if (!shift && cmd && !repeat && down) {
struct fps_counter *fps_counter = struct fps_counter *fps_counter =
im->video_buffer->fps_counter; im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter); switch_fps_counter_state(fps_counter);
} }
return; return;
case SDLK_n: case SDLK_n:
if (control && !repeat && down) { if (control && cmd && !repeat && down) {
if (shift) { if (shift) {
collapse_notification_panel(controller); collapse_notification_panel(controller);
} else { } else {
@ -483,7 +399,7 @@ input_manager_process_key(struct input_manager *im,
} }
return; return;
case SDLK_r: case SDLK_r:
if (control && !shift && !repeat && down) { if (control && cmd && !shift && !repeat && down) {
rotate_device(controller); rotate_device(controller);
} }
return; return;
@ -496,28 +412,8 @@ input_manager_process_key(struct input_manager *im,
return; return;
} }
if (event->repeat) {
if (!im->forward_key_repeat) {
return;
}
++im->repeat;
} else {
im->repeat = 0;
}
if (ctrl && !shift && keycode == SDLK_v && down && !repeat) {
if (im->legacy_paste) {
// inject the text as input events
clipboard_paste(controller);
return;
}
// Synchronize the computer clipboard to the device clipboard before
// sending Ctrl+v, to allow seamless copy-paste.
set_device_clipboard(controller, false);
}
struct control_msg msg; struct control_msg msg;
if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) { if (convert_input_key(event, &msg, im->prefer_text)) {
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'"); LOGW("Could not request 'inject keycode'");
} }
@ -531,8 +427,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 = to->inject_touch_event.position.point.x = from->x;
screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.position.point.y = 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);
@ -551,18 +447,10 @@ input_manager_process_mouse_motion(struct input_manager *im,
return; return;
} }
struct control_msg msg; struct control_msg msg;
if (!convert_mouse_motion(event, im->screen, &msg)) { if (convert_mouse_motion(event, im->screen, &msg)) {
return;
}
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'"); LOGW("Could not request 'inject mouse motion event'");
} }
if (im->vfinger_down) {
struct point mouse = msg.inject_touch_event.position.point;
struct point vfinger = inverse_point(mouse, im->screen->frame_size);
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
} }
} }
@ -575,19 +463,13 @@ 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 = screen->frame_size; to->inject_touch_event.position.screen_size = frame_size;
int dw;
int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
// SDL touch event coordinates are normalized in the range [0; 1] // SDL touch event coordinates are normalized in the range [0; 1]
int32_t x = from->x * dw; to->inject_touch_event.position.point.x = from->x * frame_size.width;
int32_t y = from->y * dh; to->inject_touch_event.position.point.y = from->y * frame_size.height;
to->inject_touch_event.position.point =
screen_convert_drawable_to_frame_coords(screen, x, y);
to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.pressure = from->pressure;
to->inject_touch_event.buttons = 0; to->inject_touch_event.buttons = 0;
return true; return true;
@ -604,6 +486,13 @@ 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) {
@ -615,10 +504,9 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.screen_size = screen->frame_size;
to->inject_touch_event.position.point = to->inject_touch_event.position.point.x = from->x;
screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.position.point.y = from->y;
to->inject_touch_event.pressure = to->inject_touch_event.pressure = 1.f;
from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f;
to->inject_touch_event.buttons = to->inject_touch_event.buttons =
convert_mouse_buttons(SDL_BUTTON(from->button)); convert_mouse_buttons(SDL_BUTTON(from->button));
@ -627,16 +515,13 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
void void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event) { const SDL_MouseButtonEvent *event,
bool control = im->control; bool control) {
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate // simulated from touch events, so it's a duplicate
return; return;
} }
if (event->type == SDL_MOUSEBUTTONDOWN) {
bool down = event->type == SDL_MOUSEBUTTONDOWN;
if (!im->forward_all_clicks && down) {
if (control && event->button == SDL_BUTTON_RIGHT) { if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im->controller); press_back_or_turn_screen_on(im->controller);
return; return;
@ -645,15 +530,10 @@ 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) {
int32_t x = event->x; bool outside =
int32_t y = event->y; is_outside_device_screen(im, event->x, 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;
@ -667,52 +547,19 @@ input_manager_process_mouse_button(struct input_manager *im,
} }
struct control_msg msg; struct control_msg msg;
if (!convert_mouse_button(event, im->screen, &msg)) { if (convert_mouse_button(event, im->screen, &msg)) {
return;
}
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'"); LOGW("Could not request 'inject mouse button event'");
return;
} }
// Pinch-to-zoom simulation.
//
// If Ctrl is hold when the left-click button is pressed, then
// pinch-to-zoom mode is enabled: on every mouse event until the left-click
// button is released, an additional "virtual finger" event is generated,
// having a position inverted through the center of the screen.
//
// In other words, the center of the rotation/scaling is the center of the
// screen.
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL))
if ((down && !im->vfinger_down && CTRL_PRESSED)
|| (!down && im->vfinger_down)) {
struct point mouse = msg.inject_touch_event.position.point;
struct point vfinger = inverse_point(mouse, im->screen->frame_size);
enum android_motionevent_action action = down
? AMOTION_EVENT_ACTION_DOWN
: AMOTION_EVENT_ACTION_UP;
if (!simulate_virtual_finger(im, action, vfinger)) {
return;
}
im->vfinger_down = down;
} }
} }
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 = screen_convert_window_to_frame_coords(screen, .point = get_mouse_point(screen),
mouse_x, mouse_y),
}; };
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;

View File

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

View File

@ -1,6 +1,5 @@
#include "scrcpy.h" #include "scrcpy.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
@ -30,24 +29,6 @@ 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__
@ -57,23 +38,20 @@ 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;
@ -100,7 +78,7 @@ main(int argc, char *argv[]) {
#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE) #if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE)
if (res != 0) { if (res != 0) {
fprintf(stderr, "Press Enter to continue...\n"); fprintf(stderr, "Press any key to continue...\n");
getchar(); getchar();
} }
#endif #endif

View File

@ -1,56 +0,0 @@
#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);
}

View File

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

View File

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

View File

@ -63,7 +63,7 @@ recorder_queue_clear(struct recorder_queue *queue) {
bool bool
recorder_init(struct recorder *recorder, recorder_init(struct recorder *recorder,
const char *filename, const char *filename,
enum sc_record_format format, enum recorder_format format,
struct size declared_frame_size) { struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename); recorder->filename = SDL_strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
@ -105,10 +105,10 @@ recorder_destroy(struct recorder *recorder) {
} }
static const char * static const char *
recorder_get_format_name(enum sc_record_format format) { recorder_get_format_name(enum recorder_format format) {
switch (format) { switch (format) {
case SC_RECORD_FORMAT_MP4: return "mp4"; case RECORDER_FORMAT_MP4: return "mp4";
case SC_RECORD_FORMAT_MKV: return "matroska"; case RECORDER_FORMAT_MKV: return "matroska";
default: return NULL; default: return NULL;
} }
} }
@ -361,14 +361,12 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) {
if (recorder->failed) { if (recorder->failed) {
// reject any new packet (this will stop the stream) // reject any new packet (this will stop the stream)
mutex_unlock(recorder->mutex);
return false; return false;
} }
struct record_packet *rec = record_packet_new(packet); struct record_packet *rec = record_packet_new(packet);
if (!rec) { if (!rec) {
LOGC("Could not allocate record packet"); LOGC("Could not allocate record packet");
mutex_unlock(recorder->mutex);
return false; return false;
} }

View File

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

View File

@ -7,12 +7,6 @@
#include <sys/time.h> #include <sys/time.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include <winsock2.h>
# include <windows.h>
#endif
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "common.h" #include "common.h"
@ -48,32 +42,12 @@ 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
// initialized later
.prefer_text = false,
.sdl_shortcut_mods = {
.data = {0},
.count = 0,
},
}; };
#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, const char *render_driver, sdl_init_and_configure(bool display) {
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());
@ -82,22 +56,10 @@ sdl_init_and_configure(bool display, const char *render_driver,
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;
} }
if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) {
LOGW("Could not set render driver");
}
// Linear filtering // Linear filtering
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering"); LOGW("Could not enable linear filtering");
@ -122,13 +84,8 @@ sdl_init_and_configure(bool display, const char *render_driver,
LOGW("Could not disable minimize on focus loss"); LOGW("Could not disable minimize on focus loss");
} }
if (disable_screensaver) { // Do not disable the screensaver when scrcpy is running
LOGD("Screensaver disabled");
SDL_DisableScreenSaver();
} else {
LOGD("Screensaver enabled");
SDL_EnableScreenSaver(); SDL_EnableScreenSaver();
}
return true; return true;
} }
@ -149,9 +106,8 @@ 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) {
// In practice, it seems to always be called from the same thread in // called from another thread, not very safe, but it's a workaround!
// that specific case. Anyway, it's just a workaround. screen_render(&screen);
screen_render(&screen, true);
} }
return 0; return 0;
} }
@ -170,7 +126,7 @@ enum event_result {
}; };
static enum event_result static enum event_result
handle_event(SDL_Event *event, const struct scrcpy_options *options) { handle_event(SDL_Event *event, bool control) {
switch (event->type) { switch (event->type) {
case EVENT_STREAM_STOPPED: case EVENT_STREAM_STOPPED:
LOGD("Video stream stopped"); LOGD("Video stream stopped");
@ -192,7 +148,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
screen_handle_window_event(&screen, &event->window); screen_handle_window_event(&screen, &event->window);
break; break;
case SDL_TEXTINPUT: case SDL_TEXTINPUT:
if (!options->control) { if (!control) {
break; break;
} }
input_manager_process_text_input(&input_manager, &event->text); input_manager_process_text_input(&input_manager, &event->text);
@ -201,16 +157,16 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
case SDL_KEYUP: case SDL_KEYUP:
// some key events do not interact with the device, so process the // some key events do not interact with the device, so process the
// event even if control is disabled // event even if control is disabled
input_manager_process_key(&input_manager, &event->key); input_manager_process_key(&input_manager, &event->key, control);
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
if (!options->control) { if (!control) {
break; break;
} }
input_manager_process_mouse_motion(&input_manager, &event->motion); input_manager_process_mouse_motion(&input_manager, &event->motion);
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
if (!options->control) { if (!control) {
break; break;
} }
input_manager_process_mouse_wheel(&input_manager, &event->wheel); input_manager_process_mouse_wheel(&input_manager, &event->wheel);
@ -219,7 +175,8 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process // some mouse events do not interact with the device, so process
// the event even if control is disabled // the event even if control is disabled
input_manager_process_mouse_button(&input_manager, &event->button); input_manager_process_mouse_button(&input_manager, &event->button,
control);
break; break;
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
@ -227,7 +184,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
input_manager_process_touch(&input_manager, &event->tfinger); input_manager_process_touch(&input_manager, &event->tfinger);
break; break;
case SDL_DROPFILE: { case SDL_DROPFILE: {
if (!options->control) { if (!control) {
break; break;
} }
file_handler_action_t action; file_handler_action_t action;
@ -244,15 +201,16 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
} }
static bool static bool
event_loop(const struct scrcpy_options *options) { event_loop(bool display, bool control) {
(void) display;
#ifdef CONTINUOUS_RESIZING_WORKAROUND #ifdef CONTINUOUS_RESIZING_WORKAROUND
if (options->display) { if (display) {
SDL_AddEventWatch(event_watcher, NULL); SDL_AddEventWatch(event_watcher, NULL);
} }
#endif #endif
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, options); enum event_result result = handle_event(&event, control);
switch (result) { switch (result) {
case EVENT_RESULT_STOPPED_BY_USER: case EVENT_RESULT_STOPPED_BY_USER:
return true; return true;
@ -266,6 +224,21 @@ event_loop(const struct scrcpy_options *options) {
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) {
@ -306,7 +279,6 @@ bool
scrcpy(const struct scrcpy_options *options) { scrcpy(const struct scrcpy_options *options) {
bool record = !!options->record_filename; bool record = !!options->record_filename;
struct server_params params = { struct server_params params = {
.log_level = options->log_level,
.crop = options->crop, .crop = options->crop,
.port_range = options->port_range, .port_range = options->port_range,
.max_size = options->max_size, .max_size = options->max_size,
@ -315,16 +287,19 @@ scrcpy(const struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation, .lock_video_orientation = options->lock_video_orientation,
.control = options->control, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.codec_options = options->codec_options,
.encoder_name = options->encoder_name,
.force_adb_forward = options->force_adb_forward,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
return false; return false;
} }
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;
@ -335,8 +310,7 @@ scrcpy(const struct scrcpy_options *options) {
bool controller_initialized = false; bool controller_initialized = false;
bool controller_started = false; bool controller_started = false;
if (!sdl_init_and_configure(options->display, options->render_driver, if (!sdl_init_and_configure(options->display)) {
options->disable_screensaver)) {
goto end; goto end;
} }
@ -422,8 +396,7 @@ 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;
} }
@ -442,9 +415,14 @@ scrcpy(const struct scrcpy_options *options) {
} }
} }
input_manager_init(&input_manager, options); if (options->show_touches) {
wait_show_touches(proc_show_touches);
show_touches_waited = true;
}
ret = event_loop(options); input_manager.prefer_text = options->prefer_text;
ret = event_loop(options->display, options->control);
LOGD("quit..."); LOGD("quit...");
screen_destroy(&screen); screen_destroy(&screen);
@ -498,6 +476,16 @@ 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;

View File

@ -2,46 +2,12 @@
#define SCRCPY_H #define SCRCPY_H
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "config.h" #include "config.h"
#include "common.h"
enum sc_log_level { #include "input_manager.h"
SC_LOG_LEVEL_DEBUG, #include "recorder.h"
SC_LOG_LEVEL_INFO,
SC_LOG_LEVEL_WARN,
SC_LOG_LEVEL_ERROR,
};
enum sc_record_format {
SC_RECORD_FORMAT_AUTO,
SC_RECORD_FORMAT_MP4,
SC_RECORD_FORMAT_MKV,
};
#define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod {
SC_MOD_LCTRL = 1 << 0,
SC_MOD_RCTRL = 1 << 1,
SC_MOD_LALT = 1 << 2,
SC_MOD_RALT = 1 << 3,
SC_MOD_LSUPER = 1 << 4,
SC_MOD_RSUPER = 1 << 5,
};
struct sc_shortcut_mods {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
};
struct sc_port_range {
uint16_t first;
uint16_t last;
};
#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
struct scrcpy_options { struct scrcpy_options {
const char *serial; const char *serial;
@ -49,20 +15,14 @@ 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; enum recorder_format record_format;
const char *codec_options; struct port_range port_range;
const char *encoder_name;
enum sc_log_level log_level;
enum sc_record_format record_format;
struct sc_port_range port_range;
struct sc_shortcut_mods shortcut_mods;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
uint8_t rotation; int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint16_t display_id; uint16_t display_id;
@ -75,13 +35,6 @@ 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;
bool forward_key_repeat;
bool forward_all_clicks;
bool legacy_paste;
}; };
#define SCRCPY_OPTIONS_DEFAULT { \ #define SCRCPY_OPTIONS_DEFAULT { \
@ -90,26 +43,17 @@ struct scrcpy_options {
.record_filename = NULL, \ .record_filename = NULL, \
.window_title = NULL, \ .window_title = NULL, \
.push_target = NULL, \ .push_target = NULL, \
.render_driver = NULL, \ .record_format = RECORDER_FORMAT_AUTO, \
.codec_options = NULL, \
.encoder_name = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \
.record_format = SC_RECORD_FORMAT_AUTO, \
.port_range = { \ .port_range = { \
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
}, \ }, \
.shortcut_mods = { \
.data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
.count = 2, \
}, \
.max_size = DEFAULT_MAX_SIZE, \ .max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \ .max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.rotation = 0, \ .window_x = WINDOW_POSITION_UNDEFINED, \
.window_x = SC_WINDOW_POSITION_UNDEFINED, \ .window_y = WINDOW_POSITION_UNDEFINED, \
.window_y = SC_WINDOW_POSITION_UNDEFINED, \
.window_width = 0, \ .window_width = 0, \
.window_height = 0, \ .window_height = 0, \
.display_id = 0, \ .display_id = 0, \
@ -122,13 +66,6 @@ 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, \
.forward_key_repeat = true, \
.forward_all_clicks = false, \
.legacy_paste = false, \
} }
bool bool

View File

@ -8,7 +8,6 @@
#include "common.h" #include "common.h"
#include "compat.h" #include "compat.h"
#include "icon.xpm" #include "icon.xpm"
#include "scrcpy.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/lock.h" #include "util/lock.h"
@ -16,25 +15,12 @@
#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(const struct screen *screen) { get_window_size(SDL_Window *window) {
int width; int width;
int height; int height;
SDL_GetWindowSize(screen->window, &width, &height); SDL_GetWindowSize(window, &width, &height);
struct size size; struct size size;
size.width = width; size.width = width;
@ -42,12 +28,31 @@ get_window_size(const struct screen *screen) {
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) {
assert(!screen->fullscreen); // setting the window size during fullscreen is implementation defined,
assert(!screen->maximized); // so apply the resize only after fullscreen is disabled
SDL_SetWindowSize(screen->window, new_size.width, new_size.height); screen->windowed_window_size = new_size;
apply_windowed_size(screen);
} }
// get the preferred display bounds (i.e. the screen bounds with some margins) // get the preferred display bounds (i.e. the screen bounds with some margins)
@ -69,179 +74,102 @@ 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 content_size) { get_optimal_size(struct size current_size, struct size frame_size) {
if (content_size.width == 0 || content_size.height == 0) { if (frame_size.width == 0 || frame_size.height == 0) {
// avoid division by 0 // avoid division by 0
return current_size; return current_size;
} }
struct size window_size;
struct size display_size; struct size display_size;
// 32 bits because we need to multiply two 16 bits values
uint32_t w;
uint32_t h;
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
window_size.width = current_size.width; w = current_size.width;
window_size.height = current_size.height; h = current_size.height;
} else { } else {
window_size.width = MIN(current_size.width, display_size.width); w = MIN(current_size.width, display_size.width);
window_size.height = MIN(current_size.height, display_size.height); h = MIN(current_size.height, display_size.height);
} }
if (is_optimal_size(window_size, content_size)) { bool keep_width = frame_size.width * h > frame_size.height * w;
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
window_size.height = content_size.height * window_size.width h = frame_size.height * w / frame_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)
window_size.width = content_size.width * window_size.height w = frame_size.width * h / frame_size.height;
/ content_size.height;
} }
return window_size; // w and h must fit into 16 bits
assert(w < 0x10000 && h < 0x10000);
return (struct size) {w, h};
} }
// same as get_optimal_size(), but read the current size from the window // same as get_optimal_size(), but read the current size from the window
static inline struct size static inline struct size
get_optimal_window_size(const struct screen *screen, struct size content_size) { get_optimal_window_size(const struct screen *screen, struct size frame_size) {
struct size window_size = get_window_size(screen); struct size windowed_size = get_windowed_window_size(screen);
return get_optimal_size(window_size, content_size); return get_optimal_size(windowed_size, frame_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 content_size, uint16_t req_width, get_initial_optimal_size(struct size frame_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(content_size, content_size); window_size = get_optimal_size(frame_size, frame_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 * content_size.width window_size.width = (uint32_t) req_height * frame_size.width
/ content_size.height; / frame_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 * content_size.height window_size.height = (uint32_t) req_width * frame_size.height
/ content_size.width; / frame_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(struct screen *screen) { create_texture(SDL_Renderer *renderer, struct size frame_size) {
SDL_Renderer *renderer = screen->renderer; return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
struct size size = screen->frame_size;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING, SDL_TEXTUREACCESS_STREAMING,
size.width, size.height); frame_size.width, frame_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(content_size, window_width, window_height); get_initial_optimal_size(frame_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;
@ -258,9 +186,9 @@ screen_init_rendering(struct screen *screen, const char *window_title,
window_flags |= SDL_WINDOW_BORDERLESS; window_flags |= SDL_WINDOW_BORDERLESS;
} }
int x = window_x != SC_WINDOW_POSITION_UNDEFINED int x = window_x != WINDOW_POSITION_UNDEFINED
? window_x : (int) SDL_WINDOWPOS_UNDEFINED; ? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = window_y != SC_WINDOW_POSITION_UNDEFINED int y = window_y != WINDOW_POSITION_UNDEFINED
? window_y : (int) SDL_WINDOWPOS_UNDEFINED; ? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y, screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height, window_size.width, window_size.height,
@ -278,35 +206,11 @@ screen_init_rendering(struct screen *screen, const char *window_title,
return false; return false;
} }
SDL_RendererInfo renderer_info; if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width,
int r = SDL_GetRendererInfo(screen->renderer, &renderer_info); frame_size.height)) {
const char *renderer_name = r ? NULL : renderer_info.name; LOGE("Could not set renderer logical size: %s", SDL_GetError());
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); screen_destroy(screen);
return false;
// starts with "opengl"
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (screen->use_opengl) {
struct sc_opengl *gl = &screen->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
screen->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
} }
SDL_Surface *icon = read_xpm(icon_xpm); SDL_Surface *icon = read_xpm(icon_xpm);
@ -319,19 +223,14 @@ 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); screen->texture = create_texture(screen->renderer, frame_size);
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;
} }
// Reset the window size to trigger a SIZE_CHANGED event, to workaround screen->windowed_window_size = window_size;
// 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;
} }
@ -354,82 +253,35 @@ 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); screen->texture = create_texture(screen->renderer, new_frame_size);
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;
@ -446,13 +298,6 @@ 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
@ -467,41 +312,14 @@ 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, false); screen_render(screen);
return true; return true;
} }
void void
screen_render(struct screen *screen, bool update_content_rect) { screen_render(struct screen *screen) {
if (update_content_rect) {
screen_update_content_rect(screen);
}
SDL_RenderClear(screen->renderer); SDL_RenderClear(screen->renderer);
if (screen->rotation == 0) { SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
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 = &rect;
} 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);
} }
@ -514,25 +332,27 @@ screen_switch_fullscreen(struct screen *screen) {
} }
screen->fullscreen = !screen->fullscreen; screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen && !screen->maximized) { apply_windowed_size(screen);
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, true); screen_render(screen);
} }
void void
screen_resize_to_fit(struct screen *screen) { screen_resize_to_fit(struct screen *screen) {
if (screen->fullscreen || screen->maximized) { if (screen->fullscreen) {
return; return;
} }
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
struct size optimal_size = struct size optimal_size =
get_optimal_window_size(screen, screen->content_size); get_optimal_window_size(screen, screen->frame_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: %ux%u", optimal_size.width, LOGD("Resized to optimal size");
optimal_size.height);
} }
void void
@ -546,10 +366,9 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
screen->maximized = false; screen->maximized = false;
} }
struct size content_size = screen->content_size; SDL_SetWindowSize(screen->window, screen->frame_size.width,
SDL_SetWindowSize(screen->window, content_size.width, content_size.height); screen->frame_size.height);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width, LOGD("Resized to pixel-perfect");
content_size.height);
} }
void void
@ -557,80 +376,39 @@ 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, true); screen_render(screen);
break; break;
case SDL_WINDOWEVENT_SIZE_CHANGED: case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(screen, true); if (!screen->fullscreen && !screen->maximized) {
// Backup the previous size: if we receive the MAXIMIZED event,
// then the new size must be ignored (it's the maximized size).
// We could not rely on the window flags due to race conditions
// (they could be updated asynchronously, at least on X11).
screen->windowed_window_size_backup =
screen->windowed_window_size;
// Save the windowed size, so that it is available once the
// window is maximized or fullscreen is enabled.
screen->windowed_window_size = get_window_size(screen->window);
}
screen_render(screen);
break; 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_pending_resize(screen); apply_windowed_size(screen);
break; break;
} }
} }
struct point
screen_convert_drawable_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;
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;
}
struct point
screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
screen_hidpi_scale_coords(screen, &x, &y);
return screen_convert_drawable_to_frame_coords(screen, x, y);
}
void
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) {
// take the HiDPI scaling (dw/ww and dh/wh) into account
int ww, wh, dw, dh;
SDL_GetWindowSize(screen->window, &ww, &wh);
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
// scale for HiDPI (64 bits for intermediate multiplications)
*x = (int64_t) *x * dw / ww;
*y = (int64_t) *y * dh / wh;
}

View File

@ -7,7 +7,8 @@
#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;
@ -15,58 +16,38 @@ 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;
struct size content_size; // rotated frame_size // The window size the last time it was not maximized or fullscreen.
struct size windowed_window_size;
bool resize_pending; // resize requested while fullscreen or maximized // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
// The content size the last time the window was not maximized or // able to revert the size to its non-maximized value.
// fullscreen (meaningful only when resize_pending is true) struct size windowed_window_size_backup;
struct size windowed_content_size;
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
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, \ .width = 0, \
.height = 0, \ .height = 0, \
}, \ }, \
.content_size = { \ .windowed_window_size = { \
.width = 0, \ .width = 0, \
.height = 0, \ .height = 0, \
}, \ }, \
.resize_pending = false, \ .windowed_window_size_backup = { \
.windowed_content_size = { \
.width = 0, \ .width = 0, \
.height = 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
@ -74,13 +55,12 @@ 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 SC_WINDOW_POSITION_UNDEFINED // 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
@ -95,11 +75,8 @@ 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, bool update_content_rect); screen_render(struct screen *screen);
// switch the fullscreen mode // switch the fullscreen mode
void void
@ -113,31 +90,8 @@ 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_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// convert point from drawable coordinates to frame coordinates
// x and y are expressed in pixels
struct point
screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// Convert coordinates from window to drawable.
// Events are expressed in window coordinates, but content is expressed in
// drawable coordinates. They are the same if HiDPI scaling is 1, but differ
// otherwise.
void
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y);
#endif #endif

View File

@ -143,7 +143,7 @@ listen_on_port(uint16_t port) {
static bool static bool
enable_tunnel_reverse_any_port(struct server *server, enable_tunnel_reverse_any_port(struct server *server,
struct sc_port_range port_range) { struct port_range port_range) {
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
if (!enable_tunnel_reverse(server->serial, port)) { if (!enable_tunnel_reverse(server->serial, port)) {
@ -172,7 +172,7 @@ enable_tunnel_reverse_any_port(struct server *server,
// check before incrementing to avoid overflow on port 65535 // check before incrementing to avoid overflow on port 65535
if (port < port_range.last) { if (port < port_range.last) {
LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
port, (uint16_t) (port + 1)); port, port + 1);
port++; port++;
continue; continue;
} }
@ -189,7 +189,7 @@ enable_tunnel_reverse_any_port(struct server *server,
static bool static bool
enable_tunnel_forward_any_port(struct server *server, enable_tunnel_forward_any_port(struct server *server,
struct sc_port_range port_range) { struct port_range port_range) {
server->tunnel_forward = true; server->tunnel_forward = true;
uint16_t port = port_range.first; uint16_t port = port_range.first;
for (;;) { for (;;) {
@ -201,7 +201,7 @@ enable_tunnel_forward_any_port(struct server *server,
if (port < port_range.last) { if (port < port_range.last) {
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
port, (uint16_t) (port + 1)); port, port + 1);
port++; port++;
continue; continue;
} }
@ -217,46 +217,24 @@ enable_tunnel_forward_any_port(struct server *server,
} }
static bool static bool
enable_tunnel_any_port(struct server *server, struct sc_port_range port_range, 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)) { if (enable_tunnel_reverse_any_port(server, port_range)) {
return true; return true;
} }
// if "adb reverse" does not work (e.g. over "adb connect"), it // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// fallbacks to "adb forward", so the app socket is the client // "adb forward", so the app socket is the client
LOGW("'adb reverse' failed, fallback to 'adb forward'"); LOGW("'adb reverse' failed, fallback to 'adb forward'");
}
return enable_tunnel_forward_any_port(server, port_range); return enable_tunnel_forward_any_port(server, port_range);
} }
static const char *
log_level_to_server_string(enum sc_log_level level) {
switch (level) {
case SC_LOG_LEVEL_DEBUG:
return "debug";
case SC_LOG_LEVEL_INFO:
return "info";
case SC_LOG_LEVEL_WARN:
return "warn";
case SC_LOG_LEVEL_ERROR:
return "error";
default:
assert(!"unexpected log level");
return "(unknown)";
}
}
static process_t static process_t
execute_server(struct server *server, const struct server_params *params) { execute_server(struct server *server, const struct server_params *params) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6]; char max_fps_string[6];
char lock_video_orientation_string[5]; char lock_video_orientation_string[3];
char display_id_string[6]; char display_id_string[6];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
@ -281,7 +259,6 @@ execute_server(struct server *server, const struct server_params *params) {
"/", // unused "/", // unused
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
SCRCPY_VERSION, SCRCPY_VERSION,
log_level_to_server_string(params->log_level),
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
max_fps_string, max_fps_string,
@ -291,10 +268,6 @@ execute_server(struct server *server, const struct server_params *params) {
"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, display_id_string,
params->show_touches ? "true" : "false",
params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-",
params->encoder_name ? params->encoder_name : "-",
}; };
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port " LOGI("Server debugger waiting for a client on device port "
@ -365,7 +338,7 @@ run_wait_server(void *data) {
// no need for synchronization, server_socket is initialized before this // no need for synchronization, server_socket is initialized before this
// thread was created // thread was created
if (server->server_socket != INVALID_SOCKET if (server->server_socket != INVALID_SOCKET
&& !atomic_flag_test_and_set(&server->server_socket_closed)) { && SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) {
// On Linux, accept() is unblocked by shutdown(), but on Windows, it is // On Linux, accept() is unblocked by shutdown(), but on Windows, it is
// unblocked by closesocket(). Therefore, call both (close_socket()). // unblocked by closesocket(). Therefore, call both (close_socket()).
close_socket(server->server_socket); close_socket(server->server_socket);
@ -390,8 +363,7 @@ server_start(struct server *server, const char *serial,
goto error1; goto error1;
} }
if (!enable_tunnel_any_port(server, params->port_range, if (!enable_tunnel_any_port(server, params->port_range)) {
params->force_adb_forward)) {
goto error1; goto error1;
} }
@ -421,11 +393,8 @@ server_start(struct server *server, const char *serial,
error2: error2:
if (!server->tunnel_forward) { if (!server->tunnel_forward) {
bool was_closed = // the wait server thread is not started, SDL_AtomicSet() is sufficient
atomic_flag_test_and_set(&server->server_socket_closed); SDL_AtomicSet(&server->server_socket_closed, 1);
// the thread is not started, the flag could not be already set
assert(!was_closed);
(void) was_closed;
close_socket(server->server_socket); close_socket(server->server_socket);
} }
disable_tunnel(server); disable_tunnel(server);
@ -449,7 +418,7 @@ server_connect_to(struct server *server) {
} }
// we don't need the server socket anymore // we don't need the server socket anymore
if (!atomic_flag_test_and_set(&server->server_socket_closed)) { if (SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) {
// close it from here // close it from here
close_socket(server->server_socket); close_socket(server->server_socket);
// otherwise, it is closed by run_wait_server() // otherwise, it is closed by run_wait_server()
@ -481,7 +450,7 @@ 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
&& !atomic_flag_test_and_set(&server->server_socket_closed)) { && SDL_AtomicCAS(&server->server_socket_closed, 0, 1)) {
close_socket(server->server_socket); close_socket(server->server_socket);
} }
if (server->video_socket != INVALID_SOCKET) { if (server->video_socket != INVALID_SOCKET) {

View File

@ -1,27 +1,25 @@
#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_atomic.h>
#include <SDL2/SDL_thread.h> #include <SDL2/SDL_thread.h>
#include "config.h" #include "config.h"
#include "command.h" #include "command.h"
#include "common.h" #include "common.h"
#include "scrcpy.h"
#include "util/log.h"
#include "util/net.h" #include "util/net.h"
struct server { struct server {
char *serial; char *serial;
process_t process; process_t process;
SDL_Thread *wait_server_thread; SDL_Thread *wait_server_thread;
atomic_flag server_socket_closed; SDL_atomic_t 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;
struct sc_port_range port_range; struct port_range port_range;
uint16_t local_port; // selected from port_range uint16_t local_port; // selected from port_range
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
@ -31,7 +29,7 @@ struct server {
.serial = NULL, \ .serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.wait_server_thread = NULL, \ .wait_server_thread = NULL, \
.server_socket_closed = ATOMIC_FLAG_INIT, \ .server_socket_closed = {0}, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \ .video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \ .control_socket = INVALID_SOCKET, \
@ -45,20 +43,14 @@ struct server {
} }
struct server_params { struct server_params {
enum sc_log_level log_level;
const char *crop; const char *crop;
const char *codec_options; struct port_range port_range;
const char *encoder_name;
struct sc_port_range port_range;
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
bool control; bool control;
uint16_t display_id; uint16_t display_id;
bool show_touches;
bool stay_awake;
bool force_adb_forward;
}; };
// init default values // init default values

View File

@ -14,6 +14,7 @@ struct video_buffer;
struct stream { struct stream {
socket_t socket; socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread; SDL_Thread *thread;
struct decoder *decoder; struct decoder *decoder;
struct recorder *recorder; struct recorder *recorder;

View File

@ -1,6 +1,6 @@
// for portability (kill, readlink, strdup, strtok_r) // for portability
#define _POSIX_C_SOURCE 200809L #define _POSIX_SOURCE // for kill()
#define _BSD_SOURCE #define _BSD_SOURCE // for readlink()
// modern glibc will complain without this // modern glibc will complain without this
#define _DEFAULT_SOURCE #define _DEFAULT_SOURCE
@ -14,7 +14,6 @@
#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/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>

View File

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

View File

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

View File

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

View File

@ -9,20 +9,18 @@ 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_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 14); assert(size == 10);
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)));
@ -36,13 +34,13 @@ static void test_serialize_inject_text(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 18); assert(size == 16);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_INJECT_TEXT, CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x00, 0x00, 0x0d, // text length 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)));
@ -51,22 +49,20 @@ 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_INJECT_TEXT_MAX_LENGTH + 1]; char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1];
memset(text, 'a', sizeof(text)); memset(text, 'a', sizeof(text));
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text; msg.inject_text.text = text;
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH);
unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH];
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x00; expected[1] = 0x01;
expected[2] = 0x00; expected[2] = 0x2c; // text length (16 bits)
expected[3] = 0x01; memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH);
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)));
} }
@ -92,7 +88,7 @@ static void test_serialize_inject_touch_event(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 28); assert(size == 28);
@ -127,7 +123,7 @@ static void test_serialize_inject_scroll_event(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 21); assert(size == 21);
@ -146,7 +142,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_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -161,7 +157,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_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -176,7 +172,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_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -191,7 +187,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_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -204,20 +200,18 @@ 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,
.set_clipboard = { .inject_text = {
.paste = true,
.text = "hello, world!", .text = "hello, world!",
}, },
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 19); assert(size == 16);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD,
1, // paste 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)));
@ -231,7 +225,7 @@ static void test_serialize_set_screen_power_mode(void) {
}, },
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
@ -247,7 +241,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_MAX_SIZE]; unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); int size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
@ -257,10 +251,7 @@ static void test_serialize_rotate_device(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
int main(int argc, char *argv[]) { int main(void) {
(void) argc;
(void) argv;
test_serialize_inject_keycode(); test_serialize_inject_keycode();
test_serialize_inject_text(); test_serialize_inject_text();
test_serialize_inject_text_long(); test_serialize_inject_text_long();

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.2' classpath 'com.android.tools.build:gradle:3.4.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
@ -19,9 +19,6 @@ allprojects {
google() google()
jcenter() jcenter()
} }
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:deprecation"
}
} }
task clean(type: Delete) { task clean(type: Delete) {

View File

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

View File

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

Binary file not shown.

View File

@ -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-6.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

33
gradlew vendored
View File

@ -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 or MSYS, switch paths to Windows format before running java # For Cygwin, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then if $cygwin ; 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" = "true" -o "$msys" = "true" ] ; then
else else
eval `echo args$i`="\"$arg\"" eval `echo args$i`="\"$arg\""
fi fi
i=`expr $i + 1` i=$((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,9 +175,14 @@ 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
View File

@ -29,9 +29,6 @@ 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"

View File

@ -1,6 +1,6 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '1.16', version: '1.12.1',
meson_version: '>= 0.48', meson_version: '>= 0.37',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',
'warning_level=2', 'warning_level=2',

View File

@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32: prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.3.1-win32-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \
357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \
ffmpeg-4.3.1-win32-shared ffmpeg-4.2.1-win32-shared
prepare-ffmpeg-dev-win32: prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.3.1-win32-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \
230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \
ffmpeg-4.3.1-win32-dev ffmpeg-4.2.1-win32-dev
prepare-ffmpeg-shared-win64: prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.3.1-win64-shared.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \
dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \
ffmpeg-4.3.1-win64-shared ffmpeg-4.2.1-win64-shared
prepare-ffmpeg-dev-win64: prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.3.1-win64-dev.zip \ @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \
2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \
ffmpeg-4.3.1-win64-dev ffmpeg-4.2.1-win64-dev
prepare-sdl2: prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \
e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \ a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \
SDL2-2.0.12 SDL2-2.0.10
prepare-adb: prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.4-windows.zip \ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \
413182fff6c5957911e231b9e97e6be4fc6a539035e3dfb580b5c54bd5950fee \ 2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
platform-tools platform-tools

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
set -e set -e
url="$1" url="$1"
sum="$2" sum="$2"

2
run
View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
# Run scrcpy generated in the specified BUILDDIR. # Run scrcpy generated in the specified BUILDDIR.
# #
# This provides the same feature as "ninja run", except that it is possible to # This provides the same feature as "ninja run", except that it is possible to

View File

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

View File

@ -1,13 +1,13 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion 30 compileSdkVersion 29
defaultConfig { defaultConfig {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 29
versionCode 19 versionCode 14
versionName "1.16" versionName "1.12.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
# #
# This script generates the scrcpy binary "manually" (without gradle). # This script generates the scrcpy binary "manually" (without gradle).
# #
@ -12,10 +12,10 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.16 SCRCPY_VERSION_NAME=1.12.1
PLATFORM=${ANDROID_PLATFORM:-30} PLATFORM=${ANDROID_PLATFORM:-29}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes" CLASSES_DIR="$BUILD_DIR/classes"
@ -42,8 +42,6 @@ echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl" cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ "$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/view/IRotationWatcher.aidl android/view/IRotationWatcher.aidl
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
@ -57,7 +55,6 @@ cd "$CLASSES_DIR"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \ --output "$BUILD_DIR/classes.dex" \
android/view/*.class \ android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class com/genymobile/scrcpy/wrappers/*.class

View File

@ -3,9 +3,7 @@
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',
# gradle is responsible for tracking source changes build_always: true, # 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,

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
# Wrapper script to invoke gradle from meson # Wrapper script to invoke gradle from meson
set -e set -e

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) 2008, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content;
/**
* {@hide}
*/
oneway interface IOnPrimaryClipChangedListener {
void dispatchPrimaryClipChanged();
}

View File

@ -1,86 +0,0 @@
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, boolean restoreNormalPowerMode) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
}
private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
restoreStayOn), String.valueOf(restoreNormalPowerMode)};
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]);
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
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));
}
}
}
if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
if (Device.isScreenOn()) {
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,23 +1,20 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.os.Build; import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Controller { public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1; private static final int DEVICE_ID_VIRTUAL = -1;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private final Device device; private final Device device;
private final DesktopConnection connection; private final DesktopConnection connection;
private final DeviceMessageSender sender; private final DeviceMessageSender sender;
@ -29,8 +26,6 @@ public class Controller {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection) { public Controller(Device device, DesktopConnection connection) {
this.device = device; this.device = device;
this.connection = connection; this.connection = connection;
@ -54,8 +49,8 @@ public class Controller {
public void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, power on the device
if (!Device.isScreenOn()) { if (!device.isScreenOn()) {
device.injectKeycode(KeyEvent.KEYCODE_POWER); 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.
@ -81,7 +76,7 @@ public class Controller {
switch (msg.getType()) { switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE: case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
} }
break; break;
case ControlMessage.TYPE_INJECT_TEXT: case ControlMessage.TYPE_INJECT_TEXT:
@ -105,43 +100,33 @@ public class Controller {
} }
break; break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
Device.expandNotificationPanel(); device.expandNotificationPanel();
break; break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
Device.collapsePanels(); device.collapsePanels();
break; break;
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
String clipboardText = Device.getClipboardText(); String clipboardText = device.getClipboardText();
if (clipboardText != null) {
sender.pushClipboardText(clipboardText); sender.pushClipboardText(clipboardText);
}
break; break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste()); device.setClipboardText(msg.getText());
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
int mode = msg.getAction(); device.setScreenPowerMode(msg.getAction());
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
} }
break; break;
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(); device.rotateDevice();
break; break;
default: default:
// do nothing // do nothing
} }
} }
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { private boolean injectKeycode(int action, int keycode, int metaState) {
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { return injectKeyEvent(action, keycode, 0, metaState);
schedulePowerModeOff();
}
return device.injectKeyEvent(action, keycode, repeat, metaState);
} }
private boolean injectChar(char c) { private boolean injectChar(char c) {
@ -152,7 +137,7 @@ public class Controller {
return false; return false;
} }
for (KeyEvent event : events) { for (KeyEvent event : events) {
if (!device.injectEvent(event)) { if (!injectEvent(event)) {
return false; return false;
} }
} }
@ -176,7 +161,7 @@ public class Controller {
Point point = device.getPhysicalPoint(position); Point point = device.getPhysicalPoint(position);
if (point == null) { if (point == null) {
Ln.w("Ignore touch event, it was generated for a different device size"); // ignore event
return false; return false;
} }
@ -205,14 +190,10 @@ public class Controller {
} }
} }
// Right-click and middle-click only work if the source is a mouse
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, source, .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
0); InputDevice.SOURCE_TOUCHSCREEN, 0);
return device.injectEvent(event); return injectEvent(event);
} }
private boolean injectScroll(Position position, int hScroll, int vScroll) { private boolean injectScroll(Position position, int hScroll, int vScroll) {
@ -234,42 +215,27 @@ public class Controller {
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0); InputDevice.SOURCE_MOUSE, 0);
return device.injectEvent(event); return injectEvent(event);
} }
/** private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
* Schedule a call to set power mode to off after a small delay. long now = SystemClock.uptimeMillis();
*/ KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
private static void schedulePowerModeOff() { InputDevice.SOURCE_KEYBOARD);
EXECUTOR.schedule(new Runnable() { return injectEvent(event);
@Override
public void run() {
Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
} }
}, 200, TimeUnit.MILLISECONDS);
private boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
private boolean injectEvent(InputEvent event) {
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} }
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn() {
int keycode = Device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) { return injectKeycode(keycode);
schedulePowerModeOff();
}
return device.injectKeycode(keycode);
}
private boolean setClipboard(String text, boolean paste) {
boolean ok = device.setClipboardText(text);
if (ok) {
Ln.i("Device clipboard set");
}
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
}
return ok;
} }
} }

View File

@ -1,44 +1,30 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager; import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.RemoteException;
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 {
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
public interface RotationListener { public interface RotationListener {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
} }
public interface ClipboardListener { private final ServiceManager serviceManager = new ServiceManager();
void onClipboardTextChanged(String text);
}
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/** /**
* Logical display identifier * Logical display identifier
@ -50,13 +36,16 @@ public final class Device {
*/ */
private final int layerStack; private final int layerStack;
private final boolean supportsInputEvents; /**
* The FLAG_PRESENTATION from the DisplayInfo
*/
private final boolean isPresentationDisplay;
public Device(Options options) { public Device(Options options) {
displayId = options.getDisplayId(); displayId = options.getDisplayId();
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) { if (displayInfo == null) {
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds); throw new InvalidDisplayIdException(displayId, displayIds);
} }
@ -64,10 +53,11 @@ public final class Device {
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
layerStack = displayInfo.getLayerStack(); layerStack = displayInfo.getLayerStack();
isPresentationDisplay = (displayInfoFlags & DisplayInfo.FLAG_PRESENTATION) != 0;
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { registerRotationWatcher(new IRotationWatcher.Stub() {
@Override @Override
public void onRotationChanged(int rotation) { public void onRotationChanged(int rotation) throws RemoteException {
synchronized (Device.this) { synchronized (Device.this) {
screenInfo = screenInfo.withDeviceRotation(rotation); screenInfo = screenInfo.withDeviceRotation(rotation);
@ -77,42 +67,14 @@ public final class Device {
} }
} }
} }
}, displayId);
if (options.getControl()) {
// If control is enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it
return;
}
synchronized (Device.this) {
if (clipboardListener != null) {
String text = getClipboardText();
if (text != null) {
clipboardListener.onClipboardTextChanged(text);
}
}
}
}
}); });
} else {
Ln.w("No clipboard manager, copy-paste between device and computer will not work");
}
}
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
} }
// main display or any display on Android >= Q if (!supportsInputEvents()) {
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; Ln.w("Input events are not supported for displays with FLAG_PRESENTATION enabled for devices with API lower than 29");
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
} }
} }
@ -154,10 +116,13 @@ public final class Device {
} }
public boolean supportsInputEvents() { public boolean supportsInputEvents() {
return supportsInputEvents; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return true;
}
return !isPresentationDisplay;
} }
public boolean injectEvent(InputEvent inputEvent, int mode) { public boolean injectInputEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) { if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()"); throw new AssertionError("Could not inject input event if !supportsInputEvents()");
} }
@ -166,94 +131,60 @@ public final class Device {
return false; return false;
} }
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode); return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
} }
public boolean injectEvent(InputEvent event) { public boolean isScreenOn() {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); return serviceManager.getPowerManager().isScreenOn();
} }
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
long now = SystemClock.uptimeMillis(); serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher);
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 static boolean isScreenOn() {
return SERVICE_MANAGER.getPowerManager().isScreenOn();
} }
public synchronized void setRotationListener(RotationListener rotationListener) { public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener; this.rotationListener = rotationListener;
} }
public synchronized void setClipboardListener(ClipboardListener clipboardListener) { public void expandNotificationPanel() {
this.clipboardListener = clipboardListener; serviceManager.getStatusBarManager().expandNotificationsPanel();
} }
public static void expandNotificationPanel() { public void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); serviceManager.getStatusBarManager().collapsePanels();
} }
public static void collapsePanels() { public String getClipboardText() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels(); CharSequence s = serviceManager.getClipboardManager().getText();
}
public static String getClipboardText() {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
if (clipboardManager == null) {
return null;
}
CharSequence s = clipboardManager.getText();
if (s == null) { if (s == null) {
return null; return null;
} }
return s.toString(); return s.toString();
} }
public boolean setClipboardText(String text) { public void setClipboardText(String text) {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); serviceManager.getClipboardManager().setText(text);
if (clipboardManager == null) { Ln.i("Device clipboard set");
return false;
}
String currentClipboard = getClipboardText();
if (currentClipboard != null && currentClipboard.equals(text)) {
// The clipboard already contains the requested text.
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
// problem, do not explicitly set the clipboard text if it already contains the expected content.
return false;
}
isSettingClipboard.set(true);
boolean ok = clipboardManager.setText(text);
isSettingClipboard.set(false);
return ok;
} }
/** /**
* @param mode one of the {@code POWER_MODE_*} constants * @param mode one of the {@code SCREEN_POWER_MODE_*} constants
*/ */
public static boolean setScreenPowerMode(int mode) { public void 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 false; return;
} }
return SurfaceControl.setDisplayPowerMode(d, mode); SurfaceControl.setDisplayPowerMode(d, mode);
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
} }
/** /**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/ */
public static void rotateDevice() { public void rotateDevice() {
WindowManager wm = SERVICE_MANAGER.getWindowManager(); WindowManager wm = serviceManager.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen(); boolean accelerometerRotation = !wm.isRotationFrozen();
@ -269,8 +200,4 @@ public final class Device {
wm.thawRotation(); wm.thawRotation();
} }
} }
public static ContentProvider createSettingsProvider() {
return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
}
} }

View File

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

View File

@ -8,6 +8,7 @@ public final class DisplayInfo {
private final int flags; private final int flags;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public static final int FLAG_PRESENTATION = 0x00000008;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
this.displayId = displayId; this.displayId = displayId;

View File

@ -1,23 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodecInfo;
public class InvalidEncoderException extends RuntimeException {
private final String name;
private final MediaCodecInfo[] availableEncoders;
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
super("There is no encoder having name '" + name + '"');
this.name = name;
this.availableEncoders = availableEncoders;
}
public String getName() {
return name;
}
public MediaCodecInfo[] getAvailableEncoders() {
return availableEncoders;
}
}

View File

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

View File

@ -3,7 +3,6 @@ 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;
@ -13,18 +12,6 @@ public class Options {
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 int displayId;
private boolean showTouches;
private boolean stayAwake;
private String codecOptions;
private String encoderName;
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;
@ -97,36 +84,4 @@ public class Options {
public void setDisplayId(int displayId) { public void setDisplayId(int displayId) {
this.displayId = 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;
}
public String getEncoderName() {
return encoderName;
}
public void setEncoderName(String encoderName) {
this.encoderName = encoderName;
}
} }

View File

@ -5,7 +5,6 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.IBinder; import android.os.IBinder;
import android.view.Surface; import android.view.Surface;
@ -13,9 +12,6 @@ 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.ArrayList;
import java.util.Arrays;
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 {
@ -29,19 +25,23 @@ 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 String encoderName;
private List<CodecOption> codecOptions;
private int bitRate; private int bitRate;
private int maxFps; private int maxFps;
private int lockedVideoOrientation;
private int iFrameInterval;
private boolean sendFrameMeta; private boolean sendFrameMeta;
private long ptsOrigin; private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName) { public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation, int iFrameInterval) {
this.sendFrameMeta = sendFrameMeta; this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate; this.bitRate = bitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
this.codecOptions = codecOptions; this.lockedVideoOrientation = lockedVideoOrientation;
this.encoderName = encoderName; this.iFrameInterval = iFrameInterval;
}
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int lockedVideoOrientation) {
this(sendFrameMeta, bitRate, maxFps, lockedVideoOrientation, DEFAULT_I_FRAME_INTERVAL);
} }
@Override @Override
@ -55,26 +55,14 @@ 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();
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(); Workarounds.fillAppInfo();
internalStreamScreen(device, fd);
}
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this); device.setRotationListener(this);
boolean alive; boolean alive;
try { try {
do { do {
MediaCodec codec = createCodec(encoderName); MediaCodec codec = createCodec();
IBinder display = createDisplay(); IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo(); ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect(); Rect contentRect = screenInfo.getContentRect();
@ -155,57 +143,18 @@ public class ScreenEncoder implements Device.RotationListener {
IO.writeFully(fd, headerBuffer); IO.writeFully(fd, headerBuffer);
} }
private static MediaCodecInfo[] listEncoders() { private static MediaCodec createCodec() throws IOException {
List<MediaCodecInfo> result = new ArrayList<>(); return MediaCodec.createEncoderByType("video/avc");
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
result.add(codecInfo);
}
}
return result.toArray(new MediaCodecInfo[result.size()]);
} }
private static MediaCodec createCodec(String encoderName) throws IOException { private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) {
if (encoderName != null) {
Ln.d("Creating encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
MediaCodecInfo[] encoders = listEncoders();
throw new InvalidEncoderException(encoderName, encoders);
}
}
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
Ln.d("Using encoder: '" + codec.getName() + "'");
return codec;
}
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
String key = codecOption.getKey();
Object value = codecOption.getValue();
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat(); MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setString(MediaFormat.KEY_MIME, "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, DEFAULT_I_FRAME_INTERVAL); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// 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) {
@ -214,13 +163,6 @@ public class ScreenEncoder implements Device.RotationListener {
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437> // <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
} }
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
setCodecOption(format, option);
}
}
return format; return format;
} }

View File

@ -1,19 +1,15 @@
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.media.MediaCodecInfo;
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
@ -22,55 +18,17 @@ public final class Server {
private static void scrcpy(Options options) throws IOException { private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options); final Device device = new Device(options);
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
boolean mustDisableShowTouchesOnCleanUp = false;
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, true);
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(),
options.getEncoderName()); options.getLockedVideoOrientation());
if (options.getControl()) { if (options.getControl()) {
final Controller controller = new Controller(device, connection); 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 {
@ -122,56 +80,40 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
} }
final int expectedParameters = 15; if (args.length != 10) {
if (args.length != expectedParameters) { throw new IllegalArgumentException("Expecting 10 parameters");
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
} }
Options options = new Options(); Options options = new Options();
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH)); int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
options.setLogLevel(level);
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
options.setMaxSize(maxSize); options.setMaxSize(maxSize);
int bitRate = Integer.parseInt(args[3]); int bitRate = Integer.parseInt(args[2]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
int maxFps = Integer.parseInt(args[4]); int maxFps = Integer.parseInt(args[3]);
options.setMaxFps(maxFps); options.setMaxFps(maxFps);
int lockedVideoOrientation = Integer.parseInt(args[5]); int lockedVideoOrientation = Integer.parseInt(args[4]);
options.setLockedVideoOrientation(lockedVideoOrientation); options.setLockedVideoOrientation(lockedVideoOrientation);
// use "adb forward" instead of "adb tunnel"? (so the server must listen) // use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[6]); boolean tunnelForward = Boolean.parseBoolean(args[5]);
options.setTunnelForward(tunnelForward); options.setTunnelForward(tunnelForward);
Rect crop = parseCrop(args[7]); Rect crop = parseCrop(args[6]);
options.setCrop(crop); options.setCrop(crop);
boolean sendFrameMeta = Boolean.parseBoolean(args[8]); boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
options.setSendFrameMeta(sendFrameMeta); options.setSendFrameMeta(sendFrameMeta);
boolean control = Boolean.parseBoolean(args[9]); boolean control = Boolean.parseBoolean(args[8]);
options.setControl(control); options.setControl(control);
int displayId = Integer.parseInt(args[10]); int displayId = Integer.parseInt(args[9]);
options.setDisplayId(displayId); 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);
String encoderName = "-".equals(args[14]) ? null : args[14];
options.setEncoderName(encoderName);
return options; return options;
} }
@ -191,6 +133,14 @@ 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);
}
}
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) {
@ -211,15 +161,6 @@ public final class Server {
Ln.e(" scrcpy --display " + id); Ln.e(" scrcpy --display " + id);
} }
} }
} else if (e instanceof InvalidEncoderException) {
InvalidEncoderException iee = (InvalidEncoderException) e;
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
if (encoders != null && encoders.length > 0) {
Ln.e("Try to use one of the available encoders:");
for (MediaCodecInfo encoder : encoders) {
Ln.e(" scrcpy --encoder-name '" + encoder.getName() + "'");
}
}
} }
} }
@ -232,10 +173,8 @@ public final class Server {
} }
}); });
unlinkSelf();
Options options = createOptions(args); Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
scrcpy(options); scrcpy(options);
} }
} }

View File

@ -16,7 +16,6 @@ public final class Workarounds {
// not instantiable // not instantiable
} }
@SuppressWarnings("deprecation")
public static void prepareMainLooper() { public static void prepareMainLooper() {
// Some devices internally create a Handler when creating an input Surface, causing an exception: // Some devices internally create a Handler when creating an input Surface, causing an exception:
// "Can't create handler inside thread that has not called Looper.prepare()" // "Can't create handler inside thread that has not called Looper.prepare()"
@ -29,7 +28,7 @@ public final class Workarounds {
Looper.prepareMainLooper(); Looper.prepareMainLooper();
} }
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi")
public static void fillAppInfo() { public static void fillAppInfo() {
try { try {
// ActivityThread activityThread = new ActivityThread(); // ActivityThread activityThread = new ActivityThread();

View File

@ -1,87 +0,0 @@
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());
}
}

View File

@ -3,7 +3,6 @@ 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;
@ -11,10 +10,13 @@ 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;
@ -44,17 +46,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, ServiceManager.PACKAGE_NAME); return (ClipData) method.invoke(manager, PACKAGE_NAME);
} }
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); return (ClipData) method.invoke(manager, PACKAGE_NAME, 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, ServiceManager.PACKAGE_NAME); method.invoke(manager, clipData, PACKAGE_NAME);
} else { } else {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); method.invoke(manager, clipData, PACKAGE_NAME, USER_ID);
} }
} }
@ -72,48 +74,13 @@ public class ClipboardManager {
} }
} }
public boolean setText(CharSequence text) { public void 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;
} }
} }
} }

View File

@ -1,146 +0,0 @@
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 int callMethodVersion;
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, String.class, Bundle.class);
callMethodVersion = 0;
} catch (NoSuchMethodException e) {
// old versions
try {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 1;
} catch (NoSuchMethodException e2) {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
callMethodVersion = 2;
}
}
}
return callMethod;
}
private Bundle call(String callMethod, String arg, Bundle extras) {
try {
Method method = getCallMethod();
Object[] args;
switch (callMethodVersion) {
case 0:
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break;
case 1:
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
break;
default:
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
break;
}
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;
}
}

View File

@ -53,7 +53,8 @@ public final class InputManager {
method.invoke(inputEvent, displayId); method.invoke(inputEvent, displayId);
return true; return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Cannot associate a display id to the input event", e); // just a warning, it might happen on old devices
Ln.w("Cannot associate a display id to the input event");
return false; return false;
} }
} }

View File

@ -6,12 +6,8 @@ import android.os.IInterface;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi")
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;
@ -20,7 +16,6 @@ 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 {
@ -77,32 +72,8 @@ public final class ServiceManager {
public ClipboardManager getClipboardManager() { public ClipboardManager getClipboardManager() {
if (clipboardManager == null) { if (clipboardManager == null) {
IInterface clipboard = getService("clipboard", "android.content.IClipboard"); clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
if (clipboard == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
clipboardManager = new ClipboardManager(clipboard);
} }
return clipboardManager; return clipboardManager;
} }
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;
}
} }

View File

@ -121,14 +121,12 @@ public final class SurfaceControl {
return setDisplayPowerModeMethod; return setDisplayPowerModeMethod;
} }
public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { public static void 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;
} }
} }

View File

@ -93,13 +93,13 @@ public final class WindowManager {
} }
} }
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { public void registerRotationWatcher(IRotationWatcher rotationWatcher) {
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, displayId); cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0);
} 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);

View File

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

View File

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

View File

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