Compare commits
4 Commits
install_re
...
resize.2
Author | SHA1 | Date | |
---|---|---|---|
61e73b7b63 | |||
5edd5a2617 | |||
9006f545fc | |||
f038518dfc |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -21,9 +21,5 @@ assignees: ''
|
||||
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.
|
||||
```
|
||||
|
||||
Format them between code blocks (delimited by ```).
|
||||
Please do not post screenshots of your terminal, just post the content as text instead.
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,9 +1,5 @@
|
||||
build/
|
||||
/dist/
|
||||
/build-*/
|
||||
/build_*/
|
||||
/release-*/
|
||||
.idea/
|
||||
.gradle/
|
||||
/x/
|
||||
local.properties
|
||||
|
131
BUILD.md
131
BUILD.md
@ -2,37 +2,11 @@
|
||||
|
||||
Here are the instructions to build _scrcpy_ (client and server).
|
||||
|
||||
You may want to build only the client: the server binary, which will be pushed
|
||||
to the Android device, does not depend on your system and architecture. In that
|
||||
case, use the [prebuilt server] (so you will not need Java or the Android SDK).
|
||||
|
||||
## Simple
|
||||
|
||||
If you just want to install the latest release from `master`, follow this
|
||||
simplified process.
|
||||
|
||||
First, you need to install the required packages:
|
||||
|
||||
```bash
|
||||
# for Debian/Ubuntu
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build \
|
||||
libavcodec-dev libavformat-dev libavutil-dev libsdl2-dev
|
||||
```
|
||||
|
||||
Then clone the repo and execute the installation script
|
||||
([source](install_release.sh)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Genymobile/scrcpy
|
||||
cd scrcpy
|
||||
./install_release.sh
|
||||
```
|
||||
|
||||
When a new release is out, update the repo and reinstall:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
./install_release.sh
|
||||
```
|
||||
|
||||
[prebuilt server]: #prebuilt-server
|
||||
|
||||
## Branches
|
||||
|
||||
@ -86,10 +60,11 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb
|
||||
|
||||
# client build dependencies
|
||||
sudo apt install gcc git pkg-config meson ninja-build \
|
||||
libavcodec-dev libavformat-dev libavutil-dev libsdl2-dev
|
||||
libavcodec-dev libavformat-dev libavutil-dev \
|
||||
libsdl2-dev
|
||||
|
||||
# server build dependencies
|
||||
sudo apt install openjdk-11-jdk
|
||||
sudo apt install openjdk-8-jdk
|
||||
```
|
||||
|
||||
On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install
|
||||
@ -131,13 +106,13 @@ sudo apt install mingw-w64 mingw-w64-tools
|
||||
You also need the JDK to build the server:
|
||||
|
||||
```bash
|
||||
sudo apt install openjdk-11-jdk
|
||||
sudo apt install openjdk-8-jdk
|
||||
```
|
||||
|
||||
Then generate the releases:
|
||||
|
||||
```bash
|
||||
./release.sh
|
||||
make -f Makefile.CrossWindows
|
||||
```
|
||||
|
||||
It will generate win32 and win64 releases into `dist/`.
|
||||
@ -201,8 +176,8 @@ Additionally, if you want to build the server, install Java 8 from Caskroom, and
|
||||
make it avaliable from the `PATH`:
|
||||
|
||||
```bash
|
||||
brew tap homebrew/cask-versions
|
||||
brew cask install adoptopenjdk/openjdk/adoptopenjdk8
|
||||
brew tap caskroom/versions
|
||||
brew cask install java8
|
||||
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
```
|
||||
@ -214,44 +189,29 @@ See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
|
||||
|
||||
## Common steps
|
||||
|
||||
**As a non-root user**, clone the project:
|
||||
If you want to build the server, install the [Android SDK] (_Android Studio_),
|
||||
and set `ANDROID_HOME` to its directory. For example:
|
||||
|
||||
[Android SDK]: https://developer.android.com/studio/index.html
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME=~/android/sdk
|
||||
```
|
||||
|
||||
If you don't want to build the server, use the [prebuilt server].
|
||||
|
||||
Clone the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Genymobile/scrcpy
|
||||
cd scrcpy
|
||||
```
|
||||
|
||||
|
||||
### Build
|
||||
|
||||
You may want to build only the client: the server binary, which will be pushed
|
||||
to the Android device, does not depend on your system and architecture. In that
|
||||
case, use the [prebuilt server] (so you will not need Java or the Android SDK).
|
||||
|
||||
[prebuilt server]: #option-2-use-prebuilt-server
|
||||
|
||||
|
||||
#### Option 1: Build everything from sources
|
||||
|
||||
Install the [Android SDK] (_Android Studio_), and set `ANDROID_SDK_ROOT` to its
|
||||
directory. For example:
|
||||
|
||||
[Android SDK]: https://developer.android.com/studio/index.html
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
export ANDROID_SDK_ROOT=~/Android/Sdk
|
||||
# Mac
|
||||
export ANDROID_SDK_ROOT=~/Library/Android/sdk
|
||||
# Windows
|
||||
set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk
|
||||
```
|
||||
|
||||
Then, build:
|
||||
|
||||
```bash
|
||||
meson x --buildtype release --strip -Db_lto=true
|
||||
ninja -Cx # DO NOT RUN AS ROOT
|
||||
ninja -Cx
|
||||
```
|
||||
|
||||
_Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja
|
||||
@ -260,27 +220,9 @@ install` must be run as root)._
|
||||
[ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a
|
||||
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
### Run
|
||||
|
||||
- [`scrcpy-server-v1.17`][direct-scrcpy-server]
|
||||
_(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725)_
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
```bash
|
||||
meson x --buildtype release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=/path/to/scrcpy-server
|
||||
ninja -Cx # DO NOT RUN AS ROOT
|
||||
```
|
||||
|
||||
The server only works with a matching client version (this server works with the
|
||||
`master` branch).
|
||||
|
||||
|
||||
### Run without installing:
|
||||
To run without installing:
|
||||
|
||||
```bash
|
||||
./run x [options]
|
||||
@ -303,3 +245,24 @@ This installs two files:
|
||||
Just remove them to "uninstall" the application.
|
||||
|
||||
You can then [run](README.md#run) _scrcpy_.
|
||||
|
||||
|
||||
## Prebuilt server
|
||||
|
||||
- [`scrcpy-server-v1.13`][direct-scrcpy-server]
|
||||
_(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
```bash
|
||||
meson x --buildtype release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=/path/to/scrcpy-server
|
||||
ninja -Cx
|
||||
sudo ninja -Cx install
|
||||
```
|
||||
|
||||
The server only works with a matching client version (this server works with the
|
||||
`master` branch).
|
||||
|
@ -3,16 +3,16 @@
|
||||
다음은 자주 제보되는 문제들과 그들의 현황입니다.
|
||||
|
||||
|
||||
### Windows 운영체제에서, 디바이스가 발견되지 않습니다.
|
||||
### Window 운영체제에서, 디바이스가 발견되지 않습니다.
|
||||
|
||||
가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다.
|
||||
다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요:
|
||||
|
||||
adb devices
|
||||
|
||||
Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다.
|
||||
Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다.
|
||||
|
||||
[드라이버]: https://developer.android.com/studio/run/oem-usb.html
|
||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||
|
||||
|
||||
### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다.
|
||||
|
53
FAQ.md
53
FAQ.md
@ -37,13 +37,8 @@ Check [stackoverflow][device-unauthorized].
|
||||
|
||||
### Device not detected
|
||||
|
||||
> adb: error: failed to get feature set: no devices/emulators found
|
||||
|
||||
Check that you correctly enabled [adb debugging][enable-adb].
|
||||
|
||||
If your device is not detected, you may need some [drivers] (on Windows).
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||
|
||||
|
||||
@ -114,6 +109,16 @@ In developer options, enable:
|
||||
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
### Mouse clicks at wrong location
|
||||
|
||||
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
||||
scaled. See [#15].
|
||||
|
||||
[#15]: https://github.com/Genymobile/scrcpy/issues/15
|
||||
|
||||
Open _scrcpy_ directly on the monitor you use it.
|
||||
|
||||
|
||||
### Special characters do not work
|
||||
|
||||
Injecting text input is [limited to ASCII characters][text-input]. A trick
|
||||
@ -192,41 +197,3 @@ scrcpy -m 1920
|
||||
scrcpy -m 1024
|
||||
scrcpy -m 800
|
||||
```
|
||||
|
||||
You could also try another [encoder](README.md#encoder).
|
||||
|
||||
|
||||
## Command line on Windows
|
||||
|
||||
Some Windows users are not familiar with the command line. Here is how to open a
|
||||
terminal and run `scrcpy` with arguments:
|
||||
|
||||
1. Press <kbd>Windows</kbd>+<kbd>r</kbd>, this opens a dialog box.
|
||||
2. Type `cmd` and press <kbd>Enter</kbd>, this opens a terminal.
|
||||
3. Go to your _scrcpy_ directory, by typing (adapt the path):
|
||||
|
||||
```bat
|
||||
cd C:\Users\user\Downloads\scrcpy-win64-xxx
|
||||
```
|
||||
|
||||
and press <kbd>Enter</kbd>
|
||||
4. Type your command. For example:
|
||||
|
||||
```bat
|
||||
scrcpy --record file.mkv
|
||||
```
|
||||
|
||||
If you plan to always use the same arguments, create a file `myscrcpy.bat`
|
||||
(enable [show file extensions] to avoid confusion) in the `scrcpy` directory,
|
||||
containing your command. For example:
|
||||
|
||||
```bat
|
||||
scrcpy --prefer-text --turn-screen-off --stay-awake
|
||||
```
|
||||
|
||||
Then just double-click on that file.
|
||||
|
||||
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
|
||||
to add some arguments.
|
||||
|
||||
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
|
||||
|
2
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 Romain Vimont
|
||||
Copyright (C) 2018-2020 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -9,20 +9,21 @@
|
||||
# the server to the device.
|
||||
|
||||
.PHONY: default clean \
|
||||
test \
|
||||
build-server \
|
||||
prepare-deps-win32 prepare-deps-win64 \
|
||||
build-win32 build-win64 \
|
||||
build-win32 build-win32-noconsole \
|
||||
build-win64 build-win64-noconsole \
|
||||
dist-win32 dist-win64 \
|
||||
zip-win32 zip-win64 \
|
||||
release
|
||||
sums release
|
||||
|
||||
GRADLE ?= ./gradlew
|
||||
|
||||
TEST_BUILD_DIR := build-test
|
||||
SERVER_BUILD_DIR := build-server
|
||||
WIN32_BUILD_DIR := build-win32
|
||||
WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole
|
||||
WIN64_BUILD_DIR := build-win64
|
||||
WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole
|
||||
|
||||
DIST := dist
|
||||
WIN32_TARGET_DIR := scrcpy-win32
|
||||
@ -32,35 +33,19 @@ VERSION := $(shell git describe --tags --always)
|
||||
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
|
||||
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
|
||||
|
||||
RELEASE_DIR := release-$(VERSION)
|
||||
|
||||
release: clean test build-server zip-win32 zip-win64
|
||||
mkdir -p "$(RELEASE_DIR)"
|
||||
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
|
||||
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
|
||||
cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
|
||||
cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
|
||||
cd "$(RELEASE_DIR)" && \
|
||||
sha256sum "scrcpy-server-$(VERSION)" \
|
||||
"scrcpy-win32-$(VERSION).zip" \
|
||||
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
|
||||
@echo "Release generated in $(RELEASE_DIR)/"
|
||||
release: clean zip-win32 zip-win64 sums
|
||||
@echo "Windows archives generated in $(DIST)/"
|
||||
|
||||
clean:
|
||||
$(GRADLE) clean
|
||||
rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
|
||||
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
|
||||
|
||||
test:
|
||||
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
|
||||
meson "$(TEST_BUILD_DIR)" -Db_sanitize=address )
|
||||
ninja -C "$(TEST_BUILD_DIR)"
|
||||
$(GRADLE) -p server check
|
||||
rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \
|
||||
"$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)"
|
||||
|
||||
build-server:
|
||||
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
|
||||
meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
|
||||
ninja -C "$(SERVER_BUILD_DIR)"
|
||||
meson "$(SERVER_BUILD_DIR)" \
|
||||
--buildtype release -Dcompile_app=false )
|
||||
ninja -C "$(SERVER_BUILD_DIR)"
|
||||
|
||||
prepare-deps-win32:
|
||||
-$(MAKE) -C prebuilt-deps prepare-win32
|
||||
@ -75,6 +60,17 @@ build-win32: prepare-deps-win32
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN32_BUILD_DIR)"
|
||||
|
||||
build-win32-noconsole: prepare-deps-win32
|
||||
[ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \
|
||||
meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \
|
||||
--cross-file cross_win32.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcrossbuild_windows=true \
|
||||
-Dcompile_server=false \
|
||||
-Dwindows_noconsole=true \
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
|
||||
|
||||
prepare-deps-win64:
|
||||
-$(MAKE) -C prebuilt-deps prepare-win64
|
||||
|
||||
@ -88,37 +84,46 @@ build-win64: prepare-deps-win64
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN64_BUILD_DIR)"
|
||||
|
||||
dist-win32: build-server build-win32
|
||||
build-win64-noconsole: prepare-deps-win64
|
||||
[ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \
|
||||
meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \
|
||||
--cross-file cross_win64.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcrossbuild_windows=true \
|
||||
-Dcompile_server=false \
|
||||
-Dwindows_noconsole=true \
|
||||
-Dportable=true )
|
||||
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
|
||||
|
||||
dist-win32: build-server build-win32 build-win32-noconsole
|
||||
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.3.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.3.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 "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/SDL2-2.0.14/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
|
||||
dist-win64: build-server build-win64
|
||||
dist-win64: build-server build-win64 build-win64-noconsole
|
||||
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.3.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.3.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 "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/ffmpeg-4.2.2-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/SDL2-2.0.14/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
|
||||
zip-win32: dist-win32
|
||||
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
|
||||
@ -127,3 +132,7 @@ zip-win32: dist-win32
|
||||
zip-win64: dist-win64
|
||||
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \
|
||||
zip -r "../$(WIN64_TARGET)" .
|
||||
|
||||
sums:
|
||||
cd "$(DIST)"; \
|
||||
sha256sum *.zip > SHA256SUMS.txt
|
696
README.id.md
696
README.id.md
@ -1,696 +0,0 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
# scrcpy (v1.16)
|
||||
|
||||
Aplikasi ini menyediakan tampilan dan kontrol perangkat Android yang terhubung pada USB (atau [melalui TCP/IP][article-tcpip]). Ini tidak membutuhkan akses _root_ apa pun. Ini bekerja pada _GNU/Linux_, _Windows_ and _macOS_.
|
||||
|
||||

|
||||
|
||||
Ini berfokus pada:
|
||||
|
||||
- **keringanan** (asli, hanya menampilkan layar perangkat)
|
||||
- **kinerja** (30~60fps)
|
||||
- **kualitas** (1920×1080 atau lebih)
|
||||
- **latensi** rendah ([35~70ms][lowlatency])
|
||||
- **waktu startup rendah** (~1 detik untuk menampilkan gambar pertama)
|
||||
- **tidak mengganggu** (tidak ada yang terpasang di perangkat)
|
||||
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## Persyaratan
|
||||
Perangkat Android membutuhkan setidaknya API 21 (Android 5.0).
|
||||
|
||||
Pastikan Anda [mengaktifkan debugging adb][enable-adb] pada perangkat Anda.
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
Di beberapa perangkat, Anda juga perlu mengaktifkan [opsi tambahan][control] untuk mengontrolnya menggunakan keyboard dan mouse.
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## Dapatkan aplikasinya
|
||||
|
||||
### Linux
|
||||
|
||||
Di Debian (_testing_ dan _sid_ untuk saat ini) dan Ubuntu (20.04):
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
Paket [Snap] tersedia: [`scrcpy`][snap-link].
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
Untuk Fedora, paket [COPR] tersedia: [`scrcpy`][copr-link].
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
Untuk Arch Linux, paket [AUR] tersedia: [`scrcpy`][aur-link].
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
Untuk Gentoo, tersedia [Ebuild]: [`scrcpy/`][ebuild-link].
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
Anda juga bisa [membangun aplikasi secara manual][BUILD] (jangan khawatir, tidak terlalu sulit).
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
Untuk Windows, untuk kesederhanaan, arsip prebuilt dengan semua dependensi (termasuk `adb`) tersedia :
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
Ini juga tersedia di [Chocolatey]:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # jika Anda belum memilikinya
|
||||
```
|
||||
|
||||
Dan di [Scoop]:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # jika Anda belum memilikinya
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
Anda juga dapat [membangun aplikasi secara manual][BUILD].
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
Aplikasi ini tersedia di [Homebrew]. Instal saja:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
Anda membutuhkan `adb`, dapat diakses dari `PATH` Anda. Jika Anda belum memilikinya:
|
||||
|
||||
```bash
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
Anda juga dapat [membangun aplikasi secara manual][BUILD].
|
||||
|
||||
|
||||
## Menjalankan
|
||||
|
||||
Pasang perangkat Android, dan jalankan:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Ini menerima argumen baris perintah, didaftarkan oleh:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## Fitur
|
||||
|
||||
### Menangkap konfigurasi
|
||||
|
||||
#### Mengurangi ukuran
|
||||
|
||||
Kadang-kadang, berguna untuk mencerminkan perangkat Android dengan definisi yang lebih rendah untuk meningkatkan kinerja.
|
||||
|
||||
Untuk membatasi lebar dan tinggi ke beberapa nilai (mis. 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # versi pendek
|
||||
```
|
||||
|
||||
Dimensi lain dihitung agar rasio aspek perangkat dipertahankan.
|
||||
Dengan begitu, perangkat 1920×1080 akan dicerminkan pada 1024×576.
|
||||
|
||||
#### Ubah kecepatan bit
|
||||
|
||||
Kecepatan bit default adalah 8 Mbps. Untuk mengubah bitrate video (mis. Menjadi 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # versi pendek
|
||||
```
|
||||
|
||||
#### Batasi frekuensi gambar
|
||||
|
||||
Kecepatan bingkai pengambilan dapat dibatasi:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
Ini secara resmi didukung sejak Android 10, tetapi dapat berfungsi pada versi sebelumnya.
|
||||
|
||||
#### Memotong
|
||||
|
||||
Layar perangkat dapat dipotong untuk mencerminkan hanya sebagian dari layar.
|
||||
|
||||
Ini berguna misalnya untuk mencerminkan hanya satu mata dari Oculus Go:
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # 1224x1440 Mengimbangi (0,0)
|
||||
```
|
||||
|
||||
Jika `--max-size` juga ditentukan, pengubahan ukuran diterapkan setelah pemotongan.
|
||||
|
||||
|
||||
#### Kunci orientasi video
|
||||
|
||||
Untuk mengunci orientasi pencerminan:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation 0 # orientasi alami
|
||||
scrcpy --lock-video-orientation 1 # 90° berlawanan arah jarum jam
|
||||
scrcpy --lock-video-orientation 2 # 180°
|
||||
scrcpy --lock-video-orientation 3 # 90° searah jarum jam
|
||||
```
|
||||
|
||||
Ini mempengaruhi orientasi perekaman.
|
||||
|
||||
|
||||
### Rekaman
|
||||
|
||||
Anda dapat merekam layar saat melakukan mirroring:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
Untuk menonaktifkan pencerminan saat merekam:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# berhenti merekam dengan Ctrl+C
|
||||
```
|
||||
|
||||
"Skipped frames" are recorded, even if they are not displayed in real time (for
|
||||
performance reasons). Frames are _timestamped_ on the device, so [packet delay
|
||||
variation] does not impact the recorded file.
|
||||
|
||||
"Frame yang dilewati" direkam, meskipun tidak ditampilkan secara real time (untuk alasan performa). Bingkai *diberi stempel waktu* pada perangkat, jadi [variasi penundaan paket] tidak memengaruhi file yang direkam.
|
||||
|
||||
[variasi penundaan paket]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
### Koneksi
|
||||
|
||||
#### Wireless
|
||||
|
||||
_Scrcpy_ menggunakan `adb` untuk berkomunikasi dengan perangkat, dan` adb` dapat [terhubung] ke perangkat melalui TCP / IP:
|
||||
|
||||
1. Hubungkan perangkat ke Wi-Fi yang sama dengan komputer Anda.
|
||||
2. Dapatkan alamat IP perangkat Anda (dalam Pengaturan → Tentang ponsel → Status).
|
||||
3. Aktifkan adb melalui TCP / IP pada perangkat Anda: `adb tcpip 5555`.
|
||||
4. Cabut perangkat Anda.
|
||||
5. Hubungkan ke perangkat Anda: `adb connect DEVICE_IP: 5555` (*ganti* *`DEVICE_IP`*).
|
||||
6. Jalankan `scrcpy` seperti biasa.
|
||||
|
||||
Mungkin berguna untuk menurunkan kecepatan bit dan definisi:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # versi pendek
|
||||
```
|
||||
|
||||
[terhubung]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### Multi-perangkat
|
||||
|
||||
Jika beberapa perangkat dicantumkan di `adb devices`, Anda harus menentukan _serial_:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # versi pendek
|
||||
```
|
||||
|
||||
If the device is connected over TCP/IP:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # versi pendek
|
||||
```
|
||||
|
||||
Anda dapat memulai beberapa contoh _scrcpy_ untuk beberapa perangkat.
|
||||
|
||||
#### Mulai otomatis pada koneksi perangkat
|
||||
|
||||
Anda bisa menggunakan [AutoAdb]:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### Koneksi via SSH tunnel
|
||||
|
||||
Untuk menyambung ke perangkat jarak jauh, dimungkinkan untuk menghubungkan klien `adb` lokal ke server `adb` jarak jauh (asalkan mereka menggunakan versi yang sama dari _adb_ protocol):
|
||||
|
||||
```bash
|
||||
adb kill-server # matikan server adb lokal di 5037
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 komputer_jarak_jauh_anda
|
||||
# jaga agar tetap terbuka
|
||||
```
|
||||
|
||||
Dari terminal lain:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Untuk menghindari mengaktifkan penerusan port jarak jauh, Anda dapat memaksa sambungan maju sebagai gantinya (perhatikan `-L`, bukan` -R`):
|
||||
|
||||
```bash
|
||||
adb kill-server # matikan server adb lokal di 5037
|
||||
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 komputer_jarak_jauh_anda
|
||||
# jaga agar tetap terbuka
|
||||
```
|
||||
|
||||
Dari terminal lain:
|
||||
|
||||
```bash
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
Seperti koneksi nirkabel, mungkin berguna untuk mengurangi kualitas:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### Konfigurasi Jendela
|
||||
|
||||
#### Judul
|
||||
|
||||
Secara default, judul jendela adalah model perangkat. Itu bisa diubah:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'Perangkat Saya'
|
||||
```
|
||||
|
||||
#### Posisi dan ukuran
|
||||
|
||||
Posisi dan ukuran jendela awal dapat ditentukan:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### Jendela tanpa batas
|
||||
|
||||
Untuk menonaktifkan dekorasi jendela:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### Selalu di atas
|
||||
|
||||
Untuk menjaga jendela scrcpy selalu di atas:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### Layar penuh
|
||||
|
||||
Aplikasi dapat dimulai langsung dalam layar penuh::
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # versi pendek
|
||||
```
|
||||
|
||||
Layar penuh kemudian dapat diubah secara dinamis dengan <kbd>MOD</kbd>+<kbd>f</kbd>.
|
||||
|
||||
#### Rotasi
|
||||
|
||||
Jendela mungkin diputar:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
Nilai yang mungkin adalah:
|
||||
- `0`: tidak ada rotasi
|
||||
- `1`: 90 derajat berlawanan arah jarum jam
|
||||
- `2`: 180 derajat
|
||||
- `3`: 90 derajat searah jarum jam
|
||||
|
||||
Rotasi juga dapat diubah secara dinamis dengan <kbd>MOD</kbd>+<kbd>←</kbd>
|
||||
_(kiri)_ and <kbd>MOD</kbd>+<kbd>→</kbd> _(kanan)_.
|
||||
|
||||
Perhatikan bahwa _scrcpy_ mengelola 3 rotasi berbeda::
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> meminta perangkat untuk beralih antara potret dan lanskap (aplikasi yang berjalan saat ini mungkin menolak, jika mendukung orientasi yang diminta).
|
||||
- `--lock-video-orientation` mengubah orientasi pencerminan (orientasi video yang dikirim dari perangkat ke komputer). Ini mempengaruhi rekaman.
|
||||
- `--rotation` (atau <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)
|
||||
memutar hanya konten jendela. Ini hanya mempengaruhi tampilan, bukan rekaman.
|
||||
|
||||
|
||||
### Opsi pencerminan lainnya
|
||||
|
||||
#### Hanya-baca
|
||||
|
||||
Untuk menonaktifkan kontrol (semua yang dapat berinteraksi dengan perangkat: tombol input, peristiwa mouse, seret & lepas file):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### Layar
|
||||
|
||||
Jika beberapa tampilan tersedia, Anda dapat memilih tampilan untuk cermin:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
Daftar id tampilan dapat diambil dengan::
|
||||
|
||||
```
|
||||
adb shell dumpsys display # cari "mDisplayId=" di keluaran
|
||||
```
|
||||
|
||||
Tampilan sekunder hanya dapat dikontrol jika perangkat menjalankan setidaknya Android 10 (jika tidak maka akan dicerminkan dalam hanya-baca).
|
||||
|
||||
|
||||
#### Tetap terjaga
|
||||
|
||||
Untuk mencegah perangkat tidur setelah beberapa penundaan saat perangkat dicolokkan:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
Keadaan awal dipulihkan ketika scrcpy ditutup.
|
||||
|
||||
|
||||
#### Matikan layar
|
||||
|
||||
Dimungkinkan untuk mematikan layar perangkat saat pencerminan mulai dengan opsi baris perintah:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
Atau dengan menekan <kbd>MOD</kbd>+<kbd>o</kbd> kapan saja.
|
||||
|
||||
Untuk menyalakannya kembali, tekan <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
|
||||
Di Android, tombol `POWER` selalu menyalakan layar. Untuk kenyamanan, jika `POWER` dikirim melalui scrcpy (melalui klik kanan atau<kbd>MOD</kbd>+<kbd>p</kbd>), itu akan memaksa untuk mematikan layar setelah penundaan kecil (atas dasar upaya terbaik).
|
||||
Tombol fisik `POWER` masih akan menyebabkan layar dihidupkan.
|
||||
|
||||
Ini juga berguna untuk mencegah perangkat tidur:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
#### Render frame kedaluwarsa
|
||||
|
||||
Secara default, untuk meminimalkan latensi, _scrcpy_ selalu menampilkan frame yang terakhir didekodekan tersedia, dan menghapus frame sebelumnya.
|
||||
|
||||
Untuk memaksa rendering semua frame (dengan kemungkinan peningkatan latensi), gunakan:
|
||||
|
||||
```bash
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
#### Tunjukkan sentuhan
|
||||
|
||||
Untuk presentasi, mungkin berguna untuk menunjukkan sentuhan fisik (pada perangkat fisik).
|
||||
|
||||
Android menyediakan fitur ini di _Opsi Pengembang_.
|
||||
|
||||
_Scrcpy_ menyediakan opsi untuk mengaktifkan fitur ini saat mulai dan mengembalikan nilai awal saat keluar:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
Perhatikan bahwa ini hanya menunjukkan sentuhan _fisik_ (dengan jari di perangkat).
|
||||
|
||||
|
||||
#### Nonaktifkan screensaver
|
||||
|
||||
Secara default, scrcpy tidak mencegah screensaver berjalan di komputer.
|
||||
|
||||
Untuk menonaktifkannya:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### Kontrol masukan
|
||||
|
||||
#### Putar layar perangkat
|
||||
|
||||
Tekan <kbd>MOD</kbd>+<kbd>r</kbd> untuk beralih antara mode potret dan lanskap.
|
||||
|
||||
Perhatikan bahwa itu berputar hanya jika aplikasi di latar depan mendukung orientasi yang diminta.
|
||||
|
||||
#### Salin-tempel
|
||||
|
||||
Setiap kali papan klip Android berubah, secara otomatis disinkronkan ke papan klip komputer.
|
||||
|
||||
Apa saja <kbd>Ctrl</kbd> pintasan diteruskan ke perangkat. Khususnya:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> biasanya salinan
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> biasanya memotong
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> biasanya menempel (setelah sinkronisasi papan klip komputer-ke-perangkat)
|
||||
|
||||
Ini biasanya berfungsi seperti yang Anda harapkan.
|
||||
|
||||
Perilaku sebenarnya tergantung pada aplikasi yang aktif. Sebagai contoh,
|
||||
_Termux_ mengirim SIGINT ke <kbd>Ctrl</kbd>+<kbd>c</kbd> sebagai gantinya, dan _K-9 Mail_ membuat pesan baru.
|
||||
|
||||
Untuk menyalin, memotong dan menempel dalam kasus seperti itu (tetapi hanya didukung di Android> = 7):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> injeksi `COPY` _(salin)_
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> injeksi `CUT` _(potong)_
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> injeksi `PASTE` (setelah sinkronisasi papan klip komputer-ke-perangkat)
|
||||
|
||||
Tambahan, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> memungkinkan untuk memasukkan teks papan klip komputer sebagai urutan peristiwa penting. Ini berguna ketika komponen tidak menerima penempelan teks (misalnya di _Termux_), tetapi dapat merusak konten non-ASCII.
|
||||
|
||||
**PERINGATAN:** Menempelkan papan klip komputer ke perangkat (baik melalui
|
||||
<kbd>Ctrl</kbd>+<kbd>v</kbd> or <kbd>MOD</kbd>+<kbd>v</kbd>) menyalin konten ke clipboard perangkat. Akibatnya, aplikasi Android apa pun dapat membaca kontennya. Anda harus menghindari menempelkan konten sensitif (seperti kata sandi) seperti itu.
|
||||
|
||||
|
||||
#### Cubit untuk memperbesar/memperkecil
|
||||
|
||||
Untuk mensimulasikan "cubit-untuk-memperbesar/memperkecil": <kbd>Ctrl</kbd>+_klik-dan-pindah_.
|
||||
|
||||
Lebih tepatnya, tahan <kbd>Ctrl</kbd> sambil menekan tombol klik kiri. Hingga tombol klik kiri dilepaskan, semua gerakan mouse berskala dan memutar konten (jika didukung oleh aplikasi) relatif ke tengah layar.
|
||||
|
||||
Secara konkret, scrcpy menghasilkan kejadian sentuh tambahan dari "jari virtual" di lokasi yang dibalik melalui bagian tengah layar.
|
||||
|
||||
|
||||
#### Preferensi injeksi teks
|
||||
|
||||
Ada dua jenis [peristiwa][textevents] dihasilkan saat mengetik teks:
|
||||
- _peristiwa penting_, menandakan bahwa tombol ditekan atau dilepaskan;
|
||||
- _peristiwa teks_, menandakan bahwa teks telah dimasukkan.
|
||||
|
||||
Secara default, huruf dimasukkan menggunakan peristiwa kunci, sehingga keyboard berperilaku seperti yang diharapkan dalam game (biasanya untuk tombol WASD).
|
||||
|
||||
Tapi ini mungkin [menyebabkan masalah][prefertext]. Jika Anda mengalami masalah seperti itu, Anda dapat menghindarinya dengan:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(tapi ini akan merusak perilaku keyboard dalam game)
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### Ulangi kunci
|
||||
|
||||
Secara default, menahan tombol akan menghasilkan peristiwa kunci yang berulang. Ini dapat menyebabkan masalah kinerja di beberapa game, di mana acara ini tidak berguna.
|
||||
|
||||
Untuk menghindari penerusan peristiwa penting yang berulang:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
|
||||
### Seret/jatuhkan file
|
||||
|
||||
#### Pasang APK
|
||||
|
||||
Untuk menginstal APK, seret & lepas file APK (diakhiri dengan `.apk`) ke jendela _scrcpy_.
|
||||
|
||||
Tidak ada umpan balik visual, log dicetak ke konsol.
|
||||
|
||||
|
||||
#### Dorong file ke perangkat
|
||||
|
||||
Untuk mendorong file ke `/sdcard/` di perangkat, seret & jatuhkan file (non-APK) ke jendela _scrcpy_.
|
||||
|
||||
Tidak ada umpan balik visual, log dicetak ke konsol.
|
||||
|
||||
Direktori target dapat diubah saat mulai:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
|
||||
### Penerusan audio
|
||||
|
||||
Audio tidak diteruskan oleh _scrcpy_. Gunakan [sndcpy].
|
||||
|
||||
Lihat juga [Masalah #14].
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[Masalah #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## Pintasan
|
||||
|
||||
Dalam daftar berikut, <kbd>MOD</kbd> adalah pengubah pintasan. Secara default, ini (kiri) <kbd>Alt</kbd> atau (kiri) <kbd>Super</kbd>.
|
||||
|
||||
Ini dapat diubah menggunakan `--shortcut-mod`. Kunci yang memungkinkan adalah `lctrl`,`rctrl`, `lalt`,` ralt`, `lsuper` dan` rsuper`. Sebagai contoh:
|
||||
|
||||
```bash
|
||||
# gunakan RCtrl untuk jalan pintas
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# gunakan baik LCtrl+LAlt atau LSuper untuk jalan pintas
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> biasanya adalah <kbd>Windows</kbd> atau <kbd>Cmd</kbd> key._
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Aksi | Pintasan
|
||||
| ------------------------------------------------------|:-----------------------------
|
||||
| Alihkan mode layar penuh | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| Putar layar kiri | <kbd>MOD</kbd>+<kbd>←</kbd> _(kiri)_
|
||||
| Putar layar kanan | <kbd>MOD</kbd>+<kbd>→</kbd> _(kanan)_
|
||||
| Ubah ukuran jendela menjadi 1:1 (piksel-sempurna) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Ubah ukuran jendela menjadi hapus batas hitam | <kbd>MOD</kbd>+<kbd>w</kbd> \| _klik-dua-kali¹_
|
||||
| Klik `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Klik-tengah_
|
||||
| Klik `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Klik-kanan²_
|
||||
| Klik `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd>
|
||||
| Klik `MENU` (buka kunci layar) | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| Klik `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(naik)_
|
||||
| Klik `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(turun)_
|
||||
| Klik `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| Menyalakan | _Klik-kanan²_
|
||||
| Matikan layar perangkat (tetap mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| Hidupkan layar perangkat | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| Putar layar perangkat | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| Luaskan panel notifikasi | <kbd>MOD</kbd>+<kbd>n</kbd>
|
||||
| Ciutkan panel notifikasi | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Menyalin ke papan klip³ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Potong ke papan klip³ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Sinkronkan papan klip dan tempel³ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Masukkan teks papan klip komputer | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Mengaktifkan/menonaktifkan penghitung FPS (di stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Cubit-untuk-memperbesar/memperkecil | <kbd>Ctrl</kbd>+_klik-dan-pindah_
|
||||
|
||||
_¹Klik-dua-kali pada batas hitam untuk menghapusnya._
|
||||
_²Klik-kanan akan menghidupkan layar jika mati, tekan BACK jika tidak._
|
||||
_³Hanya di Android >= 7._
|
||||
|
||||
Semua <kbd>Ctrl</kbd>+_key_ pintasan diteruskan ke perangkat, demikian adanya
|
||||
ditangani oleh aplikasi aktif.
|
||||
|
||||
|
||||
## Jalur kustom
|
||||
|
||||
Untuk menggunakan biner _adb_ tertentu, konfigurasikan jalurnya di variabel lingkungan `ADB`:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
Untuk mengganti jalur file `scrcpy-server`, konfigurasikan jalurnya di
|
||||
`SCRCPY_SERVER_PATH`.
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
|
||||
## Mengapa _scrcpy_?
|
||||
|
||||
Seorang kolega menantang saya untuk menemukan nama yang tidak dapat diucapkan seperti [gnirehtet].
|
||||
|
||||
[`strcpy`] menyalin sebuah **str**ing; `scrcpy` menyalin sebuah **scr**een.
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## Bagaimana Cara membangun?
|
||||
|
||||
Lihat [BUILD].
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
|
||||
## Masalah umum
|
||||
|
||||
Lihat [FAQ](FAQ.md).
|
||||
|
||||
|
||||
## Pengembang
|
||||
|
||||
Baca [halaman pengembang].
|
||||
|
||||
[halaman pengembang]: DEVELOP.md
|
||||
|
||||
|
||||
## Lisensi
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 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.
|
||||
|
||||
## Artikel
|
||||
|
||||
- [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/
|
||||
|
725
README.jp.md
725
README.jp.md
@ -1,725 +0,0 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
# scrcpy (v1.17)
|
||||
|
||||
このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。
|
||||
|
||||

|
||||
|
||||
以下に焦点を当てています:
|
||||
|
||||
- **軽量** (ネイティブ、デバイス画面表示のみ)
|
||||
- **パフォーマンス** (30~60fps)
|
||||
- **クオリティ** (1920x1080以上)
|
||||
- **低遅延** ([35~70ms][lowlatency])
|
||||
- **短い起動時間** (初回画像を1秒以内に表示)
|
||||
- **非侵入型** (デバイスに何もインストールされていない状態になる)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## 必要要件
|
||||
|
||||
AndroidデバイスはAPI21(Android 5.0)以上。
|
||||
|
||||
Androidデバイスで[adbデバッグが有効][enable-adb]であること。
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
一部のAndroidデバイスでは、キーボードとマウスを使用して制御する[追加オプション][control]を有効にする必要がある。
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## アプリの取得
|
||||
|
||||
<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
|
||||
|
||||
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
|
||||
|
||||
[自分でビルド][BUILD]も可能(心配しないでください、それほど難しくはありません。)
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
Windowsでは簡単に、(`adb`を含む)すべての依存関係を構築済みのアーカイブを利用可能です。
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
[Chocolatey]でも利用可能です:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # まだ入手していない場合
|
||||
```
|
||||
|
||||
[Scoop]でも利用可能です:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # まだ入手していない場合
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
また、[アプリケーションをビルド][BUILD]することも可能です。
|
||||
|
||||
### macOS
|
||||
|
||||
アプリケーションは[Homebrew]で利用可能です。ただインストールするだけです。
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
`PATH`から`adb`へのアクセスが必要です。もしまだ持っていない場合:
|
||||
|
||||
```bash
|
||||
# Homebrew >= 2.6.0
|
||||
brew install --cask android-platform-tools
|
||||
|
||||
# Homebrew < 2.6.0
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
また、[アプリケーションをビルド][BUILD]することも可能です。
|
||||
|
||||
|
||||
## 実行
|
||||
|
||||
Androidデバイスを接続し、実行:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
次のコマンドでリストされるコマンドライン引数も受け付けます:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## 機能
|
||||
|
||||
### キャプチャ構成
|
||||
|
||||
#### サイズ削減
|
||||
|
||||
Androidデバイスを低解像度でミラーリングする場合、パフォーマンス向上に便利な場合があります。
|
||||
|
||||
幅と高さをある値(例:1024)に制限するには:
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # 短縮版
|
||||
```
|
||||
|
||||
一方のサイズはデバイスのアスペクト比が維持されるように計算されます。この方法では、1920x1080のデバイスでは1024x576にミラーリングされます。
|
||||
|
||||
|
||||
#### ビットレート変更
|
||||
|
||||
ビットレートの初期値は8Mbpsです。ビットレートを変更するには(例:2Mbpsに変更):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # 短縮版
|
||||
```
|
||||
|
||||
#### フレームレート制限
|
||||
|
||||
キャプチャするフレームレートを制限できます:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
この機能は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 --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
利用可能なエンコーダをリストするために、無効なエンコーダ名を渡すことができます。エラー表示で利用可能なエンコーダを提供します。
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### 録画
|
||||
|
||||
ミラーリング中に画面の録画をすることが可能です:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
録画中にミラーリングを無効にするには:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# Ctrl+Cで録画を中断する
|
||||
```
|
||||
|
||||
"スキップされたフレーム"は(パフォーマンス上の理由で)リアルタイムで表示されなくても録画されます。
|
||||
|
||||
フレームはデバイス上で _タイムスタンプされる_ ため [パケット遅延のバリエーション] は録画されたファイルに影響を与えません。
|
||||
|
||||
[パケット遅延のバリエーション]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
### 接続
|
||||
|
||||
#### ワイヤレス
|
||||
|
||||
_Scrcpy_ はデバイスとの通信に`adb`を使用します。そして`adb`はTCP/IPを介しデバイスに[接続]することができます:
|
||||
|
||||
1. あなたのコンピュータと同じWi-Fiに接続します。
|
||||
2. あなたのIPアドレスを取得します。設定 → 端末情報 → ステータス情報、もしくは、このコマンドを実行します:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. あなたのデバイスでTCP/IPを介したadbを有効にします: `adb tcpip 5555`
|
||||
4. あなたのデバイスの接続を外します。
|
||||
5. あなたのデバイスに接続します:
|
||||
`adb connect DEVICE_IP:5555` _(`DEVICE_IP`は置き換える)_
|
||||
6. 通常通り`scrcpy`を実行します。
|
||||
|
||||
この方法はビットレートと解像度を減らすのにおそらく有用です:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # 短縮版
|
||||
```
|
||||
|
||||
[接続]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### マルチデバイス
|
||||
|
||||
もし`adb devices`でいくつかのデバイスがリストされる場合、 _シリアルナンバー_ を指定する必要があります:
|
||||
|
||||
```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トンネル
|
||||
|
||||
リモートデバイスに接続するため、ローカル`adb`クライアントからリモート`adb`サーバーへ接続することが可能です(同じバージョンの _adb_ プロトコルを使用している場合):
|
||||
|
||||
```bash
|
||||
adb kill-server # 5037ポートのローカルadbサーバーを終了する
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# オープンしたままにする
|
||||
```
|
||||
|
||||
他の端末から:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
リモートポート転送の有効化を回避するためには、代わりに転送接続を強制することができます(`-R`の代わりに`-L`を使用することに注意):
|
||||
|
||||
```bash
|
||||
adb kill-server # 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>はデバイスに縦向きと横向きの切り替えを要求する(現在実行中のアプリで要求している向きをサポートしていない場合、拒否することがある)
|
||||
- [`--lock-video-orientation`](#ビデオの向きをロックする)は、ミラーリングする向きを変更する(デバイスからPCへ送信される向き)。録画に影響します。
|
||||
- `--rotation` (もしくは<kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)は、ウィンドウのコンテンツのみを回転します。これは表示にのみに影響し、録画には影響しません。
|
||||
|
||||
### 他のミラーリングオプション
|
||||
|
||||
#### Read-only リードオンリー
|
||||
|
||||
制御を無効にするには(デバイスと対話する全てのもの:入力キー、マウスイベント、ファイルのドラッグ&ドロップ):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### ディスプレイ
|
||||
|
||||
いくつか利用可能なディスプレイがある場合、ミラーリングするディスプレイを選択できます:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
ディスプレイIDのリストは次の方法で取得できます:
|
||||
|
||||
```
|
||||
adb shell dumpsys display # search "mDisplayId=" in the output
|
||||
```
|
||||
|
||||
セカンダリディスプレイは、デバイスが少なくともAndroid 10の場合にコントロール可能です。(それ以外ではリードオンリーでミラーリングされます)
|
||||
|
||||
|
||||
#### 起動状態にする
|
||||
|
||||
デバイス接続時、少し遅れてからデバイスのスリープを防ぐには:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
scrcpyが閉じられた時、初期状態に復元されます。
|
||||
|
||||
#### 画面OFF
|
||||
|
||||
コマンドラインオプションを使用することで、ミラーリングの開始時にデバイスの画面をOFFにすることができます:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
(デバイス上で指を使った) _物理的な_ タッチのみ表示されることに注意してください。
|
||||
|
||||
|
||||
#### スクリーンセーバー無効
|
||||
|
||||
初期状態では、scrcpyはコンピュータ上でスクリーンセーバーが実行される事を妨げません。
|
||||
|
||||
これを無効にするには:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### 入力制御
|
||||
|
||||
#### デバイス画面の回転
|
||||
|
||||
<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> `COPY`を挿入
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> `CUT`を挿入
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> `PASTE`を挿入(コンピュータとデバイスのクリップボードが同期された後)
|
||||
|
||||
加えて、<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>はコンピュータのクリップボードテキストにキーイベントのシーケンスとして挿入することを許可します。これはコンポーネントがテキストのペーストを許可しない場合(例えば _Termux_)に有用ですが、非ASCIIコンテンツを壊す可能性があります。
|
||||
|
||||
**警告:** デバイスにコンピュータのクリップボードを(<kbd>Ctrl</kbd>+<kbd>v</kbd>または<kbd>MOD</kbd>+<kbd>v</kbd>を介して)ペーストすることは、デバイスのクリップボードにコンテンツをコピーします。結果としてどのAndoridアプリケーションもそのコンテンツを読み取ることができます。機密性の高いコンテンツ(例えばパスワードなど)をこの方法でペーストすることは避けてください。
|
||||
|
||||
プログラムでデバイスのクリップボードを設定した場合、一部のデバイスは期待どおりに動作しません。`--legacy-paste`オプションは、コンピュータのクリップボードテキストをキーイベントのシーケンスとして挿入するため(<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>と同じ方法)、<kbd>Ctrl</kbd>+<kbd>v</kbd>と<kbd>MOD</kbd>+<kbd>v</kbd>の動作の変更を提供します。
|
||||
|
||||
#### ピンチしてズームする
|
||||
|
||||
"ピンチしてズームする"をシミュレートするには: <kbd>Ctrl</kbd>+_クリック&移動_
|
||||
|
||||
より正確にするには、左クリックボタンを押している間、<kbd>Ctrl</kbd>を押したままにします。左クリックボタンを離すまで、全てのマウスの動きは、(アプリでサポートされている場合)画面の中心を基準として、コンテンツを拡大縮小および回転します。
|
||||
|
||||
具体的には、scrcpyは画面の中央を反転した位置にある"バーチャルフィンガー"から追加のタッチイベントを生成します。
|
||||
|
||||
|
||||
#### テキストインジェクション環境設定
|
||||
|
||||
テキストをタイプした時に生成される2種類の[イベント][textevents]があります:
|
||||
- _key events_ はキーを押したときと離したことを通知します。
|
||||
- _text 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
|
||||
```
|
||||
|
||||
|
||||
#### 右クリックと真ん中クリック
|
||||
|
||||
初期状態では、右クリックはバックの動作(もしくはパワーオン)を起こし、真ん中クリックではホーム画面へ戻ります。このショートカットを無効にし、代わりにデバイスへクリックを転送するには:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### ファイルのドロップ
|
||||
|
||||
#### APKのインストール
|
||||
|
||||
APKをインストールするには、(`.apk`で終わる)APKファイルを _scrcpy_ の画面にドラッグ&ドロップします。
|
||||
|
||||
見た目のフィードバックはありません。コンソールにログが出力されます。
|
||||
|
||||
|
||||
#### デバイスにファイルを送る
|
||||
|
||||
デバイスの`/sdcard/`ディレクトリにファイルを送るには、(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>でショートカット変更します。初期状態では、(left)<kbd>Alt</kbd>または(left)<kbd>Super</kbd>です。
|
||||
|
||||
これは`--shortcut-mod`で変更することができます。可能なキーは`lctrl`、`rctrl`、`lalt`、 `ralt`、 `lsuper`そして`rsuper`です。例えば:
|
||||
|
||||
```bash
|
||||
# RCtrlをショートカットとして使用します
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# ショートカットにLCtrl+LAltまたはLSuperのいずれかを使用します
|
||||
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)
|
||||
|
||||
| アクション | ショートカット
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| フルスクリーンモードへの切り替え | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| ディスプレイを左に回転 | <kbd>MOD</kbd>+<kbd>←</kbd> _(左)_
|
||||
| ディスプレイを右に回転 | <kbd>MOD</kbd>+<kbd>→</kbd> _(右)_
|
||||
| ウィンドウサイズを変更して1:1に変更(ピクセルパーフェクト) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| ウィンドウサイズを変更して黒い境界線を削除 | <kbd>MOD</kbd>+<kbd>w</kbd> \| _ダブルクリック¹_
|
||||
| `HOME`をクリック | <kbd>MOD</kbd>+<kbd>h</kbd> \| _真ん中クリック_
|
||||
| `BACK`をクリック | <kbd>MOD</kbd>+<kbd>b</kbd> \| _右クリック²_
|
||||
| `APP_SWITCH`をクリック | <kbd>MOD</kbd>+<kbd>s</kbd>
|
||||
| `MENU` (画面のアンロック)をクリック | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| `VOLUME_UP`をクリック | <kbd>MOD</kbd>+<kbd>↑</kbd> _(上)_
|
||||
| `VOLUME_DOWN`をクリック | <kbd>MOD</kbd>+<kbd>↓</kbd> _(下)_
|
||||
| `POWER`をクリック | <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カウンタ有効/無効(標準入出力上) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| ピンチしてズームする | <kbd>Ctrl</kbd>+_クリック&移動_
|
||||
|
||||
_¹黒い境界線を削除するため、境界線上でダブルクリック_
|
||||
_²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._
|
||||
_³Android 7以上のみ._
|
||||
|
||||
全ての<kbd>Ctrl</kbd>+_キー_ ショートカットはデバイスに転送されます、そのためアクティブなアプリケーションによって処理されます。
|
||||
|
||||
|
||||
## カスタムパス
|
||||
|
||||
特定の _adb_ バイナリを使用する場合、そのパスを環境変数`ADB`で構成します:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
`scrcpy-server`ファイルのパスを上書きするには、`SCRCPY_SERVER_PATH`でそのパスを構成します。
|
||||
|
||||
[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.md)を参照してください。
|
||||
|
||||
|
||||
## 開発者
|
||||
|
||||
[開発者のページ]を読んでください。
|
||||
|
||||
[開発者のページ]: DEVELOP.md
|
||||
|
||||
|
||||
## ライセンス
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 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.
|
||||
|
||||
## 記事
|
||||
|
||||
- [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/
|
@ -1,5 +1,3 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
# scrcpy (v1.11)
|
||||
|
||||
This document will be updated frequently along with the original Readme file
|
||||
@ -68,7 +66,9 @@ Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link].
|
||||
|
||||
윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) :
|
||||
해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다.
|
||||
- [README](README.md#windows)
|
||||
- [`scrcpy-win`][direct-win]
|
||||
|
||||
[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows
|
||||
|
||||
|
||||
[어플을 직접 설치][BUILD] 할 수도 있습니다.
|
||||
@ -475,7 +475,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상
|
||||
## 라이선스
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 Romain Vimont
|
||||
Copyright (C) 2018-2020 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
303
README.md
303
README.md
@ -1,6 +1,4 @@
|
||||
# scrcpy (v1.17)
|
||||
|
||||
[Read in another language](#translations)
|
||||
# scrcpy (v1.13)
|
||||
|
||||
This application provides display and control of Android devices connected on
|
||||
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
||||
@ -36,19 +34,6 @@ control it using keyboard and mouse.
|
||||
|
||||
## 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>
|
||||
|
||||
### Summary
|
||||
|
||||
- Linux: `apt install scrcpy`
|
||||
- Windows: [download][direct-win64]
|
||||
- macOS: `brew install scrcpy`
|
||||
|
||||
Build from sources: [BUILD] ([simplified process][BUILD_simple])
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
[BUILD_simple]: BUILD.md#simple
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
@ -64,11 +49,6 @@ A [Snap] package is available: [`scrcpy`][snap-link].
|
||||
|
||||
[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].
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
@ -79,8 +59,9 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
You could also [build the app manually][BUILD] ([simplified
|
||||
process][BUILD_simple]).
|
||||
You could also [build the app manually][BUILD] (don't worry, it's not that
|
||||
hard).
|
||||
|
||||
|
||||
|
||||
### Windows
|
||||
@ -88,10 +69,10 @@ process][BUILD_simple]).
|
||||
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
||||
(including `adb`) is available:
|
||||
|
||||
- [`scrcpy-win64-v1.17.zip`][direct-win64]
|
||||
_(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_
|
||||
- [`scrcpy-win64-v1.13.zip`][direct-win64]
|
||||
_(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_
|
||||
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip
|
||||
|
||||
It is also available in [Chocolatey]:
|
||||
|
||||
@ -127,10 +108,6 @@ brew install scrcpy
|
||||
You need `adb`, accessible from your `PATH`. If you don't have it yet:
|
||||
|
||||
```bash
|
||||
# Homebrew >= 2.6.0
|
||||
brew install --cask android-platform-tools
|
||||
|
||||
# Homebrew < 2.6.0
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
@ -217,24 +194,6 @@ scrcpy --lock-video-orientation 3 # 90° clockwise
|
||||
|
||||
This affects recording orientation.
|
||||
|
||||
The [window may also be rotated](#rotation) independently.
|
||||
|
||||
|
||||
#### 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
|
||||
|
||||
@ -268,13 +227,7 @@ _Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
||||
device over TCP/IP:
|
||||
|
||||
1. Connect the device to the same Wi-Fi as your computer.
|
||||
2. Get your device IP address, in Settings → About phone → Status, or by
|
||||
executing this command:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
2. Get your device IP address (in Settings → About phone → Status).
|
||||
3. Enable adb over TCP/IP on your device: `adb tcpip 5555`.
|
||||
4. Unplug your device.
|
||||
5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_.
|
||||
@ -336,22 +289,6 @@ From another terminal:
|
||||
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:
|
||||
|
||||
```
|
||||
@ -401,7 +338,7 @@ scrcpy --fullscreen
|
||||
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
|
||||
|
||||
@ -417,19 +354,18 @@ Possibles values are:
|
||||
- `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)_.
|
||||
The rotation can also be changed dynamically with `Ctrl`+`←` _(left)_ and
|
||||
`Ctrl`+`→` _(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 not support the
|
||||
requested orientation).
|
||||
- [`--lock-video-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
|
||||
- `Ctrl`+`r` requests the device to switch between portrait and landscape (the
|
||||
current running app may refuse, if it does support the requested
|
||||
orientation).
|
||||
- `--lock-video-orientation` changes the mirroring orientation (the orientation
|
||||
of the video sent from the device to the computer). This affects the
|
||||
recording.
|
||||
- `--rotation` (or `Ctrl`+`←`/`Ctrl`+`→`) rotates only the window content. This
|
||||
affects only the display, not the recording.
|
||||
|
||||
|
||||
### Other mirroring options
|
||||
@ -459,13 +395,9 @@ The list of display ids can be retrieved by:
|
||||
adb shell dumpsys display # search "mDisplayId=" in the output
|
||||
```
|
||||
|
||||
The secondary display may only be controlled if the device runs at least Android
|
||||
10 (otherwise it is mirrored in read-only).
|
||||
|
||||
|
||||
#### Stay awake
|
||||
|
||||
To prevent the device to sleep after some delay when the device is plugged in:
|
||||
To prevent the device to sleep after some delay:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
@ -485,16 +417,11 @@ scrcpy --turn-screen-off
|
||||
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>.
|
||||
To turn it back on, press `POWER` (or `Ctrl`+`p`).
|
||||
|
||||
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:
|
||||
It can be useful to also prevent the device to sleep:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
@ -532,78 +459,24 @@ scrcpy -t
|
||||
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
|
||||
|
||||
#### Rotate device screen
|
||||
|
||||
Press <kbd>MOD</kbd>+<kbd>r</kbd> to switch between portrait and landscape
|
||||
modes.
|
||||
Press `Ctrl`+`r` to switch between portrait and landscape modes.
|
||||
|
||||
Note that it rotates only if the application in foreground supports the
|
||||
requested orientation.
|
||||
|
||||
#### Copy-paste
|
||||
|
||||
Any time the Android clipboard changes, it is automatically synchronized to the
|
||||
computer clipboard.
|
||||
|
||||
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.
|
||||
It is possible to synchronize clipboards between the computer and the device, in
|
||||
both directions:
|
||||
|
||||
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
||||
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
|
||||
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||
breaks non-ASCII characters).
|
||||
|
||||
#### Text injection preference
|
||||
|
||||
@ -627,28 +500,6 @@ scrcpy --prefer-text
|
||||
[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
|
||||
|
||||
#### Install APK
|
||||
@ -669,73 +520,48 @@ There is no visual feedback, a log is printed to the console.
|
||||
The target directory can be changed on start:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Download/
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
|
||||
### Audio forwarding
|
||||
|
||||
Audio is not forwarded by _scrcpy_. Use [sndcpy].
|
||||
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
|
||||
|
||||
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
|
||||
|
||||
|
||||
## Shortcuts
|
||||
|
||||
In the following list, <kbd>MOD</kbd> is the shortcut modifier. By default, it's
|
||||
(left) <kbd>Alt</kbd> or (left) <kbd>Super</kbd>.
|
||||
|
||||
It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`,
|
||||
`lalt`, `ralt`, `lsuper` and `rsuper`. For example:
|
||||
|
||||
```bash
|
||||
# use RCtrl for shortcuts
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# use either LCtrl+LAlt or LSuper for shortcuts
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Action | Shortcut
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| 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_
|
||||
| Action | Shortcut | Shortcut (macOS)
|
||||
| -------------------------------------- |:----------------------------- |:-----------------------------
|
||||
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
|
||||
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
|
||||
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
|
||||
| Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g`
|
||||
| Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
|
||||
| Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
|
||||
| Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
|
||||
| Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
|
||||
| Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
|
||||
| Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
|
||||
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
||||
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||
| Power on | _Right-click²_ | _Right-click²_
|
||||
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
||||
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
||||
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
||||
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
||||
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
|
||||
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
||||
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
||||
|
||||
_¹Double-click on black borders to remove them._
|
||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||
_³Only on Android >= 7._
|
||||
|
||||
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
|
||||
handled by the active application.
|
||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||
|
||||
|
||||
## Custom paths
|
||||
@ -765,6 +591,8 @@ A colleague challenged me to find a name as unpronounceable as [gnirehtet].
|
||||
|
||||
See [BUILD].
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
|
||||
## Common issues
|
||||
|
||||
@ -781,7 +609,7 @@ Read the [developers page].
|
||||
## Licence
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 Romain Vimont
|
||||
Copyright (C) 2018-2020 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -802,16 +630,3 @@ Read the [developers page].
|
||||
|
||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
|
||||
## Translations
|
||||
|
||||
This README is available in other languages:
|
||||
|
||||
- [Indonesian (Indonesia, `id`) - v1.16](README.id.md)
|
||||
- [日本語 (Japanese, `jp`) - v1.17](README.jp.md)
|
||||
- [한국어 (Korean, `ko`) - v1.11](README.ko.md)
|
||||
- [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md)
|
||||
- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md)
|
||||
- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md)
|
||||
|
||||
Only this README file is guaranteed to be up-to-date.
|
||||
|
792
README.pt-br.md
792
README.pt-br.md
@ -1,792 +0,0 @@
|
||||
_Apenas o [README](README.md) original é garantido estar atualizado._
|
||||
|
||||
# scrcpy (v1.17)
|
||||
|
||||
Esta aplicação fornece exibição e controle de dispositivos Android conectados via
|
||||
USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso _root_.
|
||||
Funciona em _GNU/Linux_, _Windows_ e _macOS_.
|
||||
|
||||

|
||||
|
||||
Foco em:
|
||||
|
||||
- **leveza** (nativo, mostra apenas a tela do dispositivo)
|
||||
- **performance** (30~60fps)
|
||||
- **qualidade** (1920×1080 ou acima)
|
||||
- **baixa latência** ([35~70ms][lowlatency])
|
||||
- **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem)
|
||||
- **não intrusivo** (nada é deixado instalado no dispositivo)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## Requisitos
|
||||
|
||||
O dispositivo Android requer pelo menos a API 21 (Android 5.0).
|
||||
|
||||
Tenha certeza de ter [ativado a depuração adb][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 precisa ativar [uma opção adicional][control] para
|
||||
controlá-lo usando teclado e mouse.
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## Obter o 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
|
||||
|
||||
No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04):
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
Um 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 Fedora, um pacote [COPR] está disponível: [`scrcpy`][copr-link].
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
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 o app manualmente][BUILD] (não se preocupe, não é tão
|
||||
difícil).
|
||||
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
Para Windows, por simplicidade, um arquivo pré-compilado com todas as dependências
|
||||
(incluindo `adb`) está disponível:
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
Também está 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 o app manualmente][BUILD].
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
A aplicação está disponível em [Homebrew]. Apenas instale-a:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem:
|
||||
|
||||
```bash
|
||||
# Homebrew >= 2.6.0
|
||||
brew install --cask android-platform-tools
|
||||
|
||||
# Homebrew < 2.6.0
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
Você também pode [compilar o app manualmente][BUILD].
|
||||
|
||||
|
||||
## Executar
|
||||
|
||||
Conecte 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
|
||||
|
||||
#### Reduzir tamanho
|
||||
|
||||
Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para
|
||||
aumentar a performance.
|
||||
|
||||
Para limitar ambos (largura e altura) para algum valor (ex: 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # versão curta
|
||||
```
|
||||
|
||||
A outra dimensão é calculada para que a proporção do dispositivo seja preservada.
|
||||
Dessa forma, um dispositivo de 1920x1080 será espelhado em 1024x576.
|
||||
|
||||
|
||||
#### Mudar bit-rate
|
||||
|
||||
O bit-rate padrão é 8 Mbps. Para mudar o bit-rate do vídeo (ex: para 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # versão curta
|
||||
```
|
||||
|
||||
#### Limitar frame rate
|
||||
|
||||
O frame rate de captura pode ser limitado:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
Isso é oficialmente suportado desde o Android 10, mas pode funcionar em versões anteriores.
|
||||
|
||||
#### Cortar
|
||||
|
||||
A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela.
|
||||
|
||||
Isso é útil por exemplo, para 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, o redimensionamento é aplicado após o corte.
|
||||
|
||||
|
||||
#### Travar orientação do vídeo
|
||||
|
||||
|
||||
Para travar a orientação do espelhamento:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation 0 # orientação natural
|
||||
scrcpy --lock-video-orientation 1 # 90° sentido anti-horário
|
||||
scrcpy --lock-video-orientation 2 # 180°
|
||||
scrcpy --lock-video-orientation 3 # 90° sentido horário
|
||||
```
|
||||
|
||||
Isso afeta a orientação de gravação.
|
||||
|
||||
A [janela também pode ser rotacionada](#rotação) independentemente.
|
||||
|
||||
|
||||
#### Encoder
|
||||
|
||||
Alguns dispositivos têm mais de um encoder, e alguns deles podem causar problemas ou
|
||||
travar. É possível selecionar um encoder diferente:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
Para listar os encoders disponíveis, você pode passar um nome de encoder inválido, o
|
||||
erro dará os encoders disponíveis:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### 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
|
||||
# interrompa a gravação com Ctrl+C
|
||||
```
|
||||
|
||||
"Frames pulados" são gravados, mesmo que não sejam exibidos em tempo real (por
|
||||
motivos de performance). Frames têm seu _horário carimbado_ no dispositivo, então [variação de atraso nos
|
||||
pacotes][packet delay variation] não impacta o arquivo gravado.
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
### Conexão
|
||||
|
||||
#### Sem fio
|
||||
|
||||
_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se][connect] a um
|
||||
dispositivo via TCP/IP:
|
||||
|
||||
1. Conecte o dispositivo no mesmo Wi-Fi do seu computador.
|
||||
2. Pegue o endereço IP do seu dispositivo, em Configurações → Sobre o telefone → Status, ou
|
||||
executando este comando:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`.
|
||||
4. Desconecte seu dispositivo.
|
||||
5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua `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 curta
|
||||
```
|
||||
|
||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### Múltiplos dispositivos
|
||||
|
||||
Se vários dispositivos são listados em `adb devices`, você deve especificar o _serial_:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # versão curta
|
||||
```
|
||||
|
||||
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 curta
|
||||
```
|
||||
|
||||
Você pode iniciar várias instâncias do _scrcpy_ para vários dispositivos.
|
||||
|
||||
#### Iniciar automaticamente quando dispositivo é conectado
|
||||
|
||||
Você pode usar [AutoAdb]:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### Túnel SSH
|
||||
|
||||
Para conectar-se a um dispositivo remoto, é possível conectar um cliente `adb` local a
|
||||
um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo
|
||||
_adb_):
|
||||
|
||||
```bash
|
||||
adb kill-server # encerra o servidor adb local em 5037
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# mantenha isso aberto
|
||||
```
|
||||
|
||||
De outro terminal:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Para evitar ativar o encaminhamento de porta remota, você pode forçar uma conexão
|
||||
de encaminhamento (note o `-L` em vez de `-R`):
|
||||
|
||||
```bash
|
||||
adb kill-server # encerra o servidor adb local em 5037
|
||||
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# mantenha isso aberto
|
||||
```
|
||||
|
||||
De outro terminal:
|
||||
|
||||
```bash
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
Igual a conexões sem fio, pode ser útil reduzir a qualidade:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### Configuração de janela
|
||||
|
||||
#### Título
|
||||
|
||||
Por padrão, o título da janela é o modelo do dispositivo. Isso 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 de janela:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### Sempre no topo
|
||||
|
||||
Para manter a janela do scrcpy sempre no topo:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### Tela cheia
|
||||
|
||||
A aplicação pode ser iniciada diretamente em tela cheia:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # versão curta
|
||||
```
|
||||
|
||||
Tela cheia pode ser alternada dinamicamente com <kbd>MOD</kbd>+<kbd>f</kbd>.
|
||||
|
||||
#### Rotação
|
||||
|
||||
A janela pode ser rotacionada:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
Valores possíveis são:
|
||||
- `0`: sem rotação
|
||||
- `1`: 90 graus sentido anti-horário
|
||||
- `2`: 180 graus
|
||||
- `3`: 90 graus sentido horário
|
||||
|
||||
A rotação também pode ser mudada dinamicamente com <kbd>MOD</kbd>+<kbd>←</kbd>
|
||||
_(esquerda)_ e <kbd>MOD</kbd>+<kbd>→</kbd> _(direita)_.
|
||||
|
||||
Note que _scrcpy_ controla 3 rotações diferentes:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> requisita ao dispositivo para mudar entre retrato
|
||||
e paisagem (a aplicação em execução pode se recusar, se ela não suporta a
|
||||
orientação requisitada).
|
||||
- [`--lock-video-orientation`](#travar-orientação-do-vídeo) muda a orientação de
|
||||
espelhamento (a orientação do vídeo enviado pelo dispositivo para o
|
||||
computador). Isso afeta a gravação.
|
||||
- `--rotation` (ou <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)
|
||||
rotaciona apenas o conteúdo da janela. Isso afeta apenas a exibição, não a
|
||||
gravação.
|
||||
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
#### Display
|
||||
|
||||
Se vários displays estão disponíveis, é possível selecionar o display para
|
||||
espelhar:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
A lista de IDs dos displays pode ser obtida por:
|
||||
|
||||
```
|
||||
adb shell dumpsys display # busca "mDisplayId=" na saída
|
||||
```
|
||||
|
||||
O display secundário pode apenas ser controlado se o dispositivo roda pelo menos Android
|
||||
10 (caso contrário é espelhado como apenas leitura).
|
||||
|
||||
|
||||
#### Permanecer ativo
|
||||
|
||||
Para evitar que o dispositivo seja suspenso após um delay quando o dispositivo é conectado:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
O estado inicial é restaurado quando o scrcpy é fechado.
|
||||
|
||||
|
||||
#### Desligar 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 <kbd>MOD</kbd>+<kbd>o</kbd> a qualquer momento.
|
||||
|
||||
Para ligar novamente, pressione <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
|
||||
No Android, o botão de `POWER` sempre liga a tela. Por conveniência, se
|
||||
`POWER` é enviado via scrcpy (via clique-direito ou <kbd>MOD</kbd>+<kbd>p</kbd>), ele
|
||||
forçará a desligar a tela após um delay pequeno (numa base de melhor esforço).
|
||||
O botão `POWER` físico ainda causará a tela ser ligada.
|
||||
|
||||
Também pode ser útil evitar que o dispositivo seja suspenso:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
|
||||
#### Renderizar frames expirados
|
||||
|
||||
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 um possível aumento de
|
||||
latência), use:
|
||||
|
||||
```bash
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
#### Mostrar toques
|
||||
|
||||
Para apresentações, pode ser útil mostrar toques físicos (no 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 restaurar o
|
||||
valor inicial no encerramento:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo).
|
||||
|
||||
|
||||
#### Desativar descanso de tela
|
||||
|
||||
Por padrão, scrcpy não evita que o descanso de tela rode no computador.
|
||||
|
||||
Para desativá-lo:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### Controle de entrada
|
||||
|
||||
#### Rotacionar a tela do dispositivo
|
||||
|
||||
Pressione <kbd>MOD</kbd>+<kbd>r</kbd> para mudar entre os modos retrato e
|
||||
paisagem.
|
||||
|
||||
Note que só será rotacionado se a aplicação em primeiro plano suportar a
|
||||
orientação requisitada.
|
||||
|
||||
#### Copiar-colar
|
||||
|
||||
Sempre que a área de transferência do Android muda, é automaticamente sincronizada com a
|
||||
área de transferência do computador.
|
||||
|
||||
Qualquer atalho com <kbd>Ctrl</kbd> é encaminhado para o dispositivo. Em particular:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> tipicamente copia
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> tipicamente recorta
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> tipicamente cola (após a sincronização de área de transferência
|
||||
computador-para-dispositivo)
|
||||
|
||||
Isso tipicamente funciona como esperado.
|
||||
|
||||
O comportamento de fato depende da aplicação ativa, no entanto. Por exemplo,
|
||||
_Termux_ envia SIGINT com <kbd>Ctrl</kbd>+<kbd>c</kbd>, e _K-9 Mail_
|
||||
compõe uma nova mensagem.
|
||||
|
||||
Para copiar, recortar e colar em tais casos (mas apenas suportado no Android >= 7):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> injeta `COPY`
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> injeta `CUT`
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> injeta `PASTE` (após a sincronização de área de transferência
|
||||
computador-para-dispositivo)
|
||||
|
||||
Em adição, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> permite injetar o
|
||||
texto da área de transferência do computador como uma sequência de eventos de tecla. Isso é útil quando o
|
||||
componente não aceita colar texto (por exemplo no _Termux_), mas pode
|
||||
quebrar conteúdo não-ASCII.
|
||||
|
||||
**ADVERTÊNCIA:** Colar a área de transferência do computador para o dispositivo (tanto via
|
||||
<kbd>Ctrl</kbd>+<kbd>v</kbd> quanto <kbd>MOD</kbd>+<kbd>v</kbd>) copia o conteúdo
|
||||
para a área de transferência do dispositivo. Como consequência, qualquer aplicação Android pode ler
|
||||
o seu conteúdo. Você deve evitar colar conteúdo sensível (como senhas) dessa
|
||||
forma.
|
||||
|
||||
Alguns dispositivos não se comportam como esperado quando a área de transferência é definida
|
||||
programaticamente. Uma opção `--legacy-paste` é fornecida para mudar o comportamento
|
||||
de <kbd>Ctrl</kbd>+<kbd>v</kbd> e <kbd>MOD</kbd>+<kbd>v</kbd> para que eles
|
||||
também injetem o texto da área de transferência do computador como uma sequência de eventos de tecla (da mesma
|
||||
forma que <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
||||
|
||||
#### Pinçar para dar zoom
|
||||
|
||||
Para simular "pinçar para dar zoom": <kbd>Ctrl</kbd>+_clicar-e-mover_.
|
||||
|
||||
Mais precisamente, segure <kbd>Ctrl</kbd> enquanto pressiona o botão de clique-esquerdo. Até que
|
||||
o botão de clique-esquerdo seja liberado, todos os movimentos do mouse ampliar e rotacionam o
|
||||
conteúdo (se suportado pelo app) relativo ao centro da tela.
|
||||
|
||||
Concretamente, scrcpy gera eventos adicionais de toque de um "dedo virtual" em
|
||||
uma posição invertida em relação ao centro da tela.
|
||||
|
||||
|
||||
#### Preferência de injeção de texto
|
||||
|
||||
Existem dois tipos de [eventos][textevents] gerados ao digitar um texto:
|
||||
- _eventos de tecla_, 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 tecla, assim o teclado comporta-se
|
||||
como esperado em jogos (normalmente para teclas WASD).
|
||||
|
||||
Mas isso pode [causar problemas][prefertext]. Se você encontrar tal problema, você
|
||||
pode evitá-lo com:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(mas isso 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
|
||||
|
||||
|
||||
#### Repetir tecla
|
||||
|
||||
Por padrão, segurar uma tecla gera eventos de tecla repetidos. Isso pode causar
|
||||
problemas de performance em alguns jogos, onde esses eventos são inúteis de qualquer forma.
|
||||
|
||||
Para evitar o encaminhamento eventos de tecla repetidos:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
|
||||
#### Clique-direito e clique-do-meio
|
||||
|
||||
Por padrão, clique-direito dispara BACK (ou POWER) e clique-do-meio dispara
|
||||
HOME. Para desabilitar esses atalhos e encaminhar os cliques para o dispositivo:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### Soltar 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 dispositivo
|
||||
|
||||
Para enviar um arquivo para `/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 é encaminhado pelo _scrcpy_. Use [sndcpy].
|
||||
|
||||
Também veja [issue #14].
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## Atalhos
|
||||
|
||||
Na lista a seguir, <kbd>MOD</kbd> é o modificador de atalho. Por padrão, é
|
||||
<kbd>Alt</kbd> (esquerdo) ou <kbd>Super</kbd> (esquerdo).
|
||||
|
||||
Ele pode ser mudado usando `--shortcut-mod`. Possíveis teclas são `lctrl`, `rctrl`,
|
||||
`lalt`, `ralt`, `lsuper` e `rsuper`. Por exemplo:
|
||||
|
||||
```bash
|
||||
# usar RCtrl para atalhos
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# usar tanto LCtrl+LAlt quanto LSuper para atalhos
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> é tipicamente a tecla <kbd>Windows</kbd> ou <kbd>Cmd</kbd>._
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Ação | Atalho
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| Mudar modo de tela cheia | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| Rotacionar display para esquerda | <kbd>MOD</kbd>+<kbd>←</kbd> _(esquerda)_
|
||||
| Rotacionar display para direita | <kbd>MOD</kbd>+<kbd>→</kbd> _(direita)_
|
||||
| Redimensionar janela para 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Redimensionar janela para remover bordas pretas | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Clique-duplo¹_
|
||||
| Clicar em `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Clique-do-meio_
|
||||
| Clicar em `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Clique-direito²_
|
||||
| Clicar em `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd>
|
||||
| Clicar em `MENU` (desbloquear tela | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| Clicar em `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(cima)_
|
||||
| Clicar em `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(baixo)_
|
||||
| Clicar em `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| Ligar | _Clique-direito²_
|
||||
| Desligar tela do dispositivo (continuar espelhando) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| Ligar tela do dispositivo | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| Rotacionar tela do dispositivo | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| Expandir painel de notificação | <kbd>MOD</kbd>+<kbd>n</kbd>
|
||||
| Colapsar painel de notificação | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Copiar para área de transferência³ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Recortar para área de transferência³ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Sincronizar áreas de transferência e colar³ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Injetar texto da área de transferência do computador | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Ativar/desativar contador de FPS (em stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pinçar para dar zoom | <kbd>Ctrl</kbd>+_clicar-e-mover_
|
||||
|
||||
_¹Clique-duplo em bordas pretas para removê-las._
|
||||
_²Clique-direito liga a tela se ela estiver desligada, pressiona BACK caso contrário._
|
||||
_³Apenas em Android >= 7._
|
||||
|
||||
Todos os atalhos <kbd>Ctrl</kbd>+_tecla_ são encaminhados para o dispositivo, para que eles sejam
|
||||
tratados pela aplicação ativa.
|
||||
|
||||
|
||||
## Caminhos personalizados
|
||||
|
||||
Para usar um binário _adb_ específico, 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 tão impronunciável quanto [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 o [FAQ](FAQ.md).
|
||||
|
||||
|
||||
## Desenvolvedores
|
||||
|
||||
Leia a [página dos desenvolvedores][developers page].
|
||||
|
||||
[developers page]: DEVELOP.md
|
||||
|
||||
|
||||
## Licença
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 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/
|
@ -1,732 +0,0 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
只有原版的[README](README.md)会保持最新。
|
||||
|
||||
本文根据[ed130e05]进行翻译。
|
||||
|
||||
[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md
|
||||
|
||||
# scrcpy (v1.17)
|
||||
|
||||
本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。
|
||||
|
||||

|
||||
|
||||
它专注于:
|
||||
|
||||
- **轻量** (原生,仅显示设备屏幕)
|
||||
- **性能** (30~60fps)
|
||||
- **质量** (分辨率可达 1920×1080 或更高)
|
||||
- **低延迟** ([35~70ms][lowlatency])
|
||||
- **快速启动** (最快 1 秒内即可显示第一帧)
|
||||
- **无侵入性** (不会在设备上遗留任何程序)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## 系统要求
|
||||
|
||||
安卓设备最低需要支持 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
|
||||
|
||||
|
||||
## 获取本程序
|
||||
|
||||
<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
|
||||
|
||||
在 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
|
||||
|
||||
您也可以[自行构建][BUILD] (不必担心,这并不困难)。
|
||||
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
也可以使用 [Chocolatey]:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # 如果还没有 adb
|
||||
```
|
||||
|
||||
或者 [Scoop]:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # 如果还没有 adb
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
您也可以[自行构建][BUILD]。
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
本程序已发布到 [Homebrew]。直接安装即可:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
你还需要在 `PATH` 内有 `adb`。如果还没有:
|
||||
|
||||
```bash
|
||||
# Homebrew >= 2.6.0
|
||||
brew install --cask android-platform-tools
|
||||
|
||||
# Homebrew < 2.6.0
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
您也可以[自行构建][BUILD]。
|
||||
|
||||
|
||||
## 运行
|
||||
|
||||
连接安卓设备,然后执行:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
本程序支持命令行参数,查看参数列表:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 捕获设置
|
||||
|
||||
#### 降低分辨率
|
||||
|
||||
有时候,可以通过降低镜像的分辨率来提高性能。
|
||||
|
||||
要同时限制宽度和高度到某个值 (例如 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # 简写
|
||||
```
|
||||
|
||||
另一边会被按比例缩小以保持设备的显示比例。这样,1920×1080 分辨率的设备会以 1024×576 的分辨率进行镜像。
|
||||
|
||||
|
||||
#### 修改码率
|
||||
|
||||
默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # 简写
|
||||
```
|
||||
|
||||
#### 限制帧率
|
||||
|
||||
要限制捕获的帧率:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
本功能从 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 --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
要列出可用的编码器,可以指定一个不存在的编码器名称,错误信息中会包含所有的编码器:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### 屏幕录制
|
||||
|
||||
可以在镜像的同时录制视频:
|
||||
|
||||
```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] 不会影响录制的文件。
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
### 连接
|
||||
|
||||
#### 无线
|
||||
|
||||
_Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接]到设备:
|
||||
|
||||
1. 将设备和电脑连接至同一 Wi-Fi。
|
||||
2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令:
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. 启用设备的网络 adb 功能 `adb tcpip 5555`。
|
||||
4. 断开设备的 USB 连接。
|
||||
5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_.
|
||||
6. 正常运行 `scrcpy`。
|
||||
|
||||
可能需要降低码率和分辨率:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # 简写
|
||||
```
|
||||
|
||||
[连接]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### 多设备
|
||||
|
||||
如果 `adb devices` 列出了多个设备,您必须指定设备的 _序列号_ :
|
||||
|
||||
```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 隧道
|
||||
|
||||
要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同):
|
||||
|
||||
```bash
|
||||
adb kill-server # 关闭本地 5037 端口上的 adb 服务端
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# 保持该窗口开启
|
||||
```
|
||||
|
||||
在另一个终端:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
若要不使用远程端口转发,可以强制使用正向连接 (注意 `-L` 和 `-R` 的区别):
|
||||
|
||||
```bash
|
||||
adb kill-server # 关闭本地 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
|
||||
```
|
||||
|
||||
#### 保持窗口在最前
|
||||
|
||||
您可以通过如下命令保持窗口在最前面:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### 全屏
|
||||
|
||||
您可以通过如下命令直接全屏启动scrcpy:
|
||||
|
||||
```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_ 有三个不同的方向:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。
|
||||
- [`--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 或更高版本 (否则将在只读状态下镜像)。
|
||||
|
||||
|
||||
#### 保持常亮
|
||||
|
||||
阻止设备在连接时休眠:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
程序关闭时会恢复设备原来的设置。
|
||||
|
||||
|
||||
#### 关闭设备屏幕
|
||||
|
||||
可以通过以下的命令行参数在关闭设备屏幕的状态下进行镜像:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
或者在任何时候按 <kbd>MOD</kbd>+<kbd>o</kbd>。
|
||||
|
||||
要重新打开屏幕,按下 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
|
||||
在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 <kbd>MOD</kbd>+<kbd>p</kbd>),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。
|
||||
|
||||
还可以同时阻止设备休眠:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
|
||||
#### 渲染过期帧
|
||||
|
||||
默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。
|
||||
|
||||
强制渲染所有帧 (可能导致延迟变高):
|
||||
|
||||
```bash
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
#### 显示触摸
|
||||
|
||||
在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。
|
||||
|
||||
Android 在 _开发者选项_ 中提供了这项功能。
|
||||
|
||||
_Scrcpy_ 提供一个选项可以在启动时开启这项功能并在退出时恢复初始设置:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
请注意这项功能只能显示 _物理_ 触摸 (用手指在屏幕上的触摸)。
|
||||
|
||||
|
||||
#### 关闭屏保
|
||||
|
||||
_Scrcpy_ 默认不会阻止电脑上开启的屏幕保护。
|
||||
|
||||
关闭屏幕保护:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### 输入控制
|
||||
|
||||
#### 旋转设备屏幕
|
||||
|
||||
使用 <kbd>MOD</kbd>+<kbd>r</kbd> 在竖屏和横屏模式之间切换。
|
||||
|
||||
需要注意的是,只有在前台应用程序支持所要求的模式时,才会进行切换。
|
||||
|
||||
#### 复制粘贴
|
||||
|
||||
每次安卓的剪贴板变化时,其内容都会被自动同步到电脑的剪贴板上。
|
||||
|
||||
所有的 <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> 注入 `COPY` (复制)
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> 注入 `CUT` (剪切)
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> 注入 `PASTE` (粘贴) (在电脑到设备的剪贴板同步完成之后)
|
||||
|
||||
另外,<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> 会将电脑的剪贴板内容转换为一串按键事件输入到设备。在应用程序不接受粘贴时 (比如 _Termux_),这项功能可以派上一定的用场。不过这项功能可能会导致非 ASCII 编码的内容出现错误。
|
||||
|
||||
**警告:** 将电脑剪贴板的内容粘贴至设备 (无论是通过 <kbd>Ctrl</kbd>+<kbd>v</kbd> 还是 <kbd>MOD</kbd>+<kbd>v</kbd>) 都会将内容复制到设备的剪贴板。如此,任何安卓应用程序都能读取到。您应避免将敏感内容 (如密码) 通过这种方式粘贴。
|
||||
|
||||
一些设备不支持通过程序设置剪贴板。通过 `--legacy-paste` 选项可以修改 <kbd>Ctrl</kbd>+<kbd>v</kbd> 和 <kbd>MOD</kbd>+<kbd>v</kbd> 的工作方式,使它们通过按键事件 (同 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>) 来注入电脑剪贴板内容。
|
||||
|
||||
#### 双指缩放
|
||||
|
||||
模拟“双指缩放”:<kbd>Ctrl</kbd>+_按住并移动鼠标_。
|
||||
|
||||
更准确的说,在按住鼠标左键时按住 <kbd>Ctrl</kbd>。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。
|
||||
|
||||
实际上,_scrcpy_ 会在以屏幕中心对称的位置上生成由“虚拟手指”发出的额外触摸事件。
|
||||
|
||||
|
||||
#### 文字注入偏好
|
||||
|
||||
打字的时候,系统会产生两种[事件][textevents]:
|
||||
- _按键事件_ ,代表一个按键被按下或松开。
|
||||
- _文本事件_ ,代表一个字符被输入。
|
||||
|
||||
程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作 (例如 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
|
||||
```
|
||||
|
||||
|
||||
#### 右键和中键
|
||||
|
||||
默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### 文件拖放
|
||||
|
||||
#### 安装APK
|
||||
|
||||
将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。
|
||||
|
||||
该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。
|
||||
|
||||
|
||||
#### 将文件推送至设备
|
||||
|
||||
要推送文件到设备的 `/sdcard/`,将 (非 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`、`rctrl`、`lalt`、`ralt`、`lsuper` 和 `rsuper`。例如:
|
||||
|
||||
```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)
|
||||
|
||||
| 操作 | 快捷键 |
|
||||
| --------------------------------- | :------------------------------------------- |
|
||||
| 全屏 | <kbd>MOD</kbd>+<kbd>f</kbd> |
|
||||
| 向左旋转屏幕 | <kbd>MOD</kbd>+<kbd>←</kbd> _(左箭头)_ |
|
||||
| 向右旋转屏幕 | <kbd>MOD</kbd>+<kbd>→</kbd> _(右箭头)_ |
|
||||
| 将窗口大小重置为1:1 (匹配像素) | <kbd>MOD</kbd>+<kbd>g</kbd> |
|
||||
| 将窗口大小重置为消除黑边 | <kbd>MOD</kbd>+<kbd>w</kbd> \| _双击¹_ |
|
||||
| 点按 `主屏幕` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _鼠标中键_ |
|
||||
| 点按 `返回` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _鼠标右键²_ |
|
||||
| 点按 `切换应用` | <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> |
|
||||
| 捏拉缩放 | <kbd>Ctrl</kbd>+_按住并移动鼠标_ |
|
||||
|
||||
_¹双击黑边可以去除黑边_
|
||||
_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
|
||||
_³需要安卓版本 Android >= 7。_
|
||||
|
||||
所有的 <kbd>Ctrl</kbd>+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。
|
||||
|
||||
|
||||
## 自定义路径
|
||||
|
||||
要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。
|
||||
|
||||
[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.md)。
|
||||
|
||||
|
||||
## 开发者
|
||||
|
||||
请查看[开发者页面]。
|
||||
|
||||
[开发者页面]: DEVELOP.md
|
||||
|
||||
|
||||
## 许可协议
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2021 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.
|
||||
|
||||
## 相关文章
|
||||
|
||||
- [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/
|
@ -1,702 +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_。
|
||||
|
||||

|
||||
|
||||
特色:
|
||||
|
||||
- **輕量** (只顯示裝置螢幕)
|
||||
- **效能** (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`) 的壓縮包:
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
[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-2021 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/
|
@ -119,6 +119,9 @@ conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||
# enable High DPI support
|
||||
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
|
||||
|
||||
# disable console on Windows
|
||||
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
|
||||
|
||||
# run a server debugger and wait for a client to be attached
|
||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||
|
||||
@ -129,11 +132,18 @@ configure_file(configuration: conf, output: 'config.h')
|
||||
|
||||
src_dir = include_directories('src')
|
||||
|
||||
if get_option('windows_noconsole')
|
||||
link_args = [ '-Wl,--subsystem,windows' ]
|
||||
else
|
||||
link_args = []
|
||||
endif
|
||||
|
||||
executable('scrcpy', src,
|
||||
dependencies: dependencies,
|
||||
include_directories: src_dir,
|
||||
install: true,
|
||||
c_args: [])
|
||||
c_args: [],
|
||||
link_args: link_args)
|
||||
|
||||
install_man('scrcpy.1')
|
||||
|
||||
@ -154,12 +164,12 @@ if get_option('buildtype') == 'debug'
|
||||
'src/cli.c',
|
||||
'src/util/str_util.c',
|
||||
]],
|
||||
['test_control_msg_serialize', [
|
||||
['test_control_event_serialize', [
|
||||
'tests/test_control_msg_serialize.c',
|
||||
'src/control_msg.c',
|
||||
'src/util/str_util.c',
|
||||
]],
|
||||
['test_device_msg_deserialize', [
|
||||
['test_device_event_deserialize', [
|
||||
'tests/test_device_msg_deserialize.c',
|
||||
'src/device_msg.c',
|
||||
]],
|
||||
@ -176,7 +186,7 @@ if get_option('buildtype') == 'debug'
|
||||
exe = executable(t[0], t[1],
|
||||
include_directories: src_dir,
|
||||
dependencies: dependencies,
|
||||
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
||||
c_args: ['-DSDL_MAIN_HANDLED'])
|
||||
test(t[0], exe)
|
||||
endforeach
|
||||
endif
|
||||
|
126
app/scrcpy.1
126
app/scrcpy.1
@ -25,16 +25,6 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
||||
|
||||
Default is 8000000.
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec\-options " key[:type]=value[,...]
|
||||
Set a list of comma-separated key:type=value options for the device encoder.
|
||||
|
||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||
|
||||
The list of possible codec options is available in the Android documentation
|
||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||
.UE .
|
||||
|
||||
.TP
|
||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||
Crop the device screen on the server.
|
||||
@ -43,10 +33,6 @@ The values are expressed in the device natural orientation (typically, portrait
|
||||
.B \-\-max\-size
|
||||
value is computed on the cropped size.
|
||||
|
||||
.TP
|
||||
.BI "\-\-disable-screensaver"
|
||||
Disable screensaver while scrcpy is running.
|
||||
|
||||
.TP
|
||||
.BI "\-\-display " id
|
||||
Specify the display id to mirror.
|
||||
@ -56,18 +42,6 @@ The list of possible display ids can be listed by "adb shell dumpsys display"
|
||||
|
||||
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
|
||||
.B \-f, \-\-fullscreen
|
||||
Start in fullscreen.
|
||||
@ -76,12 +50,6 @@ Start in fullscreen.
|
||||
.B \-h, \-\-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
|
||||
.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.
|
||||
@ -106,10 +74,6 @@ Disable device control (mirror the device in read\-only).
|
||||
.B \-N, \-\-no\-display
|
||||
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.
|
||||
@ -151,7 +115,6 @@ Force recording format (either mp4 or mkv).
|
||||
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
|
||||
|
||||
@ -167,23 +130,13 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
|
||||
.BI "\-s, \-\-serial " number
|
||||
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
|
||||
.B \-S, \-\-turn\-screen\-off
|
||||
Turn the device screen off immediately.
|
||||
|
||||
.TP
|
||||
.B \-t, \-\-show\-touches
|
||||
Enable "show touches" on start, restore the initial value on exit.
|
||||
Enable "show touches" on start, restore the initial value on exit..
|
||||
|
||||
It only shows physical touches (not clicks from scrcpy).
|
||||
|
||||
@ -191,15 +144,9 @@ It only shows physical touches (not clicks from scrcpy).
|
||||
.B \-v, \-\-version
|
||||
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.
|
||||
Keep the device on while scrcpy is running.
|
||||
|
||||
.TP
|
||||
.B \-\-window\-borderless
|
||||
@ -213,77 +160,74 @@ Set a custom window title.
|
||||
.BI "\-\-window\-x " value
|
||||
Set the initial window horizontal position.
|
||||
|
||||
Default is "auto".
|
||||
Default is "auto".\n
|
||||
|
||||
.TP
|
||||
.BI "\-\-window\-y " value
|
||||
Set the initial window vertical position.
|
||||
|
||||
Default is "auto".
|
||||
Default is "auto".\n
|
||||
|
||||
.TP
|
||||
.BI "\-\-window\-width " value
|
||||
Set the initial window width.
|
||||
|
||||
Default is 0 (automatic).
|
||||
Default is 0 (automatic).\n
|
||||
|
||||
.TP
|
||||
.BI "\-\-window\-height " value
|
||||
Set the initial window height.
|
||||
|
||||
Default is 0 (automatic).
|
||||
Default is 0 (automatic).\n
|
||||
|
||||
.SH SHORTCUTS
|
||||
|
||||
In the following list, MOD is the shortcut modifier. By default, it's (left)
|
||||
Alt or (left) Super, but it can be configured by \-\-shortcut-mod (see above).
|
||||
|
||||
.TP
|
||||
.B MOD+f
|
||||
.B Ctrl+f
|
||||
Switch fullscreen mode
|
||||
|
||||
.TP
|
||||
.B MOD+Left
|
||||
.B Ctrl+Left
|
||||
Rotate display left
|
||||
|
||||
.TP
|
||||
.B MOD+Right
|
||||
.B Ctrl+Right
|
||||
Rotate display right
|
||||
|
||||
.TP
|
||||
.B MOD+g
|
||||
.B Ctrl+g
|
||||
Resize window to 1:1 (pixel\-perfect)
|
||||
|
||||
.TP
|
||||
.B MOD+w, Double\-click on black borders
|
||||
.B Ctrl+x, Double\-click on black borders
|
||||
Resize window to remove black borders
|
||||
|
||||
.TP
|
||||
.B MOD+h, Home, Middle\-click
|
||||
.B Ctrl+h, Home, Middle\-click
|
||||
Click on HOME
|
||||
|
||||
.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
|
||||
|
||||
.TP
|
||||
.B MOD+s
|
||||
.B Ctrl+s
|
||||
Click on APP_SWITCH
|
||||
|
||||
.TP
|
||||
.B MOD+m
|
||||
.B Ctrl+m
|
||||
Click on MENU
|
||||
|
||||
.TP
|
||||
.B MOD+Up
|
||||
.B Ctrl+Up
|
||||
Click on VOLUME_UP
|
||||
|
||||
.TP
|
||||
.B MOD+Down
|
||||
.B Ctrl+Down
|
||||
Click on VOLUME_DOWN
|
||||
|
||||
.TP
|
||||
.B MOD+p
|
||||
.B Ctrl+p
|
||||
Click on POWER (turn screen on/off)
|
||||
|
||||
.TP
|
||||
@ -291,49 +235,37 @@ Click on POWER (turn screen on/off)
|
||||
Turn screen on
|
||||
|
||||
.TP
|
||||
.B MOD+o
|
||||
.B Ctrl+o
|
||||
Turn device screen off (keep mirroring)
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+o
|
||||
Turn device screen on
|
||||
|
||||
.TP
|
||||
.B MOD+r
|
||||
.B Ctrl+r
|
||||
Rotate device screen
|
||||
|
||||
.TP
|
||||
.B MOD+n
|
||||
.B Ctrl+n
|
||||
Expand notification panel
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+n
|
||||
.B Ctrl+Shift+n
|
||||
Collapse notification panel
|
||||
|
||||
.TP
|
||||
.B Mod+c
|
||||
Copy to clipboard (inject COPY keycode, Android >= 7 only)
|
||||
.B Ctrl+c
|
||||
Copy device clipboard to computer
|
||||
|
||||
.TP
|
||||
.B Mod+x
|
||||
Cut to clipboard (inject CUT keycode, Android >= 7 only)
|
||||
.B Ctrl+v
|
||||
Paste computer clipboard to device
|
||||
|
||||
.TP
|
||||
.B MOD+v
|
||||
Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only)
|
||||
.B Ctrl+Shift+v
|
||||
Copy computer clipboard to device
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+v
|
||||
Inject computer clipboard text as a sequence of key events
|
||||
|
||||
.TP
|
||||
.B MOD+i
|
||||
.B Ctrl+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
|
||||
.B Drag & drop APK file
|
||||
Install APK from computer
|
||||
|
323
app/src/cli.c
323
app/src/cli.c
@ -3,16 +3,20 @@
|
||||
#include <assert.h>
|
||||
#include <getopt.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "scrcpy.h"
|
||||
#include "recorder.h"
|
||||
#include "util/log.h"
|
||||
#include "util/str_util.h"
|
||||
|
||||
void
|
||||
scrcpy_print_usage(const char *arg0) {
|
||||
#ifdef __APPLE__
|
||||
# define CTRL_OR_CMD "Cmd"
|
||||
#else
|
||||
# define CTRL_OR_CMD "Ctrl"
|
||||
#endif
|
||||
fprintf(stderr,
|
||||
"Usage: %s [options]\n"
|
||||
"\n"
|
||||
@ -26,24 +30,12 @@ scrcpy_print_usage(const char *arg0) {
|
||||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
" Default is %d.\n"
|
||||
"\n"
|
||||
" --codec-options key[:type]=value[,...]\n"
|
||||
" Set a list of comma-separated key:type=value options for the\n"
|
||||
" device encoder.\n"
|
||||
" The possible values for 'type' are 'int' (default), 'long',\n"
|
||||
" 'float' and 'string'.\n"
|
||||
" The list of possible codec options is available in the\n"
|
||||
" Android documentation:\n"
|
||||
" <https://d.android.com/reference/android/media/MediaFormat>\n"
|
||||
"\n"
|
||||
" --crop width:height:x:y\n"
|
||||
" Crop the device screen on the server.\n"
|
||||
" The values are expressed in the device natural orientation\n"
|
||||
" (typically, portrait for a phone, landscape for a tablet).\n"
|
||||
" Any --max-size value is computed on the cropped size.\n"
|
||||
"\n"
|
||||
" --disable-screensaver\n"
|
||||
" Disable screensaver while scrcpy is running.\n"
|
||||
"\n"
|
||||
" --display id\n"
|
||||
" Specify the display id to mirror.\n"
|
||||
"\n"
|
||||
@ -53,30 +45,12 @@ scrcpy_print_usage(const char *arg0) {
|
||||
"\n"
|
||||
" Default is 0.\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"
|
||||
" Start in fullscreen.\n"
|
||||
"\n"
|
||||
" -h, --help\n"
|
||||
" Print this help.\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 to value.\n"
|
||||
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n"
|
||||
@ -101,9 +75,6 @@ scrcpy_print_usage(const char *arg0) {
|
||||
" Do not display device (only when screen recording is\n"
|
||||
" enabled).\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"
|
||||
@ -155,19 +126,6 @@ scrcpy_print_usage(const char *arg0) {
|
||||
" The device serial number. Mandatory only if several devices\n"
|
||||
" are connected to adb.\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"
|
||||
" Turn the device screen off immediately.\n"
|
||||
"\n"
|
||||
@ -178,18 +136,9 @@ scrcpy_print_usage(const char *arg0) {
|
||||
"\n"
|
||||
" -v, --version\n"
|
||||
" Print the version of scrcpy.\n"
|
||||
"\n"
|
||||
" -V, --verbosity value\n"
|
||||
" Set the log level (debug, info, warn or error).\n"
|
||||
#ifndef NDEBUG
|
||||
" Default is debug.\n"
|
||||
#else
|
||||
" Default is info.\n"
|
||||
#endif
|
||||
"\n"
|
||||
" -w, --stay-awake\n"
|
||||
" Keep the device on while scrcpy is running, when the device\n"
|
||||
" is plugged in.\n"
|
||||
" Keep the device on while scrcpy is running.\n"
|
||||
"\n"
|
||||
" --window-borderless\n"
|
||||
" Disable window decorations (display borderless window).\n"
|
||||
@ -215,87 +164,73 @@ scrcpy_print_usage(const char *arg0) {
|
||||
"\n"
|
||||
"Shortcuts:\n"
|
||||
"\n"
|
||||
" In the following list, MOD is the shortcut modifier. By default,\n"
|
||||
" it's (left) Alt or (left) Super, but it can be configured by\n"
|
||||
" --shortcut-mod (see above).\n"
|
||||
"\n"
|
||||
" MOD+f\n"
|
||||
" " CTRL_OR_CMD "+f\n"
|
||||
" Switch fullscreen mode\n"
|
||||
"\n"
|
||||
" MOD+Left\n"
|
||||
" " CTRL_OR_CMD "+Left\n"
|
||||
" Rotate display left\n"
|
||||
"\n"
|
||||
" MOD+Right\n"
|
||||
" " CTRL_OR_CMD "+Right\n"
|
||||
" Rotate display right\n"
|
||||
"\n"
|
||||
" MOD+g\n"
|
||||
" " CTRL_OR_CMD "+g\n"
|
||||
" Resize window to 1:1 (pixel-perfect)\n"
|
||||
"\n"
|
||||
" MOD+w\n"
|
||||
" " CTRL_OR_CMD "+x\n"
|
||||
" Double-click on black borders\n"
|
||||
" Resize window to remove black borders\n"
|
||||
"\n"
|
||||
" MOD+h\n"
|
||||
" Ctrl+h\n"
|
||||
" Middle-click\n"
|
||||
" Click on HOME\n"
|
||||
"\n"
|
||||
" MOD+b\n"
|
||||
" MOD+Backspace\n"
|
||||
" " CTRL_OR_CMD "+b\n"
|
||||
" " CTRL_OR_CMD "+Backspace\n"
|
||||
" Right-click (when screen is on)\n"
|
||||
" Click on BACK\n"
|
||||
"\n"
|
||||
" MOD+s\n"
|
||||
" " CTRL_OR_CMD "+s\n"
|
||||
" Click on APP_SWITCH\n"
|
||||
"\n"
|
||||
" MOD+m\n"
|
||||
" Ctrl+m\n"
|
||||
" Click on MENU\n"
|
||||
"\n"
|
||||
" MOD+Up\n"
|
||||
" " CTRL_OR_CMD "+Up\n"
|
||||
" Click on VOLUME_UP\n"
|
||||
"\n"
|
||||
" MOD+Down\n"
|
||||
" " CTRL_OR_CMD "+Down\n"
|
||||
" Click on VOLUME_DOWN\n"
|
||||
"\n"
|
||||
" MOD+p\n"
|
||||
" " CTRL_OR_CMD "+p\n"
|
||||
" Click on POWER (turn screen on/off)\n"
|
||||
"\n"
|
||||
" Right-click (when screen is off)\n"
|
||||
" Power on\n"
|
||||
"\n"
|
||||
" MOD+o\n"
|
||||
" " CTRL_OR_CMD "+o\n"
|
||||
" Turn device screen off (keep mirroring)\n"
|
||||
"\n"
|
||||
" MOD+Shift+o\n"
|
||||
" Turn device screen on\n"
|
||||
"\n"
|
||||
" MOD+r\n"
|
||||
" " CTRL_OR_CMD "+r\n"
|
||||
" Rotate device screen\n"
|
||||
"\n"
|
||||
" MOD+n\n"
|
||||
" " CTRL_OR_CMD "+n\n"
|
||||
" Expand notification panel\n"
|
||||
"\n"
|
||||
" MOD+Shift+n\n"
|
||||
" " CTRL_OR_CMD "+Shift+n\n"
|
||||
" Collapse notification panel\n"
|
||||
"\n"
|
||||
" MOD+c\n"
|
||||
" Copy to clipboard (inject COPY keycode, Android >= 7 only)\n"
|
||||
" " CTRL_OR_CMD "+c\n"
|
||||
" Copy device clipboard to computer\n"
|
||||
"\n"
|
||||
" MOD+x\n"
|
||||
" Cut to clipboard (inject CUT keycode, Android >= 7 only)\n"
|
||||
" " CTRL_OR_CMD "+v\n"
|
||||
" Paste computer clipboard to device\n"
|
||||
"\n"
|
||||
" MOD+v\n"
|
||||
" Copy computer clipboard to device, then paste (inject PASTE\n"
|
||||
" keycode, Android >= 7 only)\n"
|
||||
" " CTRL_OR_CMD "+Shift+v\n"
|
||||
" Copy computer clipboard to device\n"
|
||||
"\n"
|
||||
" MOD+Shift+v\n"
|
||||
" Inject computer clipboard text as a sequence of key events\n"
|
||||
"\n"
|
||||
" MOD+i\n"
|
||||
" " CTRL_OR_CMD "+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"
|
||||
" Drag & drop APK file\n"
|
||||
" Install APK from computer\n"
|
||||
"\n",
|
||||
@ -418,10 +353,10 @@ parse_rotation(const char *s, uint8_t *rotation) {
|
||||
static bool
|
||||
parse_window_position(const char *s, int16_t *position) {
|
||||
// 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")) {
|
||||
*position = SC_WINDOW_POSITION_UNDEFINED;
|
||||
*position = WINDOW_POSITION_UNDEFINED;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -450,7 +385,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) {
|
||||
}
|
||||
|
||||
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];
|
||||
size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port");
|
||||
if (!count) {
|
||||
@ -490,143 +425,20 @@ parse_display_id(const char *s, uint16_t *display_id) {
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_log_level(const char *s, enum sc_log_level *log_level) {
|
||||
if (!strcmp(s, "debug")) {
|
||||
*log_level = SC_LOG_LEVEL_DEBUG;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "info")) {
|
||||
*log_level = SC_LOG_LEVEL_INFO;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "warn")) {
|
||||
*log_level = SC_LOG_LEVEL_WARN;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!strcmp(s, "error")) {
|
||||
*log_level = SC_LOG_LEVEL_ERROR;
|
||||
return true;
|
||||
}
|
||||
|
||||
LOGE("Could not parse log level: %s", s);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
parse_record_format(const char *optarg, enum recorder_format *format) {
|
||||
if (!strcmp(optarg, "mp4")) {
|
||||
*format = SC_RECORD_FORMAT_MP4;
|
||||
*format = RECORDER_FORMAT_MP4;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "mkv")) {
|
||||
*format = SC_RECORD_FORMAT_MKV;
|
||||
*format = RECORDER_FORMAT_MKV;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static enum sc_record_format
|
||||
static enum recorder_format
|
||||
guess_record_format(const char *filename) {
|
||||
size_t len = strlen(filename);
|
||||
if (len < 4) {
|
||||
@ -634,10 +446,10 @@ guess_record_format(const char *filename) {
|
||||
}
|
||||
const char *ext = &filename[len - 4];
|
||||
if (!strcmp(ext, ".mp4")) {
|
||||
return SC_RECORD_FORMAT_MP4;
|
||||
return RECORDER_FORMAT_MP4;
|
||||
}
|
||||
if (!strcmp(ext, ".mkv")) {
|
||||
return SC_RECORD_FORMAT_MKV;
|
||||
return RECORDER_FORMAT_MKV;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -660,40 +472,22 @@ guess_record_format(const char *filename) {
|
||||
#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
|
||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
static const struct option long_options[] = {
|
||||
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
||||
{"bit-rate", required_argument, NULL, 'b'},
|
||||
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
|
||||
{"crop", required_argument, NULL, OPT_CROP},
|
||||
{"disable-screensaver", no_argument, NULL,
|
||||
OPT_DISABLE_SCREENSAVER},
|
||||
{"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'},
|
||||
{"help", no_argument, NULL, 'h'},
|
||||
{"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
|
||||
{"lock-video-orientation", required_argument, NULL,
|
||||
OPT_LOCK_VIDEO_ORIENTATION},
|
||||
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
||||
{"max-size", required_argument, NULL, 'm'},
|
||||
{"no-control", no_argument, NULL, 'n'},
|
||||
{"no-display", no_argument, NULL, 'N'},
|
||||
{"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT},
|
||||
{"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
|
||||
{"port", required_argument, NULL, 'p'},
|
||||
{"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
|
||||
@ -705,11 +499,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
OPT_RENDER_EXPIRED_FRAMES},
|
||||
{"rotation", required_argument, NULL, OPT_ROTATION},
|
||||
{"serial", required_argument, NULL, 's'},
|
||||
{"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD},
|
||||
{"show-touches", no_argument, NULL, 't'},
|
||||
{"stay-awake", no_argument, NULL, 'w'},
|
||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||
{"verbosity", required_argument, NULL, 'V'},
|
||||
{"version", no_argument, NULL, 'v'},
|
||||
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
||||
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
||||
@ -726,8 +518,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
optind = 0; // reset to start from the first argument in tests
|
||||
|
||||
int c;
|
||||
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
|
||||
long_options, NULL)) != -1) {
|
||||
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options,
|
||||
NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'b':
|
||||
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
||||
@ -806,11 +598,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
case 'v':
|
||||
args->version = true;
|
||||
break;
|
||||
case 'V':
|
||||
if (!parse_log_level(optarg, &opts->log_level)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'w':
|
||||
opts->stay_awake = true;
|
||||
break;
|
||||
@ -860,32 +647,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||
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:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
|
@ -18,9 +18,4 @@ scrcpy_print_usage(const char *arg0);
|
||||
bool
|
||||
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
|
||||
|
@ -12,7 +12,11 @@
|
||||
# define PATH_SEPARATOR '\\'
|
||||
# define PRIexitcode "lu"
|
||||
// <https://stackoverflow.com/a/44383330/1987178>
|
||||
# define PRIsizet "Iu"
|
||||
# ifdef _WIN64
|
||||
# define PRIsizet PRIu64
|
||||
# else
|
||||
# define PRIsizet PRIu32
|
||||
# endif
|
||||
# define PROCESS_NONE NULL
|
||||
# define NO_EXIT_CODE -1u // max value as unsigned
|
||||
typedef HANDLE process_t;
|
||||
|
@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) {
|
||||
static size_t
|
||||
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
|
||||
size_t len = utf8_truncation_index(utf8, max_len);
|
||||
buffer_write32be(buf, len);
|
||||
memcpy(&buf[4], utf8, len);
|
||||
return 4 + len;
|
||||
buffer_write16be(buf, (uint16_t) len);
|
||||
memcpy(&buf[2], utf8, len);
|
||||
return 2 + len;
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
@ -42,9 +42,8 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
||||
case CONTROL_MSG_TYPE_INJECT_KEYCODE:
|
||||
buf[1] = msg->inject_keycode.action;
|
||||
buffer_write32be(&buf[2], msg->inject_keycode.keycode);
|
||||
buffer_write32be(&buf[6], msg->inject_keycode.repeat);
|
||||
buffer_write32be(&buf[10], msg->inject_keycode.metastate);
|
||||
return 14;
|
||||
buffer_write32be(&buf[6], msg->inject_keycode.metastate);
|
||||
return 10;
|
||||
case CONTROL_MSG_TYPE_INJECT_TEXT: {
|
||||
size_t len =
|
||||
write_string(msg->inject_text.text,
|
||||
@ -68,11 +67,10 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
||||
(uint32_t) msg->inject_scroll_event.vscroll);
|
||||
return 21;
|
||||
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
|
||||
buf[1] = !!msg->set_clipboard.paste;
|
||||
size_t len = write_string(msg->set_clipboard.text,
|
||||
size_t len = write_string(msg->inject_text.text,
|
||||
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
|
||||
&buf[2]);
|
||||
return 2 + len;
|
||||
&buf[1]);
|
||||
return 1 + len;
|
||||
}
|
||||
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||
buf[1] = msg->set_screen_power_mode.mode;
|
||||
|
@ -10,14 +10,12 @@
|
||||
#include "android/keycodes.h"
|
||||
#include "common.h"
|
||||
|
||||
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
|
||||
|
||||
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
|
||||
// type: 1 byte; paste flag: 1 byte; length: 4 bytes
|
||||
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
|
||||
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
|
||||
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \
|
||||
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
|
||||
|
||||
#define POINTER_ID_MOUSE UINT64_C(-1);
|
||||
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2);
|
||||
|
||||
enum control_msg_type {
|
||||
CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
||||
@ -45,7 +43,6 @@ struct control_msg {
|
||||
struct {
|
||||
enum android_keyevent_action action;
|
||||
enum android_keycode keycode;
|
||||
uint32_t repeat;
|
||||
enum android_metastate metastate;
|
||||
} inject_keycode;
|
||||
struct {
|
||||
@ -65,7 +62,6 @@ struct control_msg {
|
||||
} inject_scroll_event;
|
||||
struct {
|
||||
char *text; // owned, to be freed by SDL_free()
|
||||
bool paste;
|
||||
} set_clipboard;
|
||||
struct {
|
||||
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
|
||||
size_t
|
||||
control_msg_serialize(const struct control_msg *msg, unsigned char *buf);
|
||||
|
@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller,
|
||||
static bool
|
||||
process_msg(struct controller *controller,
|
||||
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);
|
||||
if (!length) {
|
||||
return false;
|
||||
|
@ -9,7 +9,7 @@
|
||||
ssize_t
|
||||
device_msg_deserialize(const unsigned char *buf, size_t len,
|
||||
struct device_msg *msg) {
|
||||
if (len < 5) {
|
||||
if (len < 3) {
|
||||
// at least type + empty string length
|
||||
return 0; // not available
|
||||
}
|
||||
@ -17,8 +17,8 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
|
||||
msg->type = buf[0];
|
||||
switch (msg->type) {
|
||||
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
||||
size_t clipboard_len = buffer_read32be(&buf[1]);
|
||||
if (clipboard_len > len - 5) {
|
||||
uint16_t clipboard_len = buffer_read16be(&buf[1]);
|
||||
if (clipboard_len > len - 3) {
|
||||
return 0; // not available
|
||||
}
|
||||
char *text = SDL_malloc(clipboard_len + 1);
|
||||
@ -27,12 +27,12 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
|
||||
return -1;
|
||||
}
|
||||
if (clipboard_len) {
|
||||
memcpy(text, &buf[5], clipboard_len);
|
||||
memcpy(text, &buf[3], clipboard_len);
|
||||
}
|
||||
text[clipboard_len] = '\0';
|
||||
|
||||
msg->clipboard.text = text;
|
||||
return 5 + clipboard_len;
|
||||
return 3 + clipboard_len;
|
||||
}
|
||||
default:
|
||||
LOGW("Unknown device message type: %d", (int) msg->type);
|
||||
|
@ -7,9 +7,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k
|
||||
// type: 1 byte; length: 4 bytes
|
||||
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
|
||||
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
|
||||
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)
|
||||
|
||||
enum device_msg_type {
|
||||
DEVICE_MSG_TYPE_CLIPBOARD,
|
||||
|
@ -92,10 +92,6 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
||||
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
|
||||
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
|
||||
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))) {
|
||||
@ -115,8 +111,8 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
|
||||
}
|
||||
}
|
||||
|
||||
if (prefer_text && !(mod & KMOD_CTRL)) {
|
||||
// do not forward alpha and space key events (unless Ctrl is pressed)
|
||||
if (prefer_text) {
|
||||
// do not forward alpha and space key events
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1,81 +1,42 @@
|
||||
#include "input_manager.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <SDL2/SDL_keycode.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "event_converter.h"
|
||||
#include "util/lock.h"
|
||||
#include "util/log.h"
|
||||
|
||||
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
||||
// coordinates (as provided in SDL mouse events)
|
||||
//
|
||||
// See my question:
|
||||
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
|
||||
static void
|
||||
convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
|
||||
SDL_Rect viewport;
|
||||
float scale_x, scale_y;
|
||||
SDL_RenderGetViewport(renderer, &viewport);
|
||||
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
|
||||
*x = (int) (*x / scale_x) - viewport.x;
|
||||
*y = (int) (*y / scale_y) - viewport.y;
|
||||
}
|
||||
|
||||
static struct point
|
||||
get_mouse_point(struct screen *screen) {
|
||||
int x;
|
||||
int y;
|
||||
SDL_GetMouseState(&x, &y);
|
||||
convert_to_renderer_coordinates(screen->renderer, &x, &y);
|
||||
return (struct point) {
|
||||
.x = x,
|
||||
.y = y,
|
||||
};
|
||||
}
|
||||
|
||||
static const int ACTION_DOWN = 1;
|
||||
static const int ACTION_UP = 1 << 1;
|
||||
|
||||
#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
|
||||
send_keycode(struct controller *controller, enum android_keycode keycode,
|
||||
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.inject_keycode.keycode = keycode;
|
||||
msg.inject_keycode.metastate = 0;
|
||||
msg.inject_keycode.repeat = 0;
|
||||
|
||||
if (actions & 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");
|
||||
}
|
||||
|
||||
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
|
||||
static void
|
||||
press_back_or_turn_screen_on(struct controller *controller) {
|
||||
@ -179,7 +129,17 @@ collapse_notification_panel(struct controller *controller) {
|
||||
}
|
||||
|
||||
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();
|
||||
if (!text) {
|
||||
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;
|
||||
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
|
||||
msg.set_clipboard.text = text;
|
||||
msg.set_clipboard.paste = paste;
|
||||
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
SDL_free(text);
|
||||
@ -277,10 +236,6 @@ rotate_client_right(struct screen *screen) {
|
||||
void
|
||||
input_manager_process_text_input(struct input_manager *im,
|
||||
const SDL_TextInputEvent *event) {
|
||||
if (is_shortcut_mod(im, SDL_GetModState())) {
|
||||
// A shortcut must never generate text events
|
||||
return;
|
||||
}
|
||||
if (!im->prefer_text) {
|
||||
char c = event->text[0];
|
||||
if (isalpha(c) || c == ' ') {
|
||||
@ -303,39 +258,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
|
||||
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;
|
||||
|
||||
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
||||
@ -348,7 +273,6 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
||||
return false;
|
||||
}
|
||||
|
||||
to->inject_keycode.repeat = repeat;
|
||||
to->inject_keycode.metastate = convert_meta_state(mod);
|
||||
|
||||
return true;
|
||||
@ -356,125 +280,139 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
||||
|
||||
void
|
||||
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
|
||||
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;
|
||||
|
||||
SDL_Keycode keycode = event->keysym.sym;
|
||||
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) {
|
||||
// capture all Ctrl events
|
||||
if (ctrl || cmd) {
|
||||
SDL_Keycode keycode = event->keysym.sym;
|
||||
bool down = event->type == SDL_KEYDOWN;
|
||||
int action = down ? ACTION_DOWN : ACTION_UP;
|
||||
bool repeat = event->repeat;
|
||||
bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT);
|
||||
switch (keycode) {
|
||||
case SDLK_h:
|
||||
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);
|
||||
}
|
||||
return;
|
||||
case SDLK_b: // fall-through
|
||||
case SDLK_BACKSPACE:
|
||||
if (control && !shift && !repeat) {
|
||||
if (control && cmd && !shift && !repeat) {
|
||||
action_back(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_s:
|
||||
if (control && !shift && !repeat) {
|
||||
if (control && cmd && !shift && !repeat) {
|
||||
action_app_switch(controller, action);
|
||||
}
|
||||
return;
|
||||
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);
|
||||
}
|
||||
return;
|
||||
case SDLK_p:
|
||||
if (control && !shift && !repeat) {
|
||||
if (control && cmd && !shift && !repeat) {
|
||||
action_power(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_o:
|
||||
if (control && !repeat && down) {
|
||||
enum screen_power_mode mode = shift
|
||||
? SCREEN_POWER_MODE_NORMAL
|
||||
: SCREEN_POWER_MODE_OFF;
|
||||
set_screen_power_mode(controller, mode);
|
||||
if (control && cmd && !shift && down) {
|
||||
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (control && !shift) {
|
||||
if (control && cmd && !shift) {
|
||||
// forward repeated events
|
||||
action_volume_down(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (control && !shift) {
|
||||
if (control && cmd && !shift) {
|
||||
// forward repeated events
|
||||
action_volume_up(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_LEFT:
|
||||
if (!shift && !repeat && down) {
|
||||
if (cmd && !shift && down) {
|
||||
rotate_client_left(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_RIGHT:
|
||||
if (!shift && !repeat && down) {
|
||||
if (cmd && !shift && down) {
|
||||
rotate_client_right(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_c:
|
||||
if (control && !shift && !repeat) {
|
||||
action_copy(controller, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_x:
|
||||
if (control && !shift && !repeat) {
|
||||
action_cut(controller, action);
|
||||
if (control && cmd && !shift && !repeat && down) {
|
||||
request_device_clipboard(controller);
|
||||
}
|
||||
return;
|
||||
case SDLK_v:
|
||||
if (control && !repeat && down) {
|
||||
if (shift || im->legacy_paste) {
|
||||
if (control && cmd && !repeat && down) {
|
||||
if (shift) {
|
||||
// store the text in the device clipboard
|
||||
set_device_clipboard(controller);
|
||||
} else {
|
||||
// inject the text as input events
|
||||
clipboard_paste(controller);
|
||||
} else {
|
||||
// store the text in the device clipboard and paste
|
||||
set_device_clipboard(controller, true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_f:
|
||||
if (!shift && !repeat && down) {
|
||||
if (!shift && cmd && !repeat && down) {
|
||||
screen_switch_fullscreen(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_w:
|
||||
if (!shift && !repeat && down) {
|
||||
case SDLK_x:
|
||||
if (!shift && cmd && !repeat && down) {
|
||||
screen_resize_to_fit(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_g:
|
||||
if (!shift && !repeat && down) {
|
||||
if (!shift && cmd && !repeat && down) {
|
||||
screen_resize_to_pixel_perfect(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_i:
|
||||
if (!shift && !repeat && down) {
|
||||
if (!shift && cmd && !repeat && down) {
|
||||
struct fps_counter *fps_counter =
|
||||
im->video_buffer->fps_counter;
|
||||
switch_fps_counter_state(fps_counter);
|
||||
}
|
||||
return;
|
||||
case SDLK_n:
|
||||
if (control && !repeat && down) {
|
||||
if (control && cmd && !repeat && down) {
|
||||
if (shift) {
|
||||
collapse_notification_panel(controller);
|
||||
} else {
|
||||
@ -483,7 +421,7 @@ input_manager_process_key(struct input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_r:
|
||||
if (control && !shift && !repeat && down) {
|
||||
if (control && cmd && !shift && !repeat && down) {
|
||||
rotate_device(controller);
|
||||
}
|
||||
return;
|
||||
@ -496,28 +434,8 @@ input_manager_process_key(struct input_manager *im,
|
||||
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;
|
||||
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)) {
|
||||
LOGW("Could not request 'inject keycode'");
|
||||
}
|
||||
@ -532,7 +450,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
|
||||
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
||||
to->inject_touch_event.position.screen_size = screen->frame_size;
|
||||
to->inject_touch_event.position.point =
|
||||
screen_convert_window_to_frame_coords(screen, from->x, from->y);
|
||||
screen_convert_to_frame_coords(screen, from->x, from->y);
|
||||
to->inject_touch_event.pressure = 1.f;
|
||||
to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
|
||||
|
||||
@ -551,18 +469,10 @@ input_manager_process_mouse_motion(struct input_manager *im,
|
||||
return;
|
||||
}
|
||||
struct control_msg msg;
|
||||
if (!convert_mouse_motion(event, im->screen, &msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller_push_msg(im->controller, &msg)) {
|
||||
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);
|
||||
if (convert_mouse_motion(event, im->screen, &msg)) {
|
||||
if (!controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request 'inject mouse motion event'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -577,17 +487,11 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
|
||||
|
||||
to->inject_touch_event.pointer_id = from->fingerId;
|
||||
to->inject_touch_event.position.screen_size = screen->frame_size;
|
||||
|
||||
int dw;
|
||||
int dh;
|
||||
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
|
||||
|
||||
// SDL touch event coordinates are normalized in the range [0; 1]
|
||||
int32_t x = from->x * dw;
|
||||
int32_t y = from->y * dh;
|
||||
float x = from->x * screen->content_size.width;
|
||||
float y = from->y * screen->content_size.height;
|
||||
to->inject_touch_event.position.point =
|
||||
screen_convert_drawable_to_frame_coords(screen, x, y);
|
||||
|
||||
screen_convert_to_frame_coords(screen, x, y);
|
||||
to->inject_touch_event.pressure = from->pressure;
|
||||
to->inject_touch_event.buttons = 0;
|
||||
return true;
|
||||
@ -604,6 +508,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->content_size.width ||
|
||||
y < 0 || y >= im->screen->content_size.height;
|
||||
}
|
||||
|
||||
static bool
|
||||
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
||||
struct control_msg *to) {
|
||||
@ -616,9 +527,8 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
||||
to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
|
||||
to->inject_touch_event.position.screen_size = screen->frame_size;
|
||||
to->inject_touch_event.position.point =
|
||||
screen_convert_window_to_frame_coords(screen, from->x, from->y);
|
||||
to->inject_touch_event.pressure =
|
||||
from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f;
|
||||
screen_convert_to_frame_coords(screen, from->x, from->y);
|
||||
to->inject_touch_event.pressure = 1.f;
|
||||
to->inject_touch_event.buttons =
|
||||
convert_mouse_buttons(SDL_BUTTON(from->button));
|
||||
|
||||
@ -627,16 +537,13 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
||||
|
||||
void
|
||||
input_manager_process_mouse_button(struct input_manager *im,
|
||||
const SDL_MouseButtonEvent *event) {
|
||||
bool control = im->control;
|
||||
|
||||
const SDL_MouseButtonEvent *event,
|
||||
bool control) {
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
}
|
||||
|
||||
bool down = event->type == SDL_MOUSEBUTTONDOWN;
|
||||
if (!im->forward_all_clicks && down) {
|
||||
if (event->type == SDL_MOUSEBUTTONDOWN) {
|
||||
if (control && event->button == SDL_BUTTON_RIGHT) {
|
||||
press_back_or_turn_screen_on(im->controller);
|
||||
return;
|
||||
@ -645,15 +552,10 @@ input_manager_process_mouse_button(struct input_manager *im,
|
||||
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
||||
return;
|
||||
}
|
||||
|
||||
// double-click on black borders resize to fit the device screen
|
||||
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
||||
int32_t x = event->x;
|
||||
int32_t y = event->y;
|
||||
screen_hidpi_scale_coords(im->screen, &x, &y);
|
||||
SDL_Rect *r = &im->screen->rect;
|
||||
bool outside = x < r->x || x >= r->x + r->w
|
||||
|| y < r->y || y >= r->y + r->h;
|
||||
bool outside =
|
||||
is_outside_device_screen(im, event->x, event->y);
|
||||
if (outside) {
|
||||
screen_resize_to_fit(im->screen);
|
||||
return;
|
||||
@ -667,52 +569,19 @@ input_manager_process_mouse_button(struct input_manager *im,
|
||||
}
|
||||
|
||||
struct control_msg msg;
|
||||
if (!convert_mouse_button(event, im->screen, &msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller_push_msg(im->controller, &msg)) {
|
||||
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;
|
||||
if (convert_mouse_button(event, im->screen, &msg)) {
|
||||
if (!controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request 'inject mouse button event'");
|
||||
}
|
||||
im->vfinger_down = down;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
||||
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 = {
|
||||
.screen_size = screen->frame_size,
|
||||
.point = screen_convert_window_to_frame_coords(screen,
|
||||
mouse_x, mouse_y),
|
||||
.point = get_mouse_point(screen),
|
||||
};
|
||||
|
||||
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
||||
|
@ -3,50 +3,28 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "common.h"
|
||||
#include "controller.h"
|
||||
#include "fps_counter.h"
|
||||
#include "scrcpy.h"
|
||||
#include "screen.h"
|
||||
#include "video_buffer.h"
|
||||
#include "screen.h"
|
||||
|
||||
struct input_manager {
|
||||
struct controller *controller;
|
||||
struct video_buffer *video_buffer;
|
||||
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 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
|
||||
input_manager_process_text_input(struct input_manager *im,
|
||||
const SDL_TextInputEvent *event);
|
||||
|
||||
void
|
||||
input_manager_process_key(struct input_manager *im,
|
||||
const SDL_KeyboardEvent *event);
|
||||
const SDL_KeyboardEvent *event,
|
||||
bool control);
|
||||
|
||||
void
|
||||
input_manager_process_mouse_motion(struct input_manager *im,
|
||||
@ -58,7 +36,8 @@ input_manager_process_touch(struct input_manager *im,
|
||||
|
||||
void
|
||||
input_manager_process_mouse_button(struct input_manager *im,
|
||||
const SDL_MouseButtonEvent *event);
|
||||
const SDL_MouseButtonEvent *event,
|
||||
bool control);
|
||||
|
||||
void
|
||||
input_manager_process_mouse_wheel(struct input_manager *im,
|
||||
|
@ -1,6 +1,5 @@
|
||||
#include "scrcpy.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <libavformat/avformat.h>
|
||||
@ -30,24 +29,6 @@ print_version(void) {
|
||||
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
|
||||
main(int argc, char *argv[]) {
|
||||
#ifdef __WINDOWS__
|
||||
@ -57,23 +38,20 @@ main(int argc, char *argv[]) {
|
||||
setbuf(stderr, NULL);
|
||||
#endif
|
||||
|
||||
#ifndef NDEBUG
|
||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
||||
#endif
|
||||
|
||||
struct scrcpy_cli_args args = {
|
||||
.opts = SCRCPY_OPTIONS_DEFAULT,
|
||||
.help = false,
|
||||
.version = false,
|
||||
};
|
||||
|
||||
#ifndef NDEBUG
|
||||
args.opts.log_level = SC_LOG_LEVEL_DEBUG;
|
||||
#endif
|
||||
|
||||
if (!scrcpy_parse_args(&args, argc, argv)) {
|
||||
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) {
|
||||
scrcpy_print_usage(argv[0]);
|
||||
return 0;
|
||||
@ -98,5 +76,11 @@ main(int argc, char *argv[]) {
|
||||
|
||||
avformat_network_deinit(); // ignore failure
|
||||
|
||||
#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE)
|
||||
if (res != 0) {
|
||||
fprintf(stderr, "Press any key to continue...\n");
|
||||
getchar();
|
||||
}
|
||||
#endif
|
||||
return res;
|
||||
}
|
||||
|
@ -25,19 +25,10 @@ receiver_destroy(struct receiver *receiver) {
|
||||
static void
|
||||
process_msg(struct device_msg *msg) {
|
||||
switch (msg->type) {
|
||||
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;
|
||||
}
|
||||
|
||||
case DEVICE_MSG_TYPE_CLIPBOARD:
|
||||
LOGI("Device clipboard copied");
|
||||
SDL_SetClipboardText(msg->clipboard.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,29 +60,28 @@ static int
|
||||
run_receiver(void *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;
|
||||
|
||||
for (;;) {
|
||||
assert(head < DEVICE_MSG_MAX_SIZE);
|
||||
ssize_t r = net_recv(receiver->control_socket, buf + head,
|
||||
DEVICE_MSG_MAX_SIZE - head);
|
||||
assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE);
|
||||
ssize_t r = net_recv(receiver->control_socket, buf,
|
||||
DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
|
||||
if (r <= 0) {
|
||||
LOGD("Receiver stopped");
|
||||
break;
|
||||
}
|
||||
|
||||
head += r;
|
||||
ssize_t consumed = process_msgs(buf, head);
|
||||
ssize_t consumed = process_msgs(buf, r);
|
||||
if (consumed == -1) {
|
||||
// an error occurred
|
||||
break;
|
||||
}
|
||||
|
||||
if (consumed) {
|
||||
head -= consumed;
|
||||
// shift the remaining data in the buffer
|
||||
memmove(buf, &buf[consumed], head);
|
||||
memmove(buf, &buf[consumed], r - consumed);
|
||||
head = r - consumed;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ recorder_queue_clear(struct recorder_queue *queue) {
|
||||
bool
|
||||
recorder_init(struct recorder *recorder,
|
||||
const char *filename,
|
||||
enum sc_record_format format,
|
||||
enum recorder_format format,
|
||||
struct size declared_frame_size) {
|
||||
recorder->filename = SDL_strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
@ -105,10 +105,10 @@ recorder_destroy(struct recorder *recorder) {
|
||||
}
|
||||
|
||||
static const char *
|
||||
recorder_get_format_name(enum sc_record_format format) {
|
||||
recorder_get_format_name(enum recorder_format format) {
|
||||
switch (format) {
|
||||
case SC_RECORD_FORMAT_MP4: return "mp4";
|
||||
case SC_RECORD_FORMAT_MKV: return "matroska";
|
||||
case RECORDER_FORMAT_MP4: return "mp4";
|
||||
case RECORDER_FORMAT_MKV: return "matroska";
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
@ -361,14 +361,12 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) {
|
||||
|
||||
if (recorder->failed) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
mutex_unlock(recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct record_packet *rec = record_packet_new(packet);
|
||||
if (!rec) {
|
||||
LOGC("Could not allocate record packet");
|
||||
mutex_unlock(recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -8,9 +8,14 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "common.h"
|
||||
#include "scrcpy.h"
|
||||
#include "util/queue.h"
|
||||
|
||||
enum recorder_format {
|
||||
RECORDER_FORMAT_AUTO,
|
||||
RECORDER_FORMAT_MP4,
|
||||
RECORDER_FORMAT_MKV,
|
||||
};
|
||||
|
||||
struct record_packet {
|
||||
AVPacket packet;
|
||||
struct record_packet *next;
|
||||
@ -20,7 +25,7 @@ struct recorder_queue QUEUE(struct record_packet);
|
||||
|
||||
struct recorder {
|
||||
char *filename;
|
||||
enum sc_record_format format;
|
||||
enum recorder_format format;
|
||||
AVFormatContext *ctx;
|
||||
struct size declared_frame_size;
|
||||
bool header_written;
|
||||
@ -41,7 +46,7 @@ struct recorder {
|
||||
|
||||
bool
|
||||
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
|
||||
recorder_destroy(struct recorder *recorder);
|
||||
|
@ -8,8 +8,6 @@
|
||||
#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
|
||||
|
||||
@ -34,7 +32,7 @@
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
|
||||
static struct server server;
|
||||
static struct server server = SERVER_INITIALIZER;
|
||||
static struct screen screen = SCREEN_INITIALIZER;
|
||||
static struct fps_counter fps_counter;
|
||||
static struct video_buffer video_buffer;
|
||||
@ -48,14 +46,7 @@ static struct input_manager input_manager = {
|
||||
.controller = &controller,
|
||||
.video_buffer = &video_buffer,
|
||||
.screen = &screen,
|
||||
.repeat = 0,
|
||||
|
||||
// initialized later
|
||||
.prefer_text = false,
|
||||
.sdl_shortcut_mods = {
|
||||
.data = {0},
|
||||
.count = 0,
|
||||
},
|
||||
.prefer_text = false, // initialized later
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
@ -72,8 +63,7 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
|
||||
|
||||
// init SDL and set appropriate hints
|
||||
static bool
|
||||
sdl_init_and_configure(bool display, const char *render_driver,
|
||||
bool disable_screensaver) {
|
||||
sdl_init_and_configure(bool display, const char *render_driver) {
|
||||
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
|
||||
if (SDL_Init(flags)) {
|
||||
LOGC("Could not initialize SDL: %s", SDL_GetError());
|
||||
@ -122,13 +112,8 @@ sdl_init_and_configure(bool display, const char *render_driver,
|
||||
LOGW("Could not disable minimize on focus loss");
|
||||
}
|
||||
|
||||
if (disable_screensaver) {
|
||||
LOGD("Screensaver disabled");
|
||||
SDL_DisableScreenSaver();
|
||||
} else {
|
||||
LOGD("Screensaver enabled");
|
||||
SDL_EnableScreenSaver();
|
||||
}
|
||||
// Do not disable the screensaver when scrcpy is running
|
||||
SDL_EnableScreenSaver();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -148,10 +133,10 @@ static int
|
||||
event_watcher(void *data, SDL_Event *event) {
|
||||
(void) data;
|
||||
if (event->type == SDL_WINDOWEVENT
|
||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
&& event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
// In practice, it seems to always be called from the same thread in
|
||||
// that specific case. Anyway, it's just a workaround.
|
||||
screen_render(&screen, true);
|
||||
screen_render(&screen);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@ -170,7 +155,7 @@ 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) {
|
||||
case EVENT_STREAM_STOPPED:
|
||||
LOGD("Video stream stopped");
|
||||
@ -192,7 +177,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
||||
screen_handle_window_event(&screen, &event->window);
|
||||
break;
|
||||
case SDL_TEXTINPUT:
|
||||
if (!options->control) {
|
||||
if (!control) {
|
||||
break;
|
||||
}
|
||||
input_manager_process_text_input(&input_manager, &event->text);
|
||||
@ -201,16 +186,16 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
||||
case SDL_KEYUP:
|
||||
// some key events do not interact with the device, so process the
|
||||
// event even if control is disabled
|
||||
input_manager_process_key(&input_manager, &event->key);
|
||||
input_manager_process_key(&input_manager, &event->key, control);
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
if (!options->control) {
|
||||
if (!control) {
|
||||
break;
|
||||
}
|
||||
input_manager_process_mouse_motion(&input_manager, &event->motion);
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
if (!options->control) {
|
||||
if (!control) {
|
||||
break;
|
||||
}
|
||||
input_manager_process_mouse_wheel(&input_manager, &event->wheel);
|
||||
@ -219,7 +204,8 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
// some mouse events do not interact with the device, so process
|
||||
// the event even if control is disabled
|
||||
input_manager_process_mouse_button(&input_manager, &event->button);
|
||||
input_manager_process_mouse_button(&input_manager, &event->button,
|
||||
control);
|
||||
break;
|
||||
case SDL_FINGERMOTION:
|
||||
case SDL_FINGERDOWN:
|
||||
@ -227,7 +213,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
||||
input_manager_process_touch(&input_manager, &event->tfinger);
|
||||
break;
|
||||
case SDL_DROPFILE: {
|
||||
if (!options->control) {
|
||||
if (!control) {
|
||||
break;
|
||||
}
|
||||
file_handler_action_t action;
|
||||
@ -244,15 +230,16 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
||||
}
|
||||
|
||||
static bool
|
||||
event_loop(const struct scrcpy_options *options) {
|
||||
event_loop(bool display, bool control) {
|
||||
(void) display;
|
||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||
if (options->display) {
|
||||
if (display) {
|
||||
SDL_AddEventWatch(event_watcher, NULL);
|
||||
}
|
||||
#endif
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
enum event_result result = handle_event(&event, options);
|
||||
enum event_result result = handle_event(&event, control);
|
||||
switch (result) {
|
||||
case EVENT_RESULT_STOPPED_BY_USER:
|
||||
return true;
|
||||
@ -304,24 +291,8 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
|
||||
bool
|
||||
scrcpy(const struct scrcpy_options *options) {
|
||||
if (!server_init(&server)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
|
||||
bool server_started = false;
|
||||
bool fps_counter_initialized = false;
|
||||
bool video_buffer_initialized = false;
|
||||
bool file_handler_initialized = false;
|
||||
bool recorder_initialized = false;
|
||||
bool stream_started = false;
|
||||
bool controller_initialized = false;
|
||||
bool controller_started = false;
|
||||
|
||||
bool record = !!options->record_filename;
|
||||
struct server_params params = {
|
||||
.log_level = options->log_level,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.max_size = options->max_size,
|
||||
@ -332,18 +303,22 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
.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, ¶ms)) {
|
||||
goto end;
|
||||
return false;
|
||||
}
|
||||
|
||||
server_started = true;
|
||||
bool ret = false;
|
||||
|
||||
if (!sdl_init_and_configure(options->display, options->render_driver,
|
||||
options->disable_screensaver)) {
|
||||
bool fps_counter_initialized = false;
|
||||
bool video_buffer_initialized = false;
|
||||
bool file_handler_initialized = false;
|
||||
bool recorder_initialized = false;
|
||||
bool stream_started = false;
|
||||
bool controller_initialized = false;
|
||||
bool controller_started = false;
|
||||
|
||||
if (!sdl_init_and_configure(options->display, options->render_driver)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
@ -430,7 +405,7 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
options->window_y, options->window_width,
|
||||
options->window_height,
|
||||
options->window_borderless,
|
||||
options->rotation, options->mipmaps)) {
|
||||
options->rotation, options-> mipmaps)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
@ -449,9 +424,9 @@ scrcpy(const struct scrcpy_options *options) {
|
||||
}
|
||||
}
|
||||
|
||||
input_manager_init(&input_manager, options);
|
||||
input_manager.prefer_text = options->prefer_text;
|
||||
|
||||
ret = event_loop(options);
|
||||
ret = event_loop(options->display, options->control);
|
||||
LOGD("quit...");
|
||||
|
||||
screen_destroy(&screen);
|
||||
@ -472,10 +447,8 @@ end:
|
||||
fps_counter_interrupt(&fps_counter);
|
||||
}
|
||||
|
||||
if (server_started) {
|
||||
// shutdown the sockets and kill the server
|
||||
server_stop(&server);
|
||||
}
|
||||
// shutdown the sockets and kill the server
|
||||
server_stop(&server);
|
||||
|
||||
// now that the sockets are shutdown, the stream and controller are
|
||||
// interrupted, we can join them
|
||||
|
@ -2,46 +2,12 @@
|
||||
#define SCRCPY_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
enum sc_log_level {
|
||||
SC_LOG_LEVEL_DEBUG,
|
||||
SC_LOG_LEVEL_INFO,
|
||||
SC_LOG_LEVEL_WARN,
|
||||
SC_LOG_LEVEL_ERROR,
|
||||
};
|
||||
|
||||
enum sc_record_format {
|
||||
SC_RECORD_FORMAT_AUTO,
|
||||
SC_RECORD_FORMAT_MP4,
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
};
|
||||
|
||||
#define SC_MAX_SHORTCUT_MODS 8
|
||||
|
||||
enum sc_shortcut_mod {
|
||||
SC_MOD_LCTRL = 1 << 0,
|
||||
SC_MOD_RCTRL = 1 << 1,
|
||||
SC_MOD_LALT = 1 << 2,
|
||||
SC_MOD_RALT = 1 << 3,
|
||||
SC_MOD_LSUPER = 1 << 4,
|
||||
SC_MOD_RSUPER = 1 << 5,
|
||||
};
|
||||
|
||||
struct sc_shortcut_mods {
|
||||
unsigned data[SC_MAX_SHORTCUT_MODS];
|
||||
unsigned count;
|
||||
};
|
||||
|
||||
struct sc_port_range {
|
||||
uint16_t first;
|
||||
uint16_t last;
|
||||
};
|
||||
|
||||
#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
|
||||
#include "common.h"
|
||||
#include "input_manager.h"
|
||||
#include "recorder.h"
|
||||
|
||||
struct scrcpy_options {
|
||||
const char *serial;
|
||||
@ -50,19 +16,15 @@ struct scrcpy_options {
|
||||
const char *window_title;
|
||||
const char *push_target;
|
||||
const char *render_driver;
|
||||
const char *codec_options;
|
||||
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;
|
||||
enum recorder_format record_format;
|
||||
struct port_range port_range;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||
int16_t window_x; // WINDOW_POSITION_UNDEFINED for "auto"
|
||||
int16_t window_y; // WINDOW_POSITION_UNDEFINED for "auto"
|
||||
uint16_t window_width;
|
||||
uint16_t window_height;
|
||||
uint16_t display_id;
|
||||
@ -77,11 +39,6 @@ struct scrcpy_options {
|
||||
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 { \
|
||||
@ -91,25 +48,18 @@ struct scrcpy_options {
|
||||
.window_title = NULL, \
|
||||
.push_target = NULL, \
|
||||
.render_driver = NULL, \
|
||||
.codec_options = NULL, \
|
||||
.encoder_name = NULL, \
|
||||
.log_level = SC_LOG_LEVEL_INFO, \
|
||||
.record_format = SC_RECORD_FORMAT_AUTO, \
|
||||
.record_format = RECORDER_FORMAT_AUTO, \
|
||||
.port_range = { \
|
||||
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
||||
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
|
||||
}, \
|
||||
.shortcut_mods = { \
|
||||
.data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
|
||||
.count = 2, \
|
||||
}, \
|
||||
.max_size = DEFAULT_MAX_SIZE, \
|
||||
.bit_rate = DEFAULT_BIT_RATE, \
|
||||
.max_fps = 0, \
|
||||
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
|
||||
.rotation = 0, \
|
||||
.window_x = SC_WINDOW_POSITION_UNDEFINED, \
|
||||
.window_y = SC_WINDOW_POSITION_UNDEFINED, \
|
||||
.window_x = WINDOW_POSITION_UNDEFINED, \
|
||||
.window_y = WINDOW_POSITION_UNDEFINED, \
|
||||
.window_width = 0, \
|
||||
.window_height = 0, \
|
||||
.display_id = 0, \
|
||||
@ -124,11 +74,6 @@ struct scrcpy_options {
|
||||
.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
|
||||
|
221
app/src/screen.c
221
app/src/screen.c
@ -8,7 +8,6 @@
|
||||
#include "common.h"
|
||||
#include "compat.h"
|
||||
#include "icon.xpm"
|
||||
#include "scrcpy.h"
|
||||
#include "tiny_xpm.h"
|
||||
#include "video_buffer.h"
|
||||
#include "util/lock.h"
|
||||
@ -69,16 +68,6 @@ get_preferred_display_bounds(struct size *bounds) {
|
||||
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:
|
||||
// - it attempts to keep at least one dimension of the current_size (i.e. it
|
||||
// crops the black borders)
|
||||
@ -91,36 +80,40 @@ get_optimal_size(struct size current_size, struct size content_size) {
|
||||
return current_size;
|
||||
}
|
||||
|
||||
struct size window_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)) {
|
||||
// could not get display bounds, do not constraint the size
|
||||
window_size.width = current_size.width;
|
||||
window_size.height = current_size.height;
|
||||
w = current_size.width;
|
||||
h = current_size.height;
|
||||
} else {
|
||||
window_size.width = MIN(current_size.width, display_size.width);
|
||||
window_size.height = MIN(current_size.height, display_size.height);
|
||||
w = MIN(current_size.width, display_size.width);
|
||||
h = MIN(current_size.height, display_size.height);
|
||||
}
|
||||
|
||||
if (is_optimal_size(window_size, content_size)) {
|
||||
return window_size;
|
||||
if (h == w * content_size.height / content_size.width
|
||||
|| w == h * content_size.width / content_size.height) {
|
||||
// The size is already optimal, if we ignore rounding errors due to
|
||||
// integer window dimensions
|
||||
return (struct size) {w, h};
|
||||
}
|
||||
|
||||
bool keep_width = content_size.width * window_size.height
|
||||
> content_size.height * window_size.width;
|
||||
bool keep_width = content_size.width * h > content_size.height * w;
|
||||
if (keep_width) {
|
||||
// remove black borders on top and bottom
|
||||
window_size.height = content_size.height * window_size.width
|
||||
/ content_size.width;
|
||||
h = content_size.height * w / content_size.width;
|
||||
} else {
|
||||
// remove black borders on left and right (or none at all if it already
|
||||
// fits)
|
||||
window_size.width = content_size.width * window_size.height
|
||||
/ content_size.height;
|
||||
w = content_size.width * h / 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
|
||||
@ -157,43 +150,6 @@ get_initial_optimal_size(struct size content_size, uint16_t req_width,
|
||||
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
|
||||
screen_init(struct screen *screen) {
|
||||
*screen = (struct screen) SCREEN_INITIALIZER;
|
||||
@ -218,7 +174,7 @@ create_texture(struct screen *screen) {
|
||||
// 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);
|
||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f);
|
||||
|
||||
SDL_GL_UnbindTexture(texture);
|
||||
}
|
||||
@ -258,9 +214,9 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
||||
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;
|
||||
int y = window_y != SC_WINDOW_POSITION_UNDEFINED
|
||||
int y = window_y != WINDOW_POSITION_UNDEFINED
|
||||
? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
|
||||
screen->window = SDL_CreateWindow(window_title, x, y,
|
||||
window_size.width, window_size.height,
|
||||
@ -283,6 +239,13 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
||||
const char *renderer_name = r ? NULL : renderer_info.name;
|
||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||
|
||||
if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width,
|
||||
content_size.height)) {
|
||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
||||
screen_destroy(screen);
|
||||
return false;
|
||||
}
|
||||
|
||||
// starts with "opengl"
|
||||
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||
if (screen->use_opengl) {
|
||||
@ -306,7 +269,7 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
||||
LOGI("Trilinear filtering disabled");
|
||||
}
|
||||
} else {
|
||||
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||
LOGW("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||
}
|
||||
|
||||
SDL_Surface *icon = read_xpm(icon_xpm);
|
||||
@ -326,13 +289,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset the window size to trigger a SIZE_CHANGED event, to workaround
|
||||
// HiDPI issues with some SDL renderers when several displays having
|
||||
// different HiDPI scaling are connected
|
||||
SDL_SetWindowSize(screen->window, window_size.width, window_size.height);
|
||||
|
||||
screen_update_content_rect(screen);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -403,12 +359,18 @@ screen_set_rotation(struct screen *screen, unsigned rotation) {
|
||||
struct size new_content_size =
|
||||
get_rotated_size(screen->frame_size, rotation);
|
||||
|
||||
set_content_size(screen, new_content_size);
|
||||
if (SDL_RenderSetLogicalSize(screen->renderer,
|
||||
new_content_size.width,
|
||||
new_content_size.height)) {
|
||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
set_content_size(screen, new_content_size);
|
||||
screen->rotation = rotation;
|
||||
LOGI("Display rotation set to %u", rotation);
|
||||
|
||||
screen_render(screen, true);
|
||||
screen_render(screen);
|
||||
}
|
||||
|
||||
// recreate the texture and resize the window if the frame size has changed
|
||||
@ -416,16 +378,20 @@ static bool
|
||||
prepare_for_frame(struct screen *screen, struct size new_frame_size) {
|
||||
if (screen->frame_size.width != new_frame_size.width
|
||||
|| screen->frame_size.height != new_frame_size.height) {
|
||||
struct size new_content_size =
|
||||
get_rotated_size(new_frame_size, screen->rotation);
|
||||
if (SDL_RenderSetLogicalSize(screen->renderer,
|
||||
new_content_size.width,
|
||||
new_content_size.height)) {
|
||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// frame dimension changed, destroy texture
|
||||
SDL_DestroyTexture(screen->texture);
|
||||
|
||||
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);
|
||||
screen->frame_size = new_frame_size;
|
||||
|
||||
LOGI("New texture: %" PRIu16 "x%" PRIu16,
|
||||
screen->frame_size.width, screen->frame_size.height);
|
||||
@ -467,19 +433,15 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) {
|
||||
update_texture(screen, frame);
|
||||
mutex_unlock(vb->mutex);
|
||||
|
||||
screen_render(screen, false);
|
||||
screen_render(screen);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
screen_render(struct screen *screen, bool update_content_rect) {
|
||||
if (update_content_rect) {
|
||||
screen_update_content_rect(screen);
|
||||
}
|
||||
|
||||
screen_render(struct screen *screen) {
|
||||
SDL_RenderClear(screen->renderer);
|
||||
if (screen->rotation == 0) {
|
||||
SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
|
||||
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
|
||||
} else {
|
||||
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
||||
// counterclockwise (to be consistent with --lock-video-orientation)
|
||||
@ -489,14 +451,12 @@ screen_render(struct screen *screen, bool update_content_rect) {
|
||||
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;
|
||||
struct size size = screen->content_size;
|
||||
rect.x = (size.width - size.height) / 2;
|
||||
rect.y = (size.height - size.width) / 2;
|
||||
rect.w = size.height;
|
||||
rect.h = size.width;
|
||||
dstrect = ▭
|
||||
} else {
|
||||
assert(screen->rotation == 2);
|
||||
dstrect = &screen->rect;
|
||||
}
|
||||
|
||||
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
|
||||
@ -519,15 +479,20 @@ screen_switch_fullscreen(struct screen *screen) {
|
||||
}
|
||||
|
||||
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
||||
screen_render(screen, true);
|
||||
screen_render(screen);
|
||||
}
|
||||
|
||||
void
|
||||
screen_resize_to_fit(struct screen *screen) {
|
||||
if (screen->fullscreen || screen->maximized) {
|
||||
if (screen->fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen->maximized) {
|
||||
SDL_RestoreWindow(screen->window);
|
||||
screen->maximized = false;
|
||||
}
|
||||
|
||||
struct size optimal_size =
|
||||
get_optimal_window_size(screen, screen->content_size);
|
||||
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
|
||||
@ -552,27 +517,41 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
|
||||
content_size.height);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
is_fullscreen(const struct screen *screen) {
|
||||
uint32_t flags = SDL_GetWindowFlags(screen->window);
|
||||
return !!(flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP));
|
||||
}
|
||||
|
||||
static void
|
||||
update_fullscreen_state(struct screen *screen) {
|
||||
// There is no SDL event to detect fullscreen changes, so store the
|
||||
// state in a field and compare on every "size changed" event
|
||||
bool fullscreen = is_fullscreen(screen);
|
||||
if (fullscreen != screen->fullscreen) {
|
||||
// Fullscreen state have changed
|
||||
screen->fullscreen = fullscreen;
|
||||
if (!fullscreen && !screen->maximized) {
|
||||
apply_pending_resize(screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
screen_handle_window_event(struct screen *screen,
|
||||
const SDL_WindowEvent *event) {
|
||||
switch (event->event) {
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
screen_render(screen, true);
|
||||
screen_render(screen);
|
||||
break;
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
screen_render(screen, true);
|
||||
update_fullscreen_state(screen);
|
||||
screen_render(screen);
|
||||
break;
|
||||
case SDL_WINDOWEVENT_MAXIMIZED:
|
||||
screen->maximized = true;
|
||||
break;
|
||||
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;
|
||||
apply_pending_resize(screen);
|
||||
break;
|
||||
@ -580,19 +559,12 @@ screen_handle_window_event(struct screen *screen,
|
||||
}
|
||||
|
||||
struct point
|
||||
screen_convert_drawable_to_frame_coords(struct screen *screen,
|
||||
int32_t x, int32_t y) {
|
||||
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
|
||||
unsigned rotation = screen->rotation;
|
||||
assert(rotation < 4);
|
||||
|
||||
int32_t w = screen->content_size.width;
|
||||
int32_t h = screen->content_size.height;
|
||||
|
||||
|
||||
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:
|
||||
@ -615,22 +587,3 @@ screen_convert_drawable_to_frame_coords(struct screen *screen,
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -9,6 +9,8 @@
|
||||
#include "common.h"
|
||||
#include "opengl.h"
|
||||
|
||||
#define WINDOW_POSITION_UNDEFINED (-0x8000)
|
||||
|
||||
struct video_buffer;
|
||||
|
||||
struct screen {
|
||||
@ -27,8 +29,6 @@ struct screen {
|
||||
|
||||
// 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 fullscreen;
|
||||
bool maximized;
|
||||
@ -56,12 +56,6 @@ struct screen {
|
||||
.height = 0, \
|
||||
}, \
|
||||
.rotation = 0, \
|
||||
.rect = { \
|
||||
.x = 0, \
|
||||
.y = 0, \
|
||||
.w = 0, \
|
||||
.h = 0, \
|
||||
}, \
|
||||
.has_frame = false, \
|
||||
.fullscreen = false, \
|
||||
.maximized = false, \
|
||||
@ -74,7 +68,7 @@ void
|
||||
screen_init(struct screen *screen);
|
||||
|
||||
// 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
|
||||
screen_init_rendering(struct screen *screen, const char *window_title,
|
||||
struct size frame_size, bool always_on_top,
|
||||
@ -95,11 +89,8 @@ bool
|
||||
screen_update_frame(struct screen *screen, struct video_buffer *vb);
|
||||
|
||||
// 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
|
||||
screen_render(struct screen *screen, bool update_content_rect);
|
||||
screen_render(struct screen *screen);
|
||||
|
||||
// switch the fullscreen mode
|
||||
void
|
||||
@ -124,20 +115,6 @@ 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);
|
||||
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y);
|
||||
|
||||
#endif
|
||||
|
117
app/src/server.c
117
app/src/server.c
@ -11,7 +11,6 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "command.h"
|
||||
#include "util/lock.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
#include "util/str_util.h"
|
||||
@ -144,7 +143,7 @@ listen_on_port(uint16_t port) {
|
||||
|
||||
static bool
|
||||
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;
|
||||
for (;;) {
|
||||
if (!enable_tunnel_reverse(server->serial, port)) {
|
||||
@ -190,7 +189,7 @@ enable_tunnel_reverse_any_port(struct server *server,
|
||||
|
||||
static bool
|
||||
enable_tunnel_forward_any_port(struct server *server,
|
||||
struct sc_port_range port_range) {
|
||||
struct port_range port_range) {
|
||||
server->tunnel_forward = true;
|
||||
uint16_t port = port_range.first;
|
||||
for (;;) {
|
||||
@ -202,7 +201,7 @@ enable_tunnel_forward_any_port(struct server *server,
|
||||
|
||||
if (port < port_range.last) {
|
||||
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
|
||||
port, (uint16_t) (port + 1));
|
||||
port, port + 1);
|
||||
port++;
|
||||
continue;
|
||||
}
|
||||
@ -218,46 +217,24 @@ enable_tunnel_forward_any_port(struct server *server,
|
||||
}
|
||||
|
||||
static bool
|
||||
enable_tunnel_any_port(struct server *server, struct sc_port_range port_range,
|
||||
bool force_adb_forward) {
|
||||
if (!force_adb_forward) {
|
||||
// Attempt to use "adb reverse"
|
||||
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if "adb reverse" does not work (e.g. over "adb connect"), it
|
||||
// fallbacks to "adb forward", so the app socket is the client
|
||||
|
||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
|
||||
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
|
||||
// "adb forward", so the app socket is the client
|
||||
|
||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||
return enable_tunnel_forward_any_port(server, port_range);
|
||||
}
|
||||
|
||||
static 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
|
||||
execute_server(struct server *server, const struct server_params *params) {
|
||||
char max_size_string[6];
|
||||
char bit_rate_string[11];
|
||||
char max_fps_string[6];
|
||||
char lock_video_orientation_string[5];
|
||||
char lock_video_orientation_string[3];
|
||||
char display_id_string[6];
|
||||
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
||||
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
||||
@ -282,7 +259,6 @@ execute_server(struct server *server, const struct server_params *params) {
|
||||
"/", // unused
|
||||
"com.genymobile.scrcpy.Server",
|
||||
SCRCPY_VERSION,
|
||||
log_level_to_server_string(params->log_level),
|
||||
max_size_string,
|
||||
bit_rate_string,
|
||||
max_fps_string,
|
||||
@ -294,8 +270,6 @@ execute_server(struct server *server, const struct server_params *params) {
|
||||
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
|
||||
LOGI("Server debugger waiting for a client on device port "
|
||||
@ -354,51 +328,15 @@ close_socket(socket_t socket) {
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
void
|
||||
server_init(struct server *server) {
|
||||
server->serial = NULL;
|
||||
server->process = PROCESS_NONE;
|
||||
server->wait_server_thread = NULL;
|
||||
atomic_flag_clear_explicit(&server->server_socket_closed,
|
||||
memory_order_relaxed);
|
||||
|
||||
server->mutex = SDL_CreateMutex();
|
||||
if (!server->mutex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
server->process_terminated_cond = SDL_CreateCond();
|
||||
if (!server->process_terminated_cond) {
|
||||
SDL_DestroyMutex(server->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
server->process_terminated = false;
|
||||
|
||||
server->server_socket = INVALID_SOCKET;
|
||||
server->video_socket = INVALID_SOCKET;
|
||||
server->control_socket = INVALID_SOCKET;
|
||||
|
||||
server->port_range.first = 0;
|
||||
server->port_range.last = 0;
|
||||
server->local_port = 0;
|
||||
|
||||
server->tunnel_enabled = false;
|
||||
server->tunnel_forward = false;
|
||||
|
||||
return true;
|
||||
*server = (struct server) SERVER_INITIALIZER;
|
||||
}
|
||||
|
||||
static int
|
||||
run_wait_server(void *data) {
|
||||
struct server *server = data;
|
||||
cmd_simple_wait(server->process, NULL); // ignore exit code
|
||||
|
||||
mutex_lock(server->mutex);
|
||||
server->process_terminated = true;
|
||||
cond_signal(server->process_terminated_cond);
|
||||
mutex_unlock(server->mutex);
|
||||
|
||||
// no need for synchronization, server_socket is initialized before this
|
||||
// thread was created
|
||||
if (server->server_socket != INVALID_SOCKET
|
||||
@ -427,8 +365,7 @@ server_start(struct server *server, const char *serial,
|
||||
goto error1;
|
||||
}
|
||||
|
||||
if (!enable_tunnel_any_port(server, params->port_range,
|
||||
params->force_adb_forward)) {
|
||||
if (!enable_tunnel_any_port(server, params->port_range)) {
|
||||
goto error1;
|
||||
}
|
||||
|
||||
@ -530,39 +467,17 @@ server_stop(struct server *server) {
|
||||
|
||||
assert(server->process != PROCESS_NONE);
|
||||
|
||||
cmd_terminate(server->process);
|
||||
|
||||
if (server->tunnel_enabled) {
|
||||
// ignore failure
|
||||
disable_tunnel(server);
|
||||
}
|
||||
|
||||
// Give some delay for the server to terminate properly
|
||||
mutex_lock(server->mutex);
|
||||
int r = 0;
|
||||
if (!server->process_terminated) {
|
||||
#define WATCHDOG_DELAY_MS 1000
|
||||
r = cond_wait_timeout(server->process_terminated_cond,
|
||||
server->mutex,
|
||||
WATCHDOG_DELAY_MS);
|
||||
}
|
||||
mutex_unlock(server->mutex);
|
||||
|
||||
// After this delay, kill the server if it's not dead already.
|
||||
// On some devices, closing the sockets is not sufficient to wake up the
|
||||
// blocking calls while the device is asleep.
|
||||
if (r == SDL_MUTEX_TIMEDOUT) {
|
||||
// FIXME There is a race condition here: there is a small chance that
|
||||
// the process is already terminated, and the PID assigned to a new
|
||||
// process.
|
||||
LOGW("Killing the server...");
|
||||
cmd_terminate(server->process);
|
||||
}
|
||||
|
||||
SDL_WaitThread(server->wait_server_thread, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
server_destroy(struct server *server) {
|
||||
SDL_free(server->serial);
|
||||
SDL_DestroyCond(server->process_terminated_cond);
|
||||
SDL_DestroyMutex(server->mutex);
|
||||
}
|
||||
|
@ -9,8 +9,6 @@
|
||||
#include "config.h"
|
||||
#include "command.h"
|
||||
#include "common.h"
|
||||
#include "scrcpy.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
|
||||
struct server {
|
||||
@ -18,26 +16,35 @@ struct server {
|
||||
process_t process;
|
||||
SDL_Thread *wait_server_thread;
|
||||
atomic_flag server_socket_closed;
|
||||
|
||||
SDL_mutex *mutex;
|
||||
SDL_cond *process_terminated_cond;
|
||||
bool process_terminated;
|
||||
|
||||
socket_t server_socket; // only used if !tunnel_forward
|
||||
socket_t video_socket;
|
||||
socket_t control_socket;
|
||||
struct sc_port_range port_range;
|
||||
struct port_range port_range;
|
||||
uint16_t local_port; // selected from port_range
|
||||
bool tunnel_enabled;
|
||||
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
|
||||
};
|
||||
|
||||
#define SERVER_INITIALIZER { \
|
||||
.serial = NULL, \
|
||||
.process = PROCESS_NONE, \
|
||||
.wait_server_thread = NULL, \
|
||||
.server_socket_closed = ATOMIC_FLAG_INIT, \
|
||||
.server_socket = INVALID_SOCKET, \
|
||||
.video_socket = INVALID_SOCKET, \
|
||||
.control_socket = INVALID_SOCKET, \
|
||||
.port_range = { \
|
||||
.first = 0, \
|
||||
.last = 0, \
|
||||
}, \
|
||||
.local_port = 0, \
|
||||
.tunnel_enabled = false, \
|
||||
.tunnel_forward = false, \
|
||||
}
|
||||
|
||||
struct server_params {
|
||||
enum sc_log_level log_level;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
struct sc_port_range port_range;
|
||||
struct port_range port_range;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
@ -46,11 +53,10 @@ struct server_params {
|
||||
uint16_t display_id;
|
||||
bool show_touches;
|
||||
bool stay_awake;
|
||||
bool force_adb_forward;
|
||||
};
|
||||
|
||||
// init default values
|
||||
bool
|
||||
void
|
||||
server_init(struct server *server);
|
||||
|
||||
// push, enable tunnel et start the server
|
||||
|
@ -14,6 +14,7 @@ struct video_buffer;
|
||||
|
||||
struct stream {
|
||||
socket_t socket;
|
||||
struct video_buffer *video_buffer;
|
||||
SDL_Thread *thread;
|
||||
struct decoder *decoder;
|
||||
struct recorder *recorder;
|
||||
|
@ -1,14 +1,10 @@
|
||||
// for portability (kill, readlink, strdup, strtok_r)
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#define _BSD_SOURCE
|
||||
// for portability
|
||||
#define _POSIX_SOURCE // for kill()
|
||||
#define _BSD_SOURCE // for readlink()
|
||||
|
||||
// modern glibc will complain without this
|
||||
#define _DEFAULT_SOURCE
|
||||
|
||||
#ifdef __APPLE__
|
||||
# define _DARWIN_C_SOURCE // for strdup(), strtok_r(), memset_pattern4()
|
||||
#endif
|
||||
|
||||
#include "command.h"
|
||||
|
||||
#include "config.h"
|
||||
@ -21,6 +17,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
|
@ -39,7 +39,12 @@ cmd_execute(const char *const argv[], HANDLE *handle) {
|
||||
return PROCESS_ERROR_GENERIC;
|
||||
}
|
||||
|
||||
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si,
|
||||
#ifdef WINDOWS_NOCONSOLE
|
||||
int flags = CREATE_NO_WINDOW;
|
||||
#else
|
||||
int flags = 0;
|
||||
#endif
|
||||
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
|
||||
&pi)) {
|
||||
SDL_free(wide);
|
||||
*handle = NULL;
|
||||
@ -56,7 +61,7 @@ cmd_execute(const char *const argv[], HANDLE *handle) {
|
||||
|
||||
bool
|
||||
cmd_terminate(HANDLE handle) {
|
||||
return TerminateProcess(handle, 1);
|
||||
return TerminateProcess(handle, 1) && CloseHandle(handle);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -70,7 +75,6 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
|
||||
if (exit_code) {
|
||||
*exit_code = code;
|
||||
}
|
||||
CloseHandle(handle);
|
||||
return !code;
|
||||
}
|
||||
|
||||
|
@ -65,10 +65,7 @@ static void test_buffer_read64be(void) {
|
||||
assert(val == 0xABCD1234567890EF);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
int main(void) {
|
||||
test_buffer_write16be();
|
||||
test_buffer_write32be();
|
||||
test_buffer_write64be();
|
||||
|
@ -65,10 +65,7 @@ static void test_cbuf_push_take(void) {
|
||||
assert(item == 35);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
int main(void) {
|
||||
test_cbuf_empty();
|
||||
test_cbuf_full();
|
||||
test_cbuf_push_take();
|
||||
|
@ -1,9 +1,7 @@
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cli.h"
|
||||
#include "common.h"
|
||||
#include "scrcpy.h"
|
||||
|
||||
static void test_flag_version(void) {
|
||||
struct scrcpy_cli_args args = {
|
||||
@ -75,6 +73,7 @@ static void test_options(void) {
|
||||
|
||||
const struct scrcpy_options *opts = &args.opts;
|
||||
assert(opts->always_on_top);
|
||||
fprintf(stderr, "%d\n", (int) opts->bit_rate);
|
||||
assert(opts->bit_rate == 5000000);
|
||||
assert(!strcmp(opts->crop, "100:200:300:400"));
|
||||
assert(opts->fullscreen);
|
||||
@ -85,7 +84,7 @@ static void test_options(void) {
|
||||
assert(opts->port_range.last == 1236);
|
||||
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
|
||||
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(!strcmp(opts->serial, "0123456789abcdef"));
|
||||
assert(opts->show_touches);
|
||||
@ -120,54 +119,13 @@ static void test_options2(void) {
|
||||
assert(!opts->control);
|
||||
assert(!opts->display);
|
||||
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) {
|
||||
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;
|
||||
|
||||
int main(void) {
|
||||
test_flag_version();
|
||||
test_flag_help();
|
||||
test_options();
|
||||
test_options2();
|
||||
test_parse_shortcut_mods();
|
||||
return 0;
|
||||
};
|
||||
|
@ -9,20 +9,18 @@ static void test_serialize_inject_keycode(void) {
|
||||
.inject_keycode = {
|
||||
.action = AKEY_EVENT_ACTION_UP,
|
||||
.keycode = AKEYCODE_ENTER,
|
||||
.repeat = 5,
|
||||
.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);
|
||||
assert(size == 14);
|
||||
assert(size == 10);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
||||
0x01, // AKEY_EVENT_ACTION_UP
|
||||
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
|
||||
0x00, 0x00, 0x00, 0X05, // repeat
|
||||
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
|
||||
};
|
||||
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);
|
||||
assert(size == 18);
|
||||
assert(size == 16);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
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
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
@ -56,17 +54,15 @@ static void test_serialize_inject_text_long(void) {
|
||||
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
|
||||
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);
|
||||
assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||
assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||
|
||||
unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
|
||||
unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
|
||||
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
|
||||
expected[1] = 0x00;
|
||||
expected[2] = 0x00;
|
||||
expected[3] = 0x01;
|
||||
expected[4] = 0x2c; // text length (32 bits)
|
||||
memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||
expected[1] = 0x01;
|
||||
expected[2] = 0x2c; // text length (16 bits)
|
||||
memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
@ -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);
|
||||
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);
|
||||
assert(size == 21);
|
||||
|
||||
@ -146,7 +142,7 @@ static void test_serialize_back_or_screen_on(void) {
|
||||
.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);
|
||||
assert(size == 1);
|
||||
|
||||
@ -161,7 +157,7 @@ static void test_serialize_expand_notification_panel(void) {
|
||||
.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);
|
||||
assert(size == 1);
|
||||
|
||||
@ -176,7 +172,7 @@ static void test_serialize_collapse_notification_panel(void) {
|
||||
.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);
|
||||
assert(size == 1);
|
||||
|
||||
@ -191,7 +187,7 @@ static void test_serialize_get_clipboard(void) {
|
||||
.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);
|
||||
assert(size == 1);
|
||||
|
||||
@ -204,20 +200,18 @@ static void test_serialize_get_clipboard(void) {
|
||||
static void test_serialize_set_clipboard(void) {
|
||||
struct control_msg msg = {
|
||||
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||
.set_clipboard = {
|
||||
.paste = true,
|
||||
.inject_text = {
|
||||
.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);
|
||||
assert(size == 19);
|
||||
assert(size == 16);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||
1, // paste
|
||||
0x00, 0x00, 0x00, 0x0d, // text length
|
||||
0x00, 0x0d, // text length
|
||||
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
|
||||
};
|
||||
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);
|
||||
assert(size == 2);
|
||||
|
||||
@ -247,7 +241,7 @@ static void test_serialize_rotate_device(void) {
|
||||
.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);
|
||||
assert(size == 1);
|
||||
|
||||
@ -257,10 +251,7 @@ static void test_serialize_rotate_device(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
int main(void) {
|
||||
test_serialize_inject_keycode();
|
||||
test_serialize_inject_text();
|
||||
test_serialize_inject_text_long();
|
||||
|
@ -4,17 +4,16 @@
|
||||
#include "device_msg.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
static void test_deserialize_clipboard(void) {
|
||||
const unsigned char input[] = {
|
||||
DEVICE_MSG_TYPE_CLIPBOARD,
|
||||
0x00, 0x00, 0x00, 0x03, // text length
|
||||
0x00, 0x03, // text length
|
||||
0x41, 0x42, 0x43, // "ABC"
|
||||
};
|
||||
|
||||
struct device_msg 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.clipboard.text);
|
||||
@ -23,33 +22,7 @@ static void test_deserialize_clipboard(void) {
|
||||
device_msg_destroy(&msg);
|
||||
}
|
||||
|
||||
static void test_deserialize_clipboard_big(void) {
|
||||
unsigned char input[DEVICE_MSG_MAX_SIZE];
|
||||
input[0] = DEVICE_MSG_TYPE_CLIPBOARD;
|
||||
input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24;
|
||||
input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16;
|
||||
input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8;
|
||||
input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu;
|
||||
|
||||
memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH);
|
||||
|
||||
struct device_msg msg;
|
||||
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
|
||||
assert(r == DEVICE_MSG_MAX_SIZE);
|
||||
|
||||
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
|
||||
assert(msg.clipboard.text);
|
||||
assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH);
|
||||
assert(msg.clipboard.text[0] == 'a');
|
||||
|
||||
device_msg_destroy(&msg);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
int main(void) {
|
||||
test_deserialize_clipboard();
|
||||
test_deserialize_clipboard_big();
|
||||
return 0;
|
||||
}
|
||||
|
@ -32,10 +32,7 @@ static void test_queue(void) {
|
||||
assert(queue_is_empty(&queue));
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
int main(void) {
|
||||
test_queue();
|
||||
return 0;
|
||||
}
|
||||
|
@ -286,10 +286,7 @@ static void test_parse_integer_with_suffix(void) {
|
||||
assert(!ok);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
int main(void) {
|
||||
test_xstrncpy_simple();
|
||||
test_xstrncpy_just_fit();
|
||||
test_xstrncpy_truncated();
|
||||
|
@ -7,7 +7,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@ -19,9 +19,6 @@ allprojects {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-Xlint:deprecation"
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
|
@ -15,6 +15,6 @@ cpu = 'i686'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared'
|
||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev'
|
||||
prebuilt_sdl2 = 'SDL2-2.0.14/i686-w64-mingw32'
|
||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win32-shared'
|
||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win32-dev'
|
||||
prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32'
|
||||
|
@ -15,6 +15,6 @@ cpu = 'x86_64'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared'
|
||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev'
|
||||
prebuilt_sdl2 = 'SDL2-2.0.14/x86_64-w64-mingw32'
|
||||
prebuilt_ffmpeg_shared = 'ffmpeg-4.2.2-win64-shared'
|
||||
prebuilt_ffmpeg_dev = 'ffmpeg-4.2.2-win64-dev'
|
||||
prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32'
|
||||
|
@ -1,4 +0,0 @@
|
||||
@echo off
|
||||
scrcpy.exe %*
|
||||
:: if the exit code is >= 1, then pause
|
||||
if errorlevel 1 pause
|
@ -1 +0,0 @@
|
||||
CreateObject("Wscript.Shell").Run "cmd /c scrcpy.exe", 0, false
|
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17
|
||||
PREBUILT_SERVER_SHA256=11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
echo "[scrcpy] Verifying prebuilt server..."
|
||||
echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check
|
||||
|
||||
echo "[scrcpy] Building client..."
|
||||
rm -rf "$BUILDDIR"
|
||||
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=scrcpy-server
|
||||
cd "$BUILDDIR"
|
||||
ninja
|
||||
|
||||
echo "[scrcpy] Installing (sudo)..."
|
||||
sudo ninja install
|
@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '1.17',
|
||||
version: '1.13',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
@ -1,6 +1,7 @@
|
||||
option('compile_app', type: 'boolean', value: true, description: 'Build the client')
|
||||
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
|
||||
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
|
||||
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
|
||||
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
|
||||
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
|
||||
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
|
||||
|
@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
|
||||
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
|
||||
|
||||
prepare-ffmpeg-shared-win32:
|
||||
@./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-shared.zip \
|
||||
357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \
|
||||
ffmpeg-4.3.1-win32-shared
|
||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.2-win32-shared.zip \
|
||||
ab5d603aaa54de360db2c2ffe378c82376b9343ea1175421dd644639aa07ee31 \
|
||||
ffmpeg-4.2.2-win32-shared
|
||||
|
||||
prepare-ffmpeg-dev-win32:
|
||||
@./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-dev.zip \
|
||||
230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \
|
||||
ffmpeg-4.3.1-win32-dev
|
||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.2-win32-dev.zip \
|
||||
8d224be567a2950cad4be86f4aabdd045bfa52ad758e87c72cedd278613bc6c8 \
|
||||
ffmpeg-4.2.2-win32-dev
|
||||
|
||||
prepare-ffmpeg-shared-win64:
|
||||
@./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-shared.zip \
|
||||
dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \
|
||||
ffmpeg-4.3.1-win64-shared
|
||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.2-win64-shared.zip \
|
||||
5aedf268952b7d9f6541dbfcb47cd86a7e7881a3b7ba482fd3bc4ca33bda7bf5 \
|
||||
ffmpeg-4.2.2-win64-shared
|
||||
|
||||
prepare-ffmpeg-dev-win64:
|
||||
@./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \
|
||||
2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \
|
||||
ffmpeg-4.3.1-win64-dev
|
||||
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.2-win64-dev.zip \
|
||||
f4885f859c5b0d6663c2a0a4c1cf035b1c60b146402790b796bd3ad84f4f3ca2 \
|
||||
ffmpeg-4.2.2-win64-dev
|
||||
|
||||
prepare-sdl2:
|
||||
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.14-mingw.tar.gz \
|
||||
405eaff3eb18f2e08fe669ef9e63bc9a8710b7d343756f238619761e9b60407d \
|
||||
SDL2-2.0.14
|
||||
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \
|
||||
e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \
|
||||
SDL2-2.0.12
|
||||
|
||||
prepare-adb:
|
||||
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.5-windows.zip \
|
||||
549ba2bdc31f335eb8a504f005f77606a479cc216d6b64a3e8b64c780003661f \
|
||||
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \
|
||||
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
|
||||
platform-tools
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
url="$1"
|
||||
sum="$2"
|
||||
|
44
release.sh
44
release.sh
@ -1,2 +1,44 @@
|
||||
#!/bin/bash
|
||||
make -f release.mk
|
||||
set -e
|
||||
|
||||
# test locally
|
||||
TESTDIR=build_test
|
||||
rm -rf "$TESTDIR"
|
||||
# run client tests with ASAN enabled
|
||||
meson "$TESTDIR" -Db_sanitize=address
|
||||
ninja -C"$TESTDIR" test
|
||||
|
||||
# test server
|
||||
GRADLE=${GRADLE:-./gradlew}
|
||||
$GRADLE -p server check
|
||||
|
||||
BUILDDIR=build_release
|
||||
rm -rf "$BUILDDIR"
|
||||
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
|
||||
cd "$BUILDDIR"
|
||||
ninja
|
||||
cd -
|
||||
|
||||
# build Windows releases
|
||||
make -f Makefile.CrossWindows
|
||||
|
||||
# the generated server must be the same everywhere
|
||||
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server
|
||||
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server
|
||||
|
||||
# get version name
|
||||
TAG=$(git describe --tags --always)
|
||||
|
||||
# create release directory
|
||||
mkdir -p "release-$TAG"
|
||||
cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG"
|
||||
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
|
||||
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
|
||||
|
||||
# generate checksums
|
||||
cd "release-$TAG"
|
||||
sha256sum "scrcpy-server-$TAG" \
|
||||
"scrcpy-win32-$TAG.zip" \
|
||||
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt
|
||||
|
||||
echo "Release generated in release-$TAG/"
|
||||
|
2
run
2
run
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
# Run scrcpy generated in the specified BUILDDIR.
|
||||
#
|
||||
# This provides the same feature as "ninja run", except that it is possible to
|
||||
|
@ -1,2 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"
|
||||
|
@ -1,13 +1,13 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 20
|
||||
versionName "1.17"
|
||||
targetSdkVersion 29
|
||||
versionCode 15
|
||||
versionName "1.13"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
@ -20,7 +20,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
#
|
||||
# This script generates the scrcpy binary "manually" (without gradle).
|
||||
#
|
||||
@ -12,10 +12,10 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=1.17
|
||||
SCRCPY_VERSION_NAME=1.13
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-30}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0}
|
||||
PLATFORM=${ANDROID_PLATFORM:-29}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
CLASSES_DIR="$BUILD_DIR/classes"
|
||||
@ -42,8 +42,6 @@ echo "Generating java from aidl..."
|
||||
cd "$SERVER_DIR/src/main/aidl"
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
|
||||
android/view/IRotationWatcher.aidl
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
|
||||
android/content/IOnPrimaryClipChangedListener.aidl
|
||||
|
||||
echo "Compiling java sources..."
|
||||
cd ../java
|
||||
@ -57,7 +55,6 @@ cd "$CLASSES_DIR"
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
|
||||
--output "$BUILD_DIR/classes.dex" \
|
||||
android/view/*.class \
|
||||
android/content/*.class \
|
||||
com/genymobile/scrcpy/*.class \
|
||||
com/genymobile/scrcpy/wrappers/*.class
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
# Wrapper script to invoke gradle from meson
|
||||
set -e
|
||||
|
||||
|
@ -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();
|
||||
}
|
@ -19,19 +19,18 @@ public final class CleanUp {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
|
||||
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode;
|
||||
public static void configure(boolean disableShowTouches, int restoreStayOn) throws IOException {
|
||||
boolean needProcess = disableShowTouches || restoreStayOn != -1;
|
||||
if (needProcess) {
|
||||
startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode);
|
||||
startProcess(disableShowTouches, restoreStayOn);
|
||||
} 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)};
|
||||
private static void startProcess(boolean disableShowTouches, int restoreStayOn) throws IOException {
|
||||
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(restoreStayOn)};
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
builder.environment().put("CLASSPATH", SERVER_PATH);
|
||||
@ -60,7 +59,6 @@ public final class CleanUp {
|
||||
|
||||
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();
|
||||
@ -75,12 +73,5 @@ public final class CleanUp {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (restoreNormalPowerMode) {
|
||||
Ln.i("Restoring normal power mode");
|
||||
if (Device.isScreenOn()) {
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -28,18 +28,15 @@ public final class ControlMessage {
|
||||
private Position position;
|
||||
private int hScroll;
|
||||
private int vScroll;
|
||||
private boolean paste;
|
||||
private int repeat;
|
||||
|
||||
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();
|
||||
msg.type = TYPE_INJECT_KEYCODE;
|
||||
msg.action = action;
|
||||
msg.keycode = keycode;
|
||||
msg.repeat = repeat;
|
||||
msg.metaState = metaState;
|
||||
return msg;
|
||||
}
|
||||
@ -71,11 +68,10 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createSetClipboard(String text, boolean paste) {
|
||||
public static ControlMessage createSetClipboard(String text) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_CLIPBOARD;
|
||||
msg.text = text;
|
||||
msg.paste = paste;
|
||||
return msg;
|
||||
}
|
||||
|
||||
@ -138,12 +134,4 @@ public final class ControlMessage {
|
||||
public int getVScroll() {
|
||||
return vScroll;
|
||||
}
|
||||
|
||||
public boolean getPaste() {
|
||||
return paste;
|
||||
}
|
||||
|
||||
public int getRepeat() {
|
||||
return repeat;
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,18 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
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_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
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 CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes
|
||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
||||
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
||||
private static final int RAW_BUFFER_SIZE = 1024;
|
||||
|
||||
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
|
||||
|
||||
public ControlMessageReader() {
|
||||
// invariant: the buffer is always in "get" mode
|
||||
@ -98,23 +97,20 @@ public class ControlMessageReader {
|
||||
}
|
||||
int action = toUnsigned(buffer.get());
|
||||
int keycode = buffer.getInt();
|
||||
int repeat = buffer.getInt();
|
||||
int metaState = buffer.getInt();
|
||||
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
|
||||
return ControlMessage.createInjectKeycode(action, keycode, metaState);
|
||||
}
|
||||
|
||||
private String parseString() {
|
||||
if (buffer.remaining() < 4) {
|
||||
if (buffer.remaining() < 2) {
|
||||
return null;
|
||||
}
|
||||
int len = buffer.getInt();
|
||||
int len = toUnsigned(buffer.getShort());
|
||||
if (buffer.remaining() < len) {
|
||||
return null;
|
||||
}
|
||||
int position = buffer.position();
|
||||
// Move the buffer position to consume the text
|
||||
buffer.position(position + len);
|
||||
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
|
||||
buffer.get(textBuffer, 0, len);
|
||||
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectText() {
|
||||
@ -151,15 +147,11 @@ public class ControlMessageReader {
|
||||
}
|
||||
|
||||
private ControlMessage parseSetClipboard() {
|
||||
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
boolean paste = buffer.get() != 0;
|
||||
String text = parseString();
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
return ControlMessage.createSetClipboard(text, paste);
|
||||
return ControlMessage.createSetClipboard(text);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetScreenPowerMode() {
|
||||
|
@ -1,23 +1,20 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.os.Build;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Controller {
|
||||
|
||||
private static final int DEVICE_ID_VIRTUAL = -1;
|
||||
|
||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
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.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||
|
||||
private boolean keepPowerModeOff;
|
||||
|
||||
public Controller(Device device, DesktopConnection connection) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
@ -54,8 +49,8 @@ public class Controller {
|
||||
|
||||
public void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (!Device.isScreenOn()) {
|
||||
device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||
if (!device.isScreenOn()) {
|
||||
injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||
|
||||
// dirty hack
|
||||
// After POWER is injected, the device is powered on asynchronously.
|
||||
@ -81,7 +76,7 @@ public class Controller {
|
||||
switch (msg.getType()) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
@ -105,43 +100,33 @@ public class Controller {
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
device.expandNotificationPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||
Device.collapsePanels();
|
||||
device.collapsePanels();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
String clipboardText = Device.getClipboardText();
|
||||
if (clipboardText != null) {
|
||||
sender.pushClipboardText(clipboardText);
|
||||
}
|
||||
String clipboardText = device.getClipboardText();
|
||||
sender.pushClipboardText(clipboardText);
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste());
|
||||
device.setClipboardText(msg.getText());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
if (device.supportsInputEvents()) {
|
||||
int mode = msg.getAction();
|
||||
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
|
||||
if (setPowerModeOk) {
|
||||
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||
}
|
||||
device.setScreenPowerMode(msg.getAction());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice();
|
||||
device.rotateDevice();
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||
schedulePowerModeOff();
|
||||
}
|
||||
return device.injectKeyEvent(action, keycode, repeat, metaState);
|
||||
private boolean injectKeycode(int action, int keycode, int metaState) {
|
||||
return injectKeyEvent(action, keycode, 0, metaState);
|
||||
}
|
||||
|
||||
private boolean injectChar(char c) {
|
||||
@ -152,7 +137,7 @@ public class Controller {
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!device.injectEvent(event)) {
|
||||
if (!injectEvent(event)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -176,7 +161,7 @@ public class Controller {
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
if (point == null) {
|
||||
Ln.w("Ignore touch event, it was generated for a different device size");
|
||||
// ignore event
|
||||
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
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, source,
|
||||
0);
|
||||
return device.injectEvent(event);
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||
@ -235,41 +216,26 @@ public class Controller {
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||
return device.injectEvent(event);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a call to set power mode to off after a small delay.
|
||||
*/
|
||||
private static void schedulePowerModeOff() {
|
||||
EXECUTOR.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
}
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
InputDevice.SOURCE_KEYBOARD);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int keyCode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||
}
|
||||
|
||||
private boolean injectEvent(InputEvent event) {
|
||||
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean pressBackOrTurnScreenOn() {
|
||||
int keycode = Device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
||||
if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) {
|
||||
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;
|
||||
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
||||
return injectKeycode(keycode);
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,31 @@
|
||||
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.ServiceManager;
|
||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.os.RemoteException;
|
||||
import android.view.IRotationWatcher;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyCharacterMap;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class Device {
|
||||
|
||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||
|
||||
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
||||
|
||||
public interface RotationListener {
|
||||
void onRotationChanged(int rotation);
|
||||
}
|
||||
|
||||
public interface ClipboardListener {
|
||||
void onClipboardTextChanged(String text);
|
||||
}
|
||||
private final ServiceManager serviceManager = new ServiceManager();
|
||||
|
||||
private ScreenInfo screenInfo;
|
||||
private RotationListener rotationListener;
|
||||
private ClipboardListener clipboardListener;
|
||||
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Logical display identifier
|
||||
@ -54,9 +41,9 @@ public final class Device {
|
||||
|
||||
public Device(Options options) {
|
||||
displayId = options.getDisplayId();
|
||||
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId);
|
||||
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds();
|
||||
int[] displayIds = serviceManager.getDisplayManager().getDisplayIds();
|
||||
throw new InvalidDisplayIdException(displayId, displayIds);
|
||||
}
|
||||
|
||||
@ -65,9 +52,9 @@ public final class Device {
|
||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
|
||||
layerStack = displayInfo.getLayerStack();
|
||||
|
||||
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||
registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
public void onRotationChanged(int rotation) throws RemoteException {
|
||||
synchronized (Device.this) {
|
||||
screenInfo = screenInfo.withDeviceRotation(rotation);
|
||||
|
||||
@ -79,32 +66,6 @@ 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) {
|
||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||
}
|
||||
@ -157,7 +118,7 @@ public final class Device {
|
||||
return supportsInputEvents;
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent inputEvent, int mode) {
|
||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
||||
if (!supportsInputEvents()) {
|
||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||
}
|
||||
@ -166,94 +127,64 @@ public final class Device {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode);
|
||||
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent event) {
|
||||
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
public boolean isScreenOn() {
|
||||
return serviceManager.getPowerManager().isScreenOn();
|
||||
}
|
||||
|
||||
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||
InputDevice.SOURCE_KEYBOARD);
|
||||
return injectEvent(event);
|
||||
}
|
||||
|
||||
public boolean injectKeycode(int keyCode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||
}
|
||||
|
||||
public static boolean isScreenOn() {
|
||||
return SERVICE_MANAGER.getPowerManager().isScreenOn();
|
||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
|
||||
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
||||
}
|
||||
|
||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||
this.rotationListener = rotationListener;
|
||||
}
|
||||
|
||||
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
|
||||
this.clipboardListener = clipboardListener;
|
||||
public void expandNotificationPanel() {
|
||||
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
||||
}
|
||||
|
||||
public static void expandNotificationPanel() {
|
||||
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
|
||||
public void collapsePanels() {
|
||||
serviceManager.getStatusBarManager().collapsePanels();
|
||||
}
|
||||
|
||||
public static void collapsePanels() {
|
||||
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
|
||||
}
|
||||
|
||||
public static String getClipboardText() {
|
||||
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
return null;
|
||||
}
|
||||
CharSequence s = clipboardManager.getText();
|
||||
public String getClipboardText() {
|
||||
CharSequence s = serviceManager.getClipboardManager().getText();
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
public boolean setClipboardText(String text) {
|
||||
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
|
||||
if (clipboardManager == null) {
|
||||
return false;
|
||||
public void setClipboardText(String text) {
|
||||
boolean ok = serviceManager.getClipboardManager().setText(text);
|
||||
if (ok) {
|
||||
Ln.i("Device clipboard set");
|
||||
}
|
||||
|
||||
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();
|
||||
if (d == null) {
|
||||
Ln.e("Could not get built-in display");
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
|
||||
if (ok) {
|
||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||
}
|
||||
return SurfaceControl.setDisplayPowerMode(d, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
||||
*/
|
||||
public static void rotateDevice() {
|
||||
WindowManager wm = SERVICE_MANAGER.getWindowManager();
|
||||
public void rotateDevice() {
|
||||
WindowManager wm = serviceManager.getWindowManager();
|
||||
|
||||
boolean accelerometerRotation = !wm.isRotationFrozen();
|
||||
|
||||
@ -270,7 +201,7 @@ public final class Device {
|
||||
}
|
||||
}
|
||||
|
||||
public static ContentProvider createSettingsProvider() {
|
||||
return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
|
||||
public ContentProvider createSettingsProvider() {
|
||||
return serviceManager.getActivityManager().createSettingsProvider();
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,10 @@ import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DeviceMessageWriter {
|
||||
|
||||
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
|
||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
||||
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);
|
||||
|
||||
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
||||
@ -21,7 +21,7 @@ public class DeviceMessageWriter {
|
||||
String text = msg.getText();
|
||||
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
buffer.putInt(len);
|
||||
buffer.putShort((short) len);
|
||||
buffer.put(raw, 0, len);
|
||||
output.write(rawBuffer, 0, buffer.position());
|
||||
break;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -15,25 +15,14 @@ public final class Ln {
|
||||
DEBUG, INFO, WARN, ERROR
|
||||
}
|
||||
|
||||
private static Level threshold = Level.INFO;
|
||||
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
||||
|
||||
private Ln() {
|
||||
// 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) {
|
||||
return level.ordinal() >= threshold.ordinal();
|
||||
return level.ordinal() >= THRESHOLD.ordinal();
|
||||
}
|
||||
|
||||
public static void d(String message) {
|
||||
|
@ -3,7 +3,6 @@ package com.genymobile.scrcpy;
|
||||
import android.graphics.Rect;
|
||||
|
||||
public class Options {
|
||||
private Ln.Level logLevel;
|
||||
private int maxSize;
|
||||
private int bitRate;
|
||||
private int maxFps;
|
||||
@ -15,16 +14,6 @@ public class Options {
|
||||
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() {
|
||||
return maxSize;
|
||||
@ -113,20 +102,4 @@ public class Options {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.IBinder;
|
||||
import android.view.Surface;
|
||||
@ -13,9 +12,6 @@ import android.view.Surface;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
@ -29,19 +25,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
private String encoderName;
|
||||
private List<CodecOption> codecOptions;
|
||||
private int bitRate;
|
||||
private int maxFps;
|
||||
private boolean sendFrameMeta;
|
||||
private long ptsOrigin;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName) {
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
this.bitRate = bitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
this.encoderName = encoderName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -69,12 +61,12 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
}
|
||||
|
||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||
MediaFormat format = createFormat(bitRate, maxFps);
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
MediaCodec codec = createCodec();
|
||||
IBinder display = createDisplay();
|
||||
ScreenInfo screenInfo = device.getScreenInfo();
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
@ -155,50 +147,11 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
IO.writeFully(fd, headerBuffer);
|
||||
}
|
||||
|
||||
private static MediaCodecInfo[] listEncoders() {
|
||||
List<MediaCodecInfo> result = new ArrayList<>();
|
||||
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() throws IOException {
|
||||
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
}
|
||||
|
||||
private static MediaCodec createCodec(String encoderName) throws IOException {
|
||||
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) {
|
||||
private static MediaFormat createFormat(int bitRate, int maxFps) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
@ -214,13 +167,6 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
||||
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||
}
|
||||
|
||||
if (codecOptions != null) {
|
||||
for (CodecOption option : codecOptions) {
|
||||
setCodecOption(format, option);
|
||||
}
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,10 @@ import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class Server {
|
||||
|
||||
@ -22,12 +19,11 @@ public final class Server {
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||
final Device device = new Device(options);
|
||||
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
|
||||
|
||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||
int restoreStayOn = -1;
|
||||
if (options.getShowTouches() || options.getStayAwake()) {
|
||||
try (ContentProvider settings = Device.createSettingsProvider()) {
|
||||
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
|
||||
@ -50,29 +46,18 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true);
|
||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
||||
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
||||
|
||||
Thread controllerThread = null;
|
||||
Thread deviceMessageSenderThread = null;
|
||||
if (options.getControl()) {
|
||||
final Controller controller = new Controller(device, connection);
|
||||
Controller controller = new Controller(device, connection);
|
||||
|
||||
// asynchronous
|
||||
controllerThread = startController(controller);
|
||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||
|
||||
device.setClipboardListener(new Device.ClipboardListener() {
|
||||
@Override
|
||||
public void onClipboardTextChanged(String text) {
|
||||
controller.getSender().pushClipboardText(text);
|
||||
}
|
||||
});
|
||||
startController(controller);
|
||||
startDeviceMessageSender(controller.getSender());
|
||||
}
|
||||
|
||||
try {
|
||||
@ -81,19 +66,12 @@ public final class Server {
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
if (controllerThread != null) {
|
||||
controllerThread.interrupt();
|
||||
}
|
||||
if (deviceMessageSenderThread != null) {
|
||||
deviceMessageSenderThread.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
private static void startController(final Controller controller) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
@ -103,13 +81,11 @@ public final class Server {
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
private static void startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
@ -119,9 +95,7 @@ public final class Server {
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static Options createOptions(String... args) {
|
||||
@ -135,56 +109,47 @@ public final class Server {
|
||||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||
}
|
||||
|
||||
final int expectedParameters = 15;
|
||||
final int expectedParameters = 12;
|
||||
if (args.length != expectedParameters) {
|
||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
||||
}
|
||||
|
||||
Options options = new Options();
|
||||
|
||||
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
|
||||
options.setLogLevel(level);
|
||||
|
||||
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
|
||||
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
|
||||
int bitRate = Integer.parseInt(args[3]);
|
||||
int bitRate = Integer.parseInt(args[2]);
|
||||
options.setBitRate(bitRate);
|
||||
|
||||
int maxFps = Integer.parseInt(args[4]);
|
||||
int maxFps = Integer.parseInt(args[3]);
|
||||
options.setMaxFps(maxFps);
|
||||
|
||||
int lockedVideoOrientation = Integer.parseInt(args[5]);
|
||||
int lockedVideoOrientation = Integer.parseInt(args[4]);
|
||||
options.setLockedVideoOrientation(lockedVideoOrientation);
|
||||
|
||||
// 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);
|
||||
|
||||
Rect crop = parseCrop(args[7]);
|
||||
Rect crop = parseCrop(args[6]);
|
||||
options.setCrop(crop);
|
||||
|
||||
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
|
||||
boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
|
||||
options.setSendFrameMeta(sendFrameMeta);
|
||||
|
||||
boolean control = Boolean.parseBoolean(args[9]);
|
||||
boolean control = Boolean.parseBoolean(args[8]);
|
||||
options.setControl(control);
|
||||
|
||||
int displayId = Integer.parseInt(args[10]);
|
||||
int displayId = Integer.parseInt(args[9]);
|
||||
options.setDisplayId(displayId);
|
||||
|
||||
boolean showTouches = Boolean.parseBoolean(args[11]);
|
||||
boolean showTouches = Boolean.parseBoolean(args[10]);
|
||||
options.setShowTouches(showTouches);
|
||||
|
||||
boolean stayAwake = Boolean.parseBoolean(args[12]);
|
||||
boolean stayAwake = Boolean.parseBoolean(args[11]);
|
||||
options.setStayAwake(stayAwake);
|
||||
|
||||
String codecOptions = args[13];
|
||||
options.setCodecOptions(codecOptions);
|
||||
|
||||
String encoderName = "-".equals(args[14]) ? null : args[14];
|
||||
options.setEncoderName(encoderName);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -224,15 +189,6 @@ public final class Server {
|
||||
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() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -246,9 +202,6 @@ public final class Server {
|
||||
});
|
||||
|
||||
Options options = createOptions(args);
|
||||
|
||||
Ln.initLogLevel(options.getLogLevel());
|
||||
|
||||
scrcpy(options);
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ public final class Workarounds {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static void prepareMainLooper() {
|
||||
// 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()"
|
||||
|
@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
|
||||
@ -14,7 +13,6 @@ public class ClipboardManager {
|
||||
private final IInterface manager;
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
|
||||
public ClipboardManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
@ -83,37 +81,4 @@ public class ClipboardManager {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ public class ContentProvider implements Closeable {
|
||||
private final IBinder token;
|
||||
|
||||
private Method callMethod;
|
||||
private int callMethodVersion;
|
||||
private boolean callMethodLegacy;
|
||||
|
||||
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
|
||||
this.manager = manager;
|
||||
@ -46,20 +46,12 @@ public class ContentProvider implements Closeable {
|
||||
|
||||
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;
|
||||
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
|
||||
} 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;
|
||||
}
|
||||
// old version
|
||||
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
|
||||
callMethodLegacy = true;
|
||||
}
|
||||
}
|
||||
return callMethod;
|
||||
@ -69,16 +61,10 @@ public class ContentProvider implements Closeable {
|
||||
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;
|
||||
if (!callMethodLegacy) {
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||
} else {
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
||||
}
|
||||
return (Bundle) method.invoke(provider, args);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
|
@ -77,14 +77,7 @@ public final class ServiceManager {
|
||||
|
||||
public ClipboardManager getClipboardManager() {
|
||||
if (clipboardManager == null) {
|
||||
IInterface clipboard = getService("clipboard", "android.content.IClipboard");
|
||||
if (clipboard == null) {
|
||||
// Some devices have no clipboard manager
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1440>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/1556>
|
||||
return null;
|
||||
}
|
||||
clipboardManager = new ClipboardManager(clipboard);
|
||||
clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard"));
|
||||
}
|
||||
return clipboardManager;
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ public class ControlMessageReaderTest {
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(5); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@ -38,7 +37,6 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(5, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@ -50,7 +48,7 @@ public class ControlMessageReaderTest {
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeInt(text.length);
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@ -70,7 +68,7 @@ public class ControlMessageReaderTest {
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
|
||||
Arrays.fill(text, (byte) 'a');
|
||||
dos.writeInt(text.length);
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@ -218,9 +216,8 @@ public class ControlMessageReaderTest {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||
dos.writeByte(1); // paste
|
||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||
dos.writeInt(text.length);
|
||||
dos.writeShort(text.length);
|
||||
dos.write(text);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
@ -230,33 +227,6 @@ public class ControlMessageReaderTest {
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||
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
|
||||
@ -306,13 +276,11 @@ public class ControlMessageReaderTest {
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(0); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(1); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
@ -322,14 +290,12 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(0, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
|
||||
event = reader.next();
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(1, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
|
||||
@ -343,7 +309,6 @@ public class ControlMessageReaderTest {
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
dos.writeByte(KeyEvent.ACTION_UP);
|
||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||
dos.writeInt(4); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
|
||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||
@ -356,7 +321,6 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||
Assert.assertEquals(4, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
|
||||
event = reader.next();
|
||||
@ -364,7 +328,6 @@ public class ControlMessageReaderTest {
|
||||
|
||||
bos.reset();
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(5); // repeat
|
||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||
packet = bos.toByteArray();
|
||||
reader.readFrom(new ByteArrayInputStream(packet));
|
||||
@ -374,7 +337,6 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||
Assert.assertEquals(5, event.getRepeat());
|
||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ public class DeviceMessageWriterTest {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
||||
dos.writeInt(data.length);
|
||||
dos.writeShort(data.length);
|
||||
dos.write(data);
|
||||
|
||||
byte[] expected = bos.toByteArray();
|
||||
|
Reference in New Issue
Block a user