Compare commits
36 Commits
rawkeyeven
...
windows_ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b829b0d5c | ||
|
|
c57da7c79e | ||
|
|
878ffffc36 | ||
|
|
f0361fc8b3 | ||
|
|
b5d4ec61fc | ||
|
|
3ada5c51bc | ||
|
|
09c55b0f93 | ||
|
|
682a691173 | ||
|
|
ddb9396743 | ||
|
|
cabcbc2b15 | ||
|
|
80fe12a95f | ||
|
|
099c546580 | ||
|
|
dca2c5f94f | ||
|
|
90cf956f57 | ||
|
|
36c8778d2d | ||
|
|
ae90ef22db | ||
|
|
d80bc25eba | ||
|
|
daa06abd34 | ||
|
|
94702a4309 | ||
|
|
65fbec9643 | ||
|
|
a208400133 | ||
|
|
ab00210b37 | ||
|
|
64a04b8d4a | ||
|
|
86c91e183d | ||
|
|
cb8713eb1f | ||
|
|
003e738106 | ||
|
|
30c79f2d25 | ||
|
|
b25b674c45 | ||
|
|
e2b3968c66 | ||
|
|
bfcb9d06c3 | ||
|
|
dc19ae334d | ||
|
|
cbe73b0bc3 | ||
|
|
bf97a46b0c | ||
|
|
01ab503c22 | ||
|
|
57fb08e443 | ||
|
|
02ae0db6cd |
10
BUILD.md
10
BUILD.md
@@ -15,7 +15,7 @@ First, you need to install the required packages:
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libusb-1.0-0 libusb-dev
|
||||
libusb-1.0-0 libusb-1.0-0-dev
|
||||
```
|
||||
|
||||
Then clone the repo and execute the installation script
|
||||
@@ -94,7 +94,7 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
|
||||
# client build dependencies
|
||||
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libusb-dev
|
||||
libusb-1.0-0-dev
|
||||
|
||||
# server build dependencies
|
||||
sudo apt install openjdk-11-jdk
|
||||
@@ -270,10 +270,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v1.20`][direct-scrcpy-server]
|
||||
_(SHA-256: b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34)_
|
||||
- [`scrcpy-server-v1.21`][direct-scrcpy-server]
|
||||
_(SHA-256: dbcccab523ee26796e55ea33652649e4b7af498edae9aa75e4d4d7869c0ab848)_
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-server-v1.21
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
19
README.md
19
README.md
@@ -1,4 +1,4 @@
|
||||
# scrcpy (v1.20)
|
||||
# scrcpy (v1.21)
|
||||
|
||||
<img src="data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -65,7 +65,7 @@ Build from sources: [BUILD] ([simplified process][BUILD_simple])
|
||||
|
||||
### Linux
|
||||
|
||||
On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04):
|
||||
On Debian and Ubuntu:
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
@@ -101,10 +101,10 @@ process][BUILD_simple]).
|
||||
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
||||
(including `adb`) is available:
|
||||
|
||||
- [`scrcpy-win64-v1.20.zip`][direct-win64]
|
||||
_(SHA-256: 548532b616288bcaeceff6881ad5e6f0928e5ae2b48c380385f03627401cfdba)_
|
||||
- [`scrcpy-win64-v1.21.zip`][direct-win64]
|
||||
_(SHA-256: fdab0c1421353b592a9bbcebd6e252675eadccca65cca8105686feaa9c1ded53)_
|
||||
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-win64-v1.20.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-win64-v1.21.zip
|
||||
|
||||
It is also available in [Chocolatey]:
|
||||
|
||||
@@ -359,7 +359,8 @@ scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink
|
||||
#### TCP/IP (wireless)
|
||||
|
||||
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
||||
device over TCP/IP.
|
||||
device over TCP/IP. The device must be connected on the same network as the
|
||||
computer.
|
||||
|
||||
##### Automatic
|
||||
|
||||
@@ -374,8 +375,8 @@ scrcpy --tcpip=192.168.1.1 # default port is 5555
|
||||
scrcpy --tcpip=192.168.1.1:5555
|
||||
```
|
||||
|
||||
If the device TCP/IP mode is disabled (or if you don't know the IP address),
|
||||
connect the device over USB, then run:
|
||||
If adb TCP/IP mode is disabled on the device (or if you don't know the IP
|
||||
address), connect the device over USB, then run:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip # without arguments
|
||||
@@ -1048,7 +1049,7 @@ This README is available in other languages:
|
||||
- [한국어 (Korean, `ko`) - v1.11](README.ko.md)
|
||||
- [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md)
|
||||
- [Español (Spanish, `sp`) - v1.17](README.sp.md)
|
||||
- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md)
|
||||
- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.20](README.zh-Hans.md)
|
||||
- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md)
|
||||
- [Turkish (Turkish, `tr`) - v1.18](README.tr.md)
|
||||
|
||||
|
||||
@@ -2,27 +2,41 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
只有原版的[README](README.md)会保持最新。
|
||||
|
||||
本文根据[ed130e05]进行翻译。
|
||||
Current version is based on [65b023a]
|
||||
|
||||
[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md
|
||||
本文根据[65b023a]进行翻译。
|
||||
|
||||
# scrcpy (v1.17)
|
||||
[65b023a]: https://github.com/Genymobile/scrcpy/blob/65b023ac6d586593193fd5290f65e25603b68e02/README.md
|
||||
|
||||
# scrcpy (v1.20)
|
||||
|
||||
<img src="data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。
|
||||
|
||||

|
||||
|
||||
它专注于:
|
||||
本应用专注于:
|
||||
|
||||
- **轻量** (原生,仅显示设备屏幕)
|
||||
- **性能** (30~60fps)
|
||||
- **质量** (分辨率可达 1920×1080 或更高)
|
||||
- **低延迟** ([35~70ms][lowlatency])
|
||||
- **快速启动** (最快 1 秒内即可显示第一帧)
|
||||
- **无侵入性** (不会在设备上遗留任何程序)
|
||||
- **轻量**: 原生,仅显示设备屏幕
|
||||
- **性能**: 30~120fps,取决于设备
|
||||
- **质量**: 分辨率可达 1920×1080 或更高
|
||||
- **低延迟**: [35~70ms][lowlatency]
|
||||
- **快速启动**: 最快 1 秒内即可显示第一帧
|
||||
- **无侵入性**: 不会在设备上遗留任何程序
|
||||
- **用户利益**: 无需帐号,无广告,无需联网
|
||||
- **自由**: 自由和开源软件
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
功能:
|
||||
- [屏幕录制](#屏幕录制)
|
||||
- 镜像时[关闭设备屏幕](#关闭设备屏幕)
|
||||
- 双向[复制粘贴](#复制粘贴)
|
||||
- [可配置显示质量](#采集设置)
|
||||
- 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux)
|
||||
- [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux)
|
||||
- 更多 ……
|
||||
|
||||
## 系统要求
|
||||
|
||||
@@ -41,6 +55,17 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
<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: `apt install scrcpy`
|
||||
- Windows: [下载][direct-win64]
|
||||
- macOS: `brew install scrcpy`
|
||||
|
||||
从源代码编译: [构建][BUILD] ([简化过程][BUILD_simple])
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
[BUILD_simple]: BUILD.md#simple
|
||||
|
||||
### Linux
|
||||
|
||||
在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上:
|
||||
@@ -70,13 +95,12 @@ apt install scrcpy
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
您也可以[自行构建][BUILD] (不必担心,这并不困难)。
|
||||
|
||||
您也可以[自行构建][BUILD] ([简化过程][BUILD_simple])。
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
|
||||
在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
@@ -114,13 +138,17 @@ 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
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
或者通过 [MacPorts],该方法同时设置好 adb:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
您也可以[自行构建][BUILD]。
|
||||
|
||||
|
||||
@@ -140,7 +168,7 @@ scrcpy --help
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 捕获设置
|
||||
### 采集设置
|
||||
|
||||
#### 降低分辨率
|
||||
|
||||
@@ -158,7 +186,7 @@ scrcpy -m 1024 # 简写
|
||||
|
||||
#### 修改码率
|
||||
|
||||
默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps):
|
||||
默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
@@ -167,7 +195,7 @@ scrcpy -b 2M # 简写
|
||||
|
||||
#### 限制帧率
|
||||
|
||||
要限制捕获的帧率:
|
||||
要限制采集的帧率:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
@@ -194,10 +222,11 @@ scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素
|
||||
要锁定镜像画面的方向:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation 0 # 自然方向
|
||||
scrcpy --lock-video-orientation 1 # 逆时针旋转 90°
|
||||
scrcpy --lock-video-orientation 2 # 180°
|
||||
scrcpy --lock-video-orientation 3 # 顺时针旋转 90°
|
||||
scrcpy --lock-video-orientation # 初始(目前)方向
|
||||
scrcpy --lock-video-orientation=0 # 自然方向
|
||||
scrcpy --lock-video-orientation=1 # 逆时针旋转 90°
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 顺时针旋转 90°
|
||||
```
|
||||
|
||||
只影响录制的方向。
|
||||
@@ -219,7 +248,9 @@ scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### 屏幕录制
|
||||
### 采集
|
||||
|
||||
#### 屏幕录制
|
||||
|
||||
可以在镜像的同时录制视频:
|
||||
|
||||
@@ -241,6 +272,75 @@ scrcpy -Nr file.mkv
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
在 Linux 上,可以将视频流发送至 v4l2 回环 (loopback) 设备,因此可以使用任何 v4l2 工具像摄像头一样打开安卓设备。
|
||||
|
||||
需安装 `v4l2loopback` 模块:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
创建一个 v4l2 设备:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
这样会在 `/dev/videoN` 创建一个新的视频设备,其中 `N` 是整数。 ([更多选项](https://github.com/umlaeute/v4l2loopback#options) 可以用来创建多个设备或者特定 ID 的设备)。
|
||||
|
||||
列出已启用的设备:
|
||||
|
||||
```bash
|
||||
# 需要 v4l-utils 包
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# 简单但或许足够
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
使用一个 v4l2 漏开启 scrcpy:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # 禁用窗口镜像
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # 简写
|
||||
```
|
||||
|
||||
(将 `N` 替换为设备 ID,使用 `ls /dev/video*` 命令查看)
|
||||
|
||||
启用之后,可以使用 v4l2 工具打开视频流:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC 可能存在一些缓冲延迟
|
||||
```
|
||||
|
||||
例如,可以在 [OBS] 中采集视频。
|
||||
|
||||
[OBS]: https://obsproject.com/
|
||||
|
||||
|
||||
#### 缓冲
|
||||
|
||||
可以加入缓冲,会增加延迟,但可以减少抖动 (见 [#2464])。
|
||||
|
||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
||||
|
||||
对于显示缓冲:
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # 为显示增加 50 毫秒的缓冲
|
||||
```
|
||||
|
||||
对于 V4L2 漏:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲
|
||||
```
|
||||
|
||||
|
||||
### 连接
|
||||
|
||||
#### 无线
|
||||
@@ -249,16 +349,17 @@ _Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接
|
||||
|
||||
1. 将设备和电脑连接至同一 Wi-Fi。
|
||||
2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. 启用设备的网络 adb 功能 `adb tcpip 5555`。
|
||||
3. 启用设备的网络 adb 功能: `adb tcpip 5555`。
|
||||
4. 断开设备的 USB 连接。
|
||||
5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_.
|
||||
5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_。
|
||||
6. 正常运行 `scrcpy`。
|
||||
|
||||
可能需要降低码率和分辨率:
|
||||
可能降低码率和分辨率会更好一些:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
@@ -327,7 +428,7 @@ scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
类似无线网络连接,可能需要降低画面质量:
|
||||
类似地,对于无线连接,可能需要降低画面质量:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
@@ -353,7 +454,7 @@ scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
|
||||
#### 无边框
|
||||
|
||||
关闭边框:
|
||||
禁用窗口边框:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
@@ -369,7 +470,7 @@ scrcpy --always-on-top
|
||||
|
||||
#### 全屏
|
||||
|
||||
您可以通过如下命令直接全屏启动scrcpy:
|
||||
您可以通过如下命令直接全屏启动 scrcpy:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
@@ -394,7 +495,7 @@ scrcpy --rotation 1
|
||||
|
||||
也可以使用 <kbd>MOD</kbd>+<kbd>←</kbd> _(左箭头)_ 和 <kbd>MOD</kbd>+<kbd>→</kbd> _(右箭头)_ 随时更改。
|
||||
|
||||
需要注意的是, _scrcpy_ 有三个不同的方向:
|
||||
需要注意的是, _scrcpy_ 中有三类旋转方向:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。
|
||||
- [`--lock-video-orientation`](#锁定屏幕方向) 改变镜像的朝向 (设备传输到电脑的画面的朝向)。这会影响录制。
|
||||
- `--rotation` (或 <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>) 只旋转窗口的内容。这只影响显示,不影响录制。
|
||||
@@ -404,7 +505,7 @@ scrcpy --rotation 1
|
||||
|
||||
#### 只读
|
||||
|
||||
禁用电脑对设备的控制 (如键盘输入、鼠标事件和文件拖放):
|
||||
禁用电脑对设备的控制 (任何可与设备交互的方式:如键盘输入、鼠标事件和文件拖放):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
@@ -430,14 +531,14 @@ adb shell dumpsys display # 在输出中搜索 “mDisplayId=”
|
||||
|
||||
#### 保持常亮
|
||||
|
||||
阻止设备在连接时休眠:
|
||||
阻止设备在连接时一段时间后休眠:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
程序关闭时会恢复设备原来的设置。
|
||||
scrcpy 关闭时会恢复设备原来的设置。
|
||||
|
||||
|
||||
#### 关闭设备屏幕
|
||||
@@ -451,7 +552,7 @@ scrcpy -S
|
||||
|
||||
或者在任何时候按 <kbd>MOD</kbd>+<kbd>o</kbd>。
|
||||
|
||||
要重新打开屏幕,按下 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
要重新打开屏幕,按下 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>。
|
||||
|
||||
在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 <kbd>MOD</kbd>+<kbd>p</kbd>),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。
|
||||
|
||||
@@ -462,20 +563,17 @@ scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
#### 退出时息屏
|
||||
|
||||
#### 渲染过期帧
|
||||
|
||||
默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。
|
||||
|
||||
强制渲染所有帧 (可能导致延迟变高):
|
||||
scrcpy 退出时关闭设备屏幕:
|
||||
|
||||
```bash
|
||||
scrcpy --render-expired-frames
|
||||
scrcpy --power-off-on-close
|
||||
```
|
||||
|
||||
#### 显示触摸
|
||||
|
||||
在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。
|
||||
在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。
|
||||
|
||||
Android 在 _开发者选项_ 中提供了这项功能。
|
||||
|
||||
@@ -538,10 +636,32 @@ scrcpy --disable-screensaver
|
||||
|
||||
更准确的说,在按住鼠标左键时按住 <kbd>Ctrl</kbd>。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。
|
||||
|
||||
实际上,_scrcpy_ 会在以屏幕中心对称的位置上生成由“虚拟手指”发出的额外触摸事件。
|
||||
实际上,_scrcpy_ 会在关于屏幕中心对称的位置上用“虚拟手指”发出触摸事件。
|
||||
|
||||
#### 物理键盘模拟 (HID)
|
||||
|
||||
#### 文字注入偏好
|
||||
默认情况下,scrcpy 使用安卓按键或文本注入,这在任何情况都可以使用,但仅限于ASCII字符。
|
||||
|
||||
在 Linux 上,scrcpy 可以模拟为 Android 上的物理 USB 键盘,以提供更好地输入体验 (使用 [USB HID over AOAv2][hid-aoav2]):禁用虚拟键盘,并适用于任何字符和输入法。
|
||||
|
||||
[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
|
||||
|
||||
不过,这种方法仅支持 USB 连接以及 Linux平台。
|
||||
|
||||
启用 HID 模式:
|
||||
|
||||
```bash
|
||||
scrcpy --hid-keyboard
|
||||
scrcpy -K # 简写
|
||||
```
|
||||
|
||||
如果失败了 (如设备未通过 USB 连接),则自动回退至默认模式 (终端中会输出日志)。这即允许通过 USB 和 TCP/IP 连接时使用相同的命令行参数。
|
||||
|
||||
在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。
|
||||
|
||||
[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
|
||||
|
||||
#### 文本注入偏好
|
||||
|
||||
打字的时候,系统会产生两种[事件][textevents]:
|
||||
- _按键事件_ ,代表一个按键被按下或松开。
|
||||
@@ -557,13 +677,15 @@ scrcpy --prefer-text
|
||||
|
||||
(这会导致键盘在游戏中工作不正常)
|
||||
|
||||
该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### 按键重复
|
||||
|
||||
默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这可能会导致性能问题。
|
||||
默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这通常没有实际用途,且可能会导致性能问题。
|
||||
|
||||
避免转发重复按键事件:
|
||||
|
||||
@@ -571,10 +693,11 @@ scrcpy --prefer-text
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。
|
||||
|
||||
#### 右键和中键
|
||||
|
||||
默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
|
||||
默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
@@ -587,27 +710,27 @@ scrcpy --forward-all-clicks
|
||||
|
||||
将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。
|
||||
|
||||
该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。
|
||||
不会有视觉反馈,终端会输出一条日志。
|
||||
|
||||
|
||||
#### 将文件推送至设备
|
||||
|
||||
要推送文件到设备的 `/sdcard/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
|
||||
要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
|
||||
|
||||
该操作没有可见的响应,只会在控制台输出日志。
|
||||
不会有视觉反馈,终端会输出一条日志。
|
||||
|
||||
在启动时可以修改目标目录:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
|
||||
### 音频转发
|
||||
|
||||
_Scrcpy_ 不支持音频。请使用 [sndcpy].
|
||||
_Scrcpy_ 不支持音频。请使用 [sndcpy]。
|
||||
|
||||
另外请阅读 [issue #14]。
|
||||
另见 [issue #14]。
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
@@ -632,36 +755,46 @@ _<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>+_按住并移动鼠标_ |
|
||||
| 操作 | 快捷键
|
||||
| --------------------------------- | :-------------------------------------------
|
||||
| 全屏 | <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> \| _第4键³_
|
||||
| 点按 `菜单` (解锁屏幕) | <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> \| _第5键³_
|
||||
| 展开设置面板 | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _双击第5键³_
|
||||
| 收起通知面板 | <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>+_按住并移动鼠标_
|
||||
| 拖放 APK 文件 | 从电脑安装 APK 文件
|
||||
| 拖放非 APK 文件 | [将文件推送至设备](#push-file-to-device)
|
||||
|
||||
_¹双击黑边可以去除黑边_
|
||||
_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
|
||||
_³需要安卓版本 Android >= 7。_
|
||||
_¹双击黑边可以去除黑边。_
|
||||
_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
|
||||
_³鼠标的第4键和第5键。_
|
||||
_⁴需要安卓版本 Android >= 7。_
|
||||
|
||||
有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”:
|
||||
|
||||
1. 按下 <kbd>MOD</kbd> 不放。
|
||||
2. 双击 <kbd>n</kbd>。
|
||||
3. 松开 <kbd>MOD</kbd>。
|
||||
|
||||
所有的 <kbd>Ctrl</kbd>+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。
|
||||
|
||||
@@ -670,18 +803,20 @@ _³需要安卓版本 Android >= 7。_
|
||||
|
||||
要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
```bash
|
||||
ADB=/path/to/adb scrcpy
|
||||
```
|
||||
|
||||
要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。
|
||||
|
||||
|
||||
## 为什么叫 _scrcpy_ ?
|
||||
|
||||
一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。
|
||||
|
||||
[`strcpy`] 复制一个 **str**ing; `scrcpy` 复制一个 **scr**een。
|
||||
[`strcpy`] 复制一个 **str**ing (字符串); `scrcpy` 复制一个 **scr**een (屏幕)。
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
@@ -689,14 +824,12 @@ _³需要安卓版本 Android >= 7。_
|
||||
|
||||
## 如何构建?
|
||||
|
||||
请查看[BUILD]。
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
请查看 [BUILD]。
|
||||
|
||||
|
||||
## 常见问题
|
||||
|
||||
请查看[FAQ](FAQ.md)。
|
||||
请查看 [FAQ](FAQ.md)。
|
||||
|
||||
|
||||
## 开发者
|
||||
|
||||
@@ -49,9 +49,11 @@ conf.set('_XOPEN_SOURCE', '700')
|
||||
conf.set('_GNU_SOURCE', true)
|
||||
|
||||
if host_machine.system() == 'windows'
|
||||
windows = import('windows')
|
||||
src += [
|
||||
'src/sys/win/file.c',
|
||||
'src/sys/win/process.c',
|
||||
windows.compile_resources('scrcpy-windows.rc'),
|
||||
]
|
||||
conf.set('_WIN32_WINNT', '0x0600')
|
||||
conf.set('WINVER', '0x0600')
|
||||
@@ -84,10 +86,10 @@ if not get_option('crossbuild_windows')
|
||||
|
||||
# native build
|
||||
dependencies = [
|
||||
dependency('libavformat'),
|
||||
dependency('libavcodec'),
|
||||
dependency('libavformat', version: '>= 57.33'),
|
||||
dependency('libavcodec', version: '>= 57.37'),
|
||||
dependency('libavutil'),
|
||||
dependency('sdl2'),
|
||||
dependency('sdl2', version: '>= 2.0.5'),
|
||||
]
|
||||
|
||||
if v4l2_support
|
||||
|
||||
20
app/scrcpy-windows.rc
Normal file
20
app/scrcpy-windows.rc
Normal file
@@ -0,0 +1,20 @@
|
||||
0 ICON "../data/icon.ico"
|
||||
1 VERSIONINFO
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904E4"
|
||||
BEGIN
|
||||
VALUE "FileDescription", "Display and control your Android device"
|
||||
VALUE "InternalName", "scrcpy"
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "1.21"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
@@ -413,11 +413,15 @@ Push file to device (see \fB\-\-push\-target\fR)
|
||||
|
||||
.TP
|
||||
.B ADB
|
||||
Specify the path to adb.
|
||||
Path to adb.
|
||||
|
||||
.TP
|
||||
.B SCRCPY_ICON_PATH
|
||||
Path to the program icon.
|
||||
|
||||
.TP
|
||||
.B SCRCPY_SERVER_PATH
|
||||
Specify the path to server binary.
|
||||
Path to the server binary.
|
||||
|
||||
|
||||
.SH AUTHORS
|
||||
|
||||
@@ -373,7 +373,7 @@ bool
|
||||
sc_aoa_start(struct sc_aoa *aoa) {
|
||||
LOGD("Starting AOA thread");
|
||||
|
||||
bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "aoa_thread", aoa);
|
||||
bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "scrcpy-aoa", aoa);
|
||||
if (!ok) {
|
||||
LOGC("Could not start AOA thread");
|
||||
return false;
|
||||
|
||||
@@ -71,6 +71,11 @@ struct sc_shortcut {
|
||||
const char *text;
|
||||
};
|
||||
|
||||
struct sc_envvar {
|
||||
const char *name;
|
||||
const char *text;
|
||||
};
|
||||
|
||||
struct sc_getopt_adapter {
|
||||
char *optstring;
|
||||
struct option *longopts;
|
||||
@@ -585,6 +590,21 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
},
|
||||
};
|
||||
|
||||
static const struct sc_envvar envvars[] = {
|
||||
{
|
||||
.name = "ADB",
|
||||
.text = "Path to adb executable",
|
||||
},
|
||||
{
|
||||
.name = "SCRCPY_ICON_PATH",
|
||||
.text = "Path to the program icon",
|
||||
},
|
||||
{
|
||||
.name = "SCRCPY_SERVER_PATH",
|
||||
.text = "Path to the server binary",
|
||||
}
|
||||
};
|
||||
|
||||
static char *
|
||||
sc_getopt_adapter_create_optstring(void) {
|
||||
struct sc_strbuf buf;
|
||||
@@ -678,7 +698,7 @@ sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) {
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
static void
|
||||
sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) {
|
||||
@@ -776,7 +796,7 @@ print_shortcuts_intro(unsigned cols) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf("%s\n", intro);
|
||||
printf("\n%s\n", intro);
|
||||
free(intro);
|
||||
}
|
||||
|
||||
@@ -804,6 +824,23 @@ print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) {
|
||||
free(text);
|
||||
}
|
||||
|
||||
static void
|
||||
print_envvar(const struct sc_envvar *envvar, unsigned cols) {
|
||||
assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
|
||||
assert(envvar->name);
|
||||
assert(envvar->text);
|
||||
|
||||
printf("\n %s\n", envvar->name);
|
||||
char *text = sc_str_wrap_lines(envvar->text, cols, 8);
|
||||
if (!text) {
|
||||
printf("<ERROR>\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("%s\n", text);
|
||||
free(text);
|
||||
}
|
||||
|
||||
void
|
||||
scrcpy_print_usage(const char *arg0) {
|
||||
#define SC_TERM_COLS_DEFAULT 80
|
||||
@@ -831,11 +868,17 @@ scrcpy_print_usage(const char *arg0) {
|
||||
}
|
||||
|
||||
// Print shortcuts section
|
||||
printf("\nShortcuts:\n\n");
|
||||
printf("\nShortcuts:\n");
|
||||
print_shortcuts_intro(cols);
|
||||
for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) {
|
||||
print_shortcut(&shortcuts[i], cols);
|
||||
}
|
||||
|
||||
// Print environment variables section
|
||||
printf("\nEnvironment variables:\n");
|
||||
for (size_t i = 0; i < ARRAY_LEN(envvars); ++i) {
|
||||
print_envvar(&envvars[i], cols);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
|
||||
@@ -35,15 +35,6 @@
|
||||
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 5)
|
||||
// <https://wiki.libsdl.org/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH>
|
||||
# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
|
||||
// <https://wiki.libsdl.org/SDL_GetDisplayUsableBounds>
|
||||
# define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS
|
||||
// <https://wiki.libsdl.org/SDL_WindowFlags>
|
||||
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 6)
|
||||
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
|
||||
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
||||
|
||||
@@ -41,7 +41,7 @@ static const char *const android_motionevent_action_labels[] = {
|
||||
"pointer-up",
|
||||
"hover-move",
|
||||
"scroll",
|
||||
"hover-enter"
|
||||
"hover-enter",
|
||||
"hover-exit",
|
||||
"btn-press",
|
||||
"btn-release",
|
||||
@@ -55,6 +55,12 @@ static const char *const screen_power_mode_labels[] = {
|
||||
"suspend",
|
||||
};
|
||||
|
||||
static const char *const copy_key_labels[] = {
|
||||
"none",
|
||||
"copy",
|
||||
"cut",
|
||||
};
|
||||
|
||||
static void
|
||||
write_position(uint8_t *buf, const struct sc_position *position) {
|
||||
buffer_write32be(&buf[0], position->point.x);
|
||||
@@ -63,7 +69,7 @@ write_position(uint8_t *buf, const struct sc_position *position) {
|
||||
buffer_write16be(&buf[10], position->screen_size.height);
|
||||
}
|
||||
|
||||
// write length (2 bytes) + string (non nul-terminated)
|
||||
// write length (4 bytes) + string (non null-terminated)
|
||||
static size_t
|
||||
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
|
||||
size_t len = sc_str_utf8_truncation_index(utf8, max_len);
|
||||
@@ -117,6 +123,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
||||
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
|
||||
buf[1] = msg->inject_keycode.action;
|
||||
return 2;
|
||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||
buf[1] = msg->get_clipboard.copy_key;
|
||||
return 2;
|
||||
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
|
||||
buffer_write64be(&buf[1], msg->set_clipboard.sequence);
|
||||
buf[9] = !!msg->set_clipboard.paste;
|
||||
@@ -131,7 +140,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
||||
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||
// no additional data
|
||||
return 1;
|
||||
@@ -194,10 +202,14 @@ control_msg_log(const struct control_msg *msg) {
|
||||
LOG_CMSG("back-or-screen-on %s",
|
||||
KEYEVENT_ACTION_LABEL(msg->inject_keycode.action));
|
||||
break;
|
||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||
LOG_CMSG("get clipboard copy_key=%s",
|
||||
copy_key_labels[msg->get_clipboard.copy_key]);
|
||||
break;
|
||||
case CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
||||
LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"",
|
||||
msg->set_clipboard.sequence,
|
||||
msg->set_clipboard.paste ? "paste" : "copy",
|
||||
msg->set_clipboard.paste ? "paste" : "nopaste",
|
||||
msg->set_clipboard.text);
|
||||
break;
|
||||
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||
@@ -213,9 +225,6 @@ control_msg_log(const struct control_msg *msg) {
|
||||
case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
||||
LOG_CMSG("collapse panels");
|
||||
break;
|
||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||
LOG_CMSG("get clipboard");
|
||||
break;
|
||||
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||
LOG_CMSG("rotate device");
|
||||
break;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
#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)
|
||||
// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
|
||||
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 14)
|
||||
|
||||
#define POINTER_ID_MOUSE UINT64_C(-1)
|
||||
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2)
|
||||
@@ -41,6 +41,12 @@ enum screen_power_mode {
|
||||
SCREEN_POWER_MODE_NORMAL = 2,
|
||||
};
|
||||
|
||||
enum get_clipboard_copy_key {
|
||||
GET_CLIPBOARD_COPY_KEY_NONE,
|
||||
GET_CLIPBOARD_COPY_KEY_COPY,
|
||||
GET_CLIPBOARD_COPY_KEY_CUT,
|
||||
};
|
||||
|
||||
struct control_msg {
|
||||
enum control_msg_type type;
|
||||
union {
|
||||
@@ -69,6 +75,9 @@ struct control_msg {
|
||||
enum android_keyevent_action action; // action for the BACK key
|
||||
// screen may only be turned on on ACTION_DOWN
|
||||
} back_or_screen_on;
|
||||
struct {
|
||||
enum get_clipboard_copy_key copy_key;
|
||||
} get_clipboard;
|
||||
struct {
|
||||
uint64_t sequence;
|
||||
char *text; // owned, to be freed by free()
|
||||
|
||||
@@ -110,7 +110,7 @@ controller_start(struct controller *controller) {
|
||||
LOGD("Starting controller thread");
|
||||
|
||||
bool ok = sc_thread_create(&controller->thread, run_controller,
|
||||
"controller", controller);
|
||||
"scrcpy-ctl", controller);
|
||||
if (!ok) {
|
||||
LOGC("Could not start controller thread");
|
||||
return false;
|
||||
|
||||
@@ -46,6 +46,8 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Could not open codec");
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
|
||||
@@ -154,7 +154,7 @@ file_handler_start(struct file_handler *file_handler) {
|
||||
LOGD("Starting file_handler thread");
|
||||
|
||||
bool ok = sc_thread_create(&file_handler->thread, run_file_handler,
|
||||
"file_handler", file_handler);
|
||||
"scrcpy-file", file_handler);
|
||||
if (!ok) {
|
||||
LOGC("Could not start file_handler thread");
|
||||
return false;
|
||||
|
||||
@@ -108,7 +108,7 @@ fps_counter_start(struct fps_counter *counter) {
|
||||
// same thread, no need to lock
|
||||
if (!counter->thread_started) {
|
||||
bool ok = sc_thread_create(&counter->thread, run_fps_counter,
|
||||
"fps counter", counter);
|
||||
"scrcpy-fps", counter);
|
||||
if (!ok) {
|
||||
LOGE("Could not start FPS counter thread");
|
||||
return false;
|
||||
|
||||
@@ -148,16 +148,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
|
||||
// If the screen is off, it is turned on only on ACTION_DOWN
|
||||
static void
|
||||
@@ -211,6 +201,21 @@ collapse_panels(struct controller *controller) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
get_device_clipboard(struct controller *controller,
|
||||
enum get_clipboard_copy_key copy_key) {
|
||||
struct control_msg msg;
|
||||
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
|
||||
msg.get_clipboard.copy_key = copy_key;
|
||||
|
||||
if (!controller_push_msg(controller, &msg)) {
|
||||
LOGW("Could not request 'get device clipboard'");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
set_device_clipboard(struct controller *controller, bool paste,
|
||||
uint64_t sequence) {
|
||||
@@ -450,13 +455,15 @@ input_manager_process_key(struct input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_c:
|
||||
if (control && !shift && !repeat) {
|
||||
action_copy(controller, action);
|
||||
if (control && !shift && !repeat && down) {
|
||||
get_device_clipboard(controller,
|
||||
GET_CLIPBOARD_COPY_KEY_COPY);
|
||||
}
|
||||
return;
|
||||
case SDLK_x:
|
||||
if (control && !shift && !repeat) {
|
||||
action_cut(controller, action);
|
||||
if (control && !shift && !repeat && down) {
|
||||
get_device_clipboard(controller,
|
||||
GET_CLIPBOARD_COPY_KEY_CUT);
|
||||
}
|
||||
return;
|
||||
case SDLK_v:
|
||||
|
||||
@@ -111,8 +111,8 @@ bool
|
||||
receiver_start(struct receiver *receiver) {
|
||||
LOGD("Starting receiver thread");
|
||||
|
||||
bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver",
|
||||
receiver);
|
||||
bool ok = sc_thread_create(&receiver->thread, run_receiver,
|
||||
"scrcpy-receiver", receiver);
|
||||
if (!ok) {
|
||||
LOGC("Could not start receiver thread");
|
||||
return false;
|
||||
|
||||
@@ -287,8 +287,8 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
||||
}
|
||||
|
||||
LOGD("Starting recorder thread");
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "recorder",
|
||||
recorder);
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
|
||||
recorder);
|
||||
if (!ok) {
|
||||
LOGC("Could not start recorder thread");
|
||||
goto error_avio_close;
|
||||
|
||||
@@ -94,12 +94,10 @@ sdl_set_hints(const char *render_driver) {
|
||||
LOGW("Could not enable linear filtering");
|
||||
}
|
||||
|
||||
#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
|
||||
// Handle a click to gain focus as any other click
|
||||
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
|
||||
LOGW("Could not enable mouse focus clickthrough");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
||||
// Disable synthetic mouse events from touch events
|
||||
|
||||
@@ -64,12 +64,7 @@ set_window_size(struct screen *screen, struct sc_size new_size) {
|
||||
static bool
|
||||
get_preferred_display_bounds(struct sc_size *bounds) {
|
||||
SDL_Rect rect;
|
||||
#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS
|
||||
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r))
|
||||
#else
|
||||
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r))
|
||||
#endif
|
||||
if (GET_DISPLAY_BOUNDS(0, &rect)) {
|
||||
if (SDL_GetDisplayUsableBounds(0, &rect)) {
|
||||
LOGW("Could not get display usable bounds: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
@@ -394,12 +389,7 @@ screen_init(struct screen *screen, const struct screen_params *params) {
|
||||
| SDL_WINDOW_RESIZABLE
|
||||
| SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
if (params->always_on_top) {
|
||||
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
|
||||
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
|
||||
#else
|
||||
LOGW("The 'always on top' flag is not available "
|
||||
"(compile with SDL >= 2.0.5 to enable it)");
|
||||
#endif
|
||||
}
|
||||
if (params->window_borderless) {
|
||||
window_flags |= SDL_WINDOW_BORDERLESS;
|
||||
|
||||
@@ -388,6 +388,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
assert(tunnel->enabled);
|
||||
|
||||
const char *serial = server->params.serial;
|
||||
bool control = server->params.control;
|
||||
|
||||
sc_socket video_socket = SC_SOCKET_NONE;
|
||||
sc_socket control_socket = SC_SOCKET_NONE;
|
||||
@@ -397,9 +398,12 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
control_socket = net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
if (control_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
if (control) {
|
||||
control_socket =
|
||||
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
if (control_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uint32_t tunnel_host = server->params.tunnel_host;
|
||||
@@ -420,15 +424,18 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
// we know that the device is listening, we don't need several attempts
|
||||
control_socket = net_socket();
|
||||
if (control_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
bool ok = net_connect_intr(&server->intr, control_socket, tunnel_host,
|
||||
tunnel_port);
|
||||
if (!ok) {
|
||||
goto fail;
|
||||
if (control) {
|
||||
// we know that the device is listening, we don't need several
|
||||
// attempts
|
||||
control_socket = net_socket();
|
||||
if (control_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
bool ok = net_connect_intr(&server->intr, control_socket,
|
||||
tunnel_host, tunnel_port);
|
||||
if (!ok) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +449,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
}
|
||||
|
||||
assert(video_socket != SC_SOCKET_NONE);
|
||||
assert(control_socket != SC_SOCKET_NONE);
|
||||
assert(!control || control_socket != SC_SOCKET_NONE);
|
||||
|
||||
server->video_socket = video_socket;
|
||||
server->control_socket = control_socket;
|
||||
@@ -756,6 +763,17 @@ run_server(void *data) {
|
||||
}
|
||||
sc_mutex_unlock(&server->mutex);
|
||||
|
||||
// Interrupt sockets to wake up socket blocking calls on the server
|
||||
assert(server->video_socket != SC_SOCKET_NONE);
|
||||
net_interrupt(server->video_socket);
|
||||
net_close(server->video_socket);
|
||||
|
||||
if (server->control_socket != SC_SOCKET_NONE) {
|
||||
// There is no control_socket if --no-control is set
|
||||
net_interrupt(server->control_socket);
|
||||
net_close(server->control_socket);
|
||||
}
|
||||
|
||||
// Give some delay for the server to terminate properly
|
||||
#define WATCHDOG_DELAY SC_TICK_FROM_SEC(1)
|
||||
sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY;
|
||||
@@ -786,7 +804,8 @@ error_connection_failed:
|
||||
|
||||
bool
|
||||
sc_server_start(struct sc_server *server) {
|
||||
bool ok = sc_thread_create(&server->thread, run_server, "server", server);
|
||||
bool ok =
|
||||
sc_thread_create(&server->thread, run_server, "scrcpy-server", server);
|
||||
if (!ok) {
|
||||
LOGE("Could not create server thread");
|
||||
return false;
|
||||
|
||||
@@ -284,7 +284,8 @@ bool
|
||||
stream_start(struct stream *stream) {
|
||||
LOGD("Starting stream thread");
|
||||
|
||||
bool ok = sc_thread_create(&stream->thread, run_stream, "stream", stream);
|
||||
bool ok =
|
||||
sc_thread_create(&stream->thread, run_stream, "scrcpy-stream", stream);
|
||||
if (!ok) {
|
||||
LOGC("Could not start stream thread");
|
||||
return false;
|
||||
|
||||
@@ -30,9 +30,7 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
|
||||
bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR);
|
||||
|
||||
// Add 1 per non-NULL pointer
|
||||
unsigned handle_count = !!pin
|
||||
+ (pout || inherit_stdout)
|
||||
+ (perr || inherit_stderr);
|
||||
unsigned handle_count = !!pin || !!pout || !!perr;
|
||||
|
||||
enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC;
|
||||
|
||||
@@ -81,23 +79,29 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
|
||||
si.StartupInfo.cb = sizeof(si);
|
||||
HANDLE handles[3];
|
||||
|
||||
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
if (inherit_stdout) {
|
||||
si.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
}
|
||||
if (inherit_stderr) {
|
||||
si.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
||||
}
|
||||
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL;
|
||||
if (handle_count) {
|
||||
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
|
||||
unsigned i = 0;
|
||||
if (pin) {
|
||||
si.StartupInfo.hStdInput = stdin_read_handle;
|
||||
handles[i++] = si.StartupInfo.hStdInput;
|
||||
}
|
||||
if (pout || inherit_stdout) {
|
||||
si.StartupInfo.hStdOutput = pout ? stdout_write_handle
|
||||
: GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (pout) {
|
||||
assert(!inherit_stdout);
|
||||
si.StartupInfo.hStdOutput = stdout_write_handle;
|
||||
handles[i++] = si.StartupInfo.hStdOutput;
|
||||
}
|
||||
if (perr || inherit_stderr) {
|
||||
si.StartupInfo.hStdError = perr ? stderr_write_handle
|
||||
: GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (perr) {
|
||||
assert(!inherit_stderr);
|
||||
si.StartupInfo.hStdError = stderr_write_handle;
|
||||
handles[i++] = si.StartupInfo.hStdError;
|
||||
}
|
||||
|
||||
@@ -146,15 +150,22 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
|
||||
goto error_free_attribute_list;
|
||||
}
|
||||
|
||||
BOOL bInheritHandles = handle_count > 0;
|
||||
// DETACHED_PROCESS to disable stdin, stdout and stderr
|
||||
DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT
|
||||
: DETACHED_PROCESS;
|
||||
BOOL bInheritHandles = handle_count > 0 || inherit_stdout || inherit_stderr;
|
||||
DWORD dwCreationFlags = 0;
|
||||
if (handle_count > 0) {
|
||||
dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT;
|
||||
}
|
||||
if (!inherit_stdout && !inherit_stderr) {
|
||||
// DETACHED_PROCESS to disable stdin, stdout and stderr
|
||||
dwCreationFlags |= DETACHED_PROCESS;
|
||||
}
|
||||
BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles,
|
||||
dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi);
|
||||
free(wide);
|
||||
if (!ok) {
|
||||
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
|
||||
int err = GetLastError();
|
||||
LOGE("CreateProcessW() error %d", err);
|
||||
if (err == ERROR_FILE_NOT_FOUND) {
|
||||
ret = SC_PROCESS_ERROR_MISSING_BINARY;
|
||||
}
|
||||
goto error_free_attribute_list;
|
||||
|
||||
@@ -64,7 +64,7 @@ sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid,
|
||||
observer->listener_userdata = listener_userdata;
|
||||
observer->terminated = false;
|
||||
|
||||
ok = sc_thread_create(&observer->thread, run_observer, "process_observer",
|
||||
ok = sc_thread_create(&observer->thread, run_observer, "scrcpy-proc",
|
||||
observer);
|
||||
if (!ok) {
|
||||
sc_cond_destroy(&observer->cond_terminated);
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
bool
|
||||
sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
|
||||
void *userdata) {
|
||||
// The thread name length is limited on some systems. Never use a name
|
||||
// longer than 16 bytes (including the final '\0')
|
||||
assert(strlen(name) <= 15);
|
||||
|
||||
SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata);
|
||||
if (!sdl_thread) {
|
||||
LOG_OOM();
|
||||
|
||||
@@ -1,16 +1,55 @@
|
||||
#include "tick.h"
|
||||
|
||||
#include <SDL2/SDL_timer.h>
|
||||
#include <assert.h>
|
||||
#include <time.h>
|
||||
#ifdef _WIN32
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
sc_tick
|
||||
sc_tick_now(void) {
|
||||
// SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed
|
||||
// in microseconds to store PTS without precision loss.
|
||||
//
|
||||
// As an alternative, SDL_GetPerformanceCounter() and
|
||||
// SDL_GetPerformanceFrequency() could be used, but:
|
||||
// - the conversions (avoiding overflow) are expansive, since the
|
||||
// frequency is not known at compile time;
|
||||
// - in practice, we don't need more precision for now.
|
||||
return (sc_tick) SDL_GetTicks() * 1000;
|
||||
#ifndef _WIN32
|
||||
// Maximum sc_tick precision (microsecond)
|
||||
struct timespec ts;
|
||||
int ret = clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
if (ret) {
|
||||
abort();
|
||||
}
|
||||
|
||||
return SC_TICK_FROM_SEC(ts.tv_sec) + SC_TICK_FROM_NS(ts.tv_nsec);
|
||||
#else
|
||||
LARGE_INTEGER c;
|
||||
|
||||
// On systems that run Windows XP or later, the function will always
|
||||
// succeed and will thus never return zero.
|
||||
// <https://docs.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter>
|
||||
// <https://docs.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency>
|
||||
|
||||
BOOL ok = QueryPerformanceCounter(&c);
|
||||
assert(ok);
|
||||
(void) ok;
|
||||
|
||||
LONGLONG counter = c.QuadPart;
|
||||
|
||||
static LONGLONG frequency;
|
||||
if (!frequency) {
|
||||
// Initialize on first call
|
||||
LARGE_INTEGER f;
|
||||
ok = QueryPerformanceFrequency(&f);
|
||||
assert(ok);
|
||||
frequency = f.QuadPart;
|
||||
assert(frequency);
|
||||
}
|
||||
|
||||
if (frequency % SC_TICK_FREQ == 0) {
|
||||
// Expected case (typically frequency = 10000000, i.e. 100ns precision)
|
||||
sc_tick div = frequency / SC_TICK_FREQ;
|
||||
return SC_TICK_FROM_US(counter / div);
|
||||
}
|
||||
|
||||
// Split the division to avoid overflow
|
||||
sc_tick secs = SC_TICK_FROM_SEC(counter / frequency);
|
||||
sc_tick subsec = SC_TICK_FREQ * (counter % frequency) / frequency;
|
||||
return secs + subsec;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -10,9 +10,11 @@ typedef int64_t sc_tick;
|
||||
#define SC_TICK_FREQ 1000000 // microsecond
|
||||
|
||||
// To be adapted if SC_TICK_FREQ changes
|
||||
#define SC_TICK_TO_NS(tick) ((tick) * 1000)
|
||||
#define SC_TICK_TO_US(tick) (tick)
|
||||
#define SC_TICK_TO_MS(tick) ((tick) / 1000)
|
||||
#define SC_TICK_TO_SEC(tick) ((tick) / 1000000)
|
||||
#define SC_TICK_FROM_NS(ns) ((ns) / 1000)
|
||||
#define SC_TICK_FROM_US(us) (us)
|
||||
#define SC_TICK_FROM_MS(ms) ((ms) * 1000)
|
||||
#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000)
|
||||
|
||||
@@ -272,7 +272,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
|
||||
vs->stopped = false;
|
||||
|
||||
LOGD("Starting v4l2 thread");
|
||||
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs);
|
||||
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "scrcpy-v4l2", vs);
|
||||
if (!ok) {
|
||||
LOGC("Could not start v4l2 thread");
|
||||
goto error_av_packet_free;
|
||||
|
||||
@@ -170,7 +170,7 @@ bool
|
||||
sc_video_buffer_start(struct sc_video_buffer *vb) {
|
||||
if (vb->buffering_time) {
|
||||
bool ok =
|
||||
sc_thread_create(&vb->b.thread, run_buffering, "buffering", vb);
|
||||
sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb);
|
||||
if (!ok) {
|
||||
LOGE("Could not start buffering thread");
|
||||
return false;
|
||||
|
||||
@@ -169,4 +169,4 @@ int main(int argc, char *argv[]) {
|
||||
test_options2();
|
||||
test_parse_shortcut_mods();
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ static void test_serialize_inject_text_long(void) {
|
||||
struct control_msg msg;
|
||||
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
|
||||
char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1];
|
||||
memset(text, 'a', sizeof(text));
|
||||
memset(text, 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
|
||||
msg.inject_text.text = text;
|
||||
|
||||
@@ -210,14 +210,18 @@ static void test_serialize_collapse_panels(void) {
|
||||
static void test_serialize_get_clipboard(void) {
|
||||
struct control_msg msg = {
|
||||
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||
.get_clipboard = {
|
||||
.copy_key = GET_CLIPBOARD_COPY_KEY_COPY,
|
||||
},
|
||||
};
|
||||
|
||||
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = control_msg_serialize(&msg, buf);
|
||||
assert(size == 1);
|
||||
assert(size == 2);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||
GET_CLIPBOARD_COPY_KEY_COPY,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
@@ -246,6 +250,40 @@ static void test_serialize_set_clipboard(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_set_clipboard_long(void) {
|
||||
struct control_msg msg = {
|
||||
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||
.set_clipboard = {
|
||||
.sequence = UINT64_C(0x0102030405060708),
|
||||
.paste = true,
|
||||
.text = NULL,
|
||||
},
|
||||
};
|
||||
|
||||
char text[CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1];
|
||||
memset(text, 'a', CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
text[CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0';
|
||||
msg.set_clipboard.text = text;
|
||||
|
||||
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = control_msg_serialize(&msg, buf);
|
||||
assert(size == CONTROL_MSG_MAX_SIZE);
|
||||
|
||||
unsigned char expected[CONTROL_MSG_MAX_SIZE] = {
|
||||
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
|
||||
1, // paste
|
||||
// text length
|
||||
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24,
|
||||
(CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff,
|
||||
(CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff,
|
||||
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff,
|
||||
};
|
||||
memset(expected + 14, 'a', CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
||||
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_set_screen_power_mode(void) {
|
||||
struct control_msg msg = {
|
||||
.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
||||
@@ -295,6 +333,7 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_collapse_panels();
|
||||
test_serialize_get_clipboard();
|
||||
test_serialize_set_clipboard();
|
||||
test_serialize_set_clipboard_long();
|
||||
test_serialize_set_screen_power_mode();
|
||||
test_serialize_rotate_device();
|
||||
return 0;
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -7,6 +7,7 @@ cpp = 'i686-w64-mingw32-g++'
|
||||
ar = 'i686-w64-mingw32-ar'
|
||||
strip = 'i686-w64-mingw32-strip'
|
||||
pkgconfig = 'i686-w64-mingw32-pkg-config'
|
||||
windres = 'i686-w64-mingw32-windres'
|
||||
|
||||
[host_machine]
|
||||
system = 'windows'
|
||||
|
||||
@@ -7,6 +7,7 @@ cpp = 'x86_64-w64-mingw32-g++'
|
||||
ar = 'x86_64-w64-mingw32-ar'
|
||||
strip = 'x86_64-w64-mingw32-strip'
|
||||
pkgconfig = 'x86_64-w64-mingw32-pkg-config'
|
||||
windres = 'x86_64-w64-mingw32-windres'
|
||||
|
||||
[host_machine]
|
||||
system = 'windows'
|
||||
|
||||
BIN
data/icon.ico
Normal file
BIN
data/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20
|
||||
PREBUILT_SERVER_SHA256=b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.21/scrcpy-server-v1.21
|
||||
PREBUILT_SERVER_SHA256=dbcccab523ee26796e55ea33652649e4b7af498edae9aa75e4d4d7869c0ab848
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '1.20',
|
||||
version: '1.21',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
@@ -6,8 +6,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
versionCode 12000
|
||||
versionName "1.20"
|
||||
versionCode 12100
|
||||
versionName "1.21"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=1.20
|
||||
SCRCPY_VERSION_NAME=1.21
|
||||
|
||||
PLATFORM_VERSION=31
|
||||
PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION}
|
||||
PLATFORM=${ANDROID_PLATFORM:-31}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0}
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
@@ -57,7 +56,7 @@ javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
|
||||
echo "Dexing..."
|
||||
cd "$CLASSES_DIR"
|
||||
|
||||
if [[ $PLATFORM_VERSION -lt 31 ]]
|
||||
if [[ $PLATFORM -lt 31 ]]
|
||||
then
|
||||
# use dx
|
||||
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
|
||||
|
||||
@@ -20,6 +20,10 @@ public final class ControlMessage {
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
public static final int COPY_KEY_NONE = 0;
|
||||
public static final int COPY_KEY_COPY = 1;
|
||||
public static final int COPY_KEY_CUT = 2;
|
||||
|
||||
private int type;
|
||||
private String text;
|
||||
private int metaState; // KeyEvent.META_*
|
||||
@@ -31,6 +35,7 @@ public final class ControlMessage {
|
||||
private Position position;
|
||||
private int hScroll;
|
||||
private int vScroll;
|
||||
private int copyKey;
|
||||
private boolean paste;
|
||||
private int repeat;
|
||||
private long sequence;
|
||||
@@ -82,6 +87,13 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createGetClipboard(int copyKey) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_GET_CLIPBOARD;
|
||||
msg.copyKey = copyKey;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_SET_CLIPBOARD;
|
||||
@@ -151,6 +163,10 @@ public final class ControlMessage {
|
||||
return vScroll;
|
||||
}
|
||||
|
||||
public int getCopyKey() {
|
||||
return copyKey;
|
||||
}
|
||||
|
||||
public boolean getPaste() {
|
||||
return paste;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class ControlMessageReader {
|
||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
static final int GET_CLIPBOARD_LENGTH = 1;
|
||||
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
|
||||
|
||||
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||
@@ -70,6 +71,9 @@ public class ControlMessageReader {
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
msg = parseBackOrScreenOnEvent();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
msg = parseGetClipboard();
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
msg = parseSetClipboard();
|
||||
break;
|
||||
@@ -79,7 +83,6 @@ public class ControlMessageReader {
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
msg = ControlMessage.createEmpty(type);
|
||||
break;
|
||||
@@ -162,6 +165,14 @@ public class ControlMessageReader {
|
||||
return ControlMessage.createBackOrScreenOn(action);
|
||||
}
|
||||
|
||||
private ControlMessage parseGetClipboard() {
|
||||
if (buffer.remaining() < GET_CLIPBOARD_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
int copyKey = toUnsigned(buffer.get());
|
||||
return ControlMessage.createGetClipboard(copyKey);
|
||||
}
|
||||
|
||||
private ControlMessage parseSetClipboard() {
|
||||
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||
return null;
|
||||
|
||||
@@ -21,6 +21,7 @@ public class Controller {
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final DeviceMessageSender sender;
|
||||
private final boolean clipboardAutosync;
|
||||
|
||||
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
|
||||
|
||||
@@ -31,9 +32,10 @@ public class Controller {
|
||||
|
||||
private boolean keepPowerModeOff;
|
||||
|
||||
public Controller(Device device, DesktopConnection connection) {
|
||||
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
this.clipboardAutosync = clipboardAutosync;
|
||||
initPointers();
|
||||
sender = new DeviceMessageSender(connection);
|
||||
}
|
||||
@@ -55,7 +57,7 @@ public class Controller {
|
||||
public void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (!Device.isScreenOn()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
|
||||
// dirty hack
|
||||
// After POWER is injected, the device is powered on asynchronously.
|
||||
@@ -114,18 +116,10 @@ public class Controller {
|
||||
Device.collapsePanels();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
String clipboardText = Device.getClipboardText();
|
||||
if (clipboardText != null) {
|
||||
sender.pushClipboardText(clipboardText);
|
||||
}
|
||||
getClipboard(msg.getCopyKey());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
long sequence = msg.getSequence();
|
||||
setClipboard(msg.getText(), msg.getPaste());
|
||||
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
||||
// Acknowledgement requested
|
||||
sender.pushAckClipboard(sequence);
|
||||
}
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||
if (device.supportsInputEvents()) {
|
||||
@@ -149,7 +143,7 @@ public class Controller {
|
||||
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||
schedulePowerModeOff();
|
||||
}
|
||||
return device.injectKeyEvent(action, keycode, repeat, metaState);
|
||||
return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean injectChar(char c) {
|
||||
@@ -160,7 +154,7 @@ public class Controller {
|
||||
return false;
|
||||
}
|
||||
for (KeyEvent event : events) {
|
||||
if (!device.injectEvent(event)) {
|
||||
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -224,7 +218,7 @@ public class Controller {
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
|
||||
0);
|
||||
return device.injectEvent(event);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||
@@ -247,7 +241,7 @@ public class Controller {
|
||||
MotionEvent event = MotionEvent
|
||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0,
|
||||
InputDevice.SOURCE_MOUSE, 0);
|
||||
return device.injectEvent(event);
|
||||
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,7 +259,7 @@ public class Controller {
|
||||
|
||||
private boolean pressBackOrTurnScreenOn(int action) {
|
||||
if (Device.isScreenOn()) {
|
||||
return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0);
|
||||
return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
// Screen is off
|
||||
@@ -278,10 +272,29 @@ public class Controller {
|
||||
if (keepPowerModeOff) {
|
||||
schedulePowerModeOff();
|
||||
}
|
||||
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
|
||||
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
private boolean setClipboard(String text, boolean paste) {
|
||||
private void getClipboard(int copyKey) {
|
||||
// On Android >= 7, press the COPY or CUT key if requested
|
||||
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
|
||||
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
|
||||
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
|
||||
}
|
||||
|
||||
// If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in
|
||||
// particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than
|
||||
// copying an old clipboard content.
|
||||
if (!clipboardAutosync) {
|
||||
String clipboardText = Device.getClipboardText();
|
||||
if (clipboardText != null) {
|
||||
sender.pushClipboardText(clipboardText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setClipboard(String text, boolean paste, long sequence) {
|
||||
boolean ok = device.setClipboardText(text);
|
||||
if (ok) {
|
||||
Ln.i("Device clipboard set");
|
||||
@@ -289,7 +302,12 @@ public class Controller {
|
||||
|
||||
// On Android >= 7, also press the PASTE key if requested
|
||||
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE);
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
if (sequence != ControlMessage.SEQUENCE_INVALID) {
|
||||
// Acknowledgement requested
|
||||
sender.pushAckClipboard(sequence);
|
||||
}
|
||||
|
||||
return ok;
|
||||
|
||||
@@ -30,8 +30,13 @@ public final class DesktopConnection implements Closeable {
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
||||
this.videoSocket = videoSocket;
|
||||
this.controlSocket = controlSocket;
|
||||
controlInputStream = controlSocket.getInputStream();
|
||||
controlOutputStream = controlSocket.getOutputStream();
|
||||
if (controlSocket != null) {
|
||||
controlInputStream = controlSocket.getInputStream();
|
||||
controlOutputStream = controlSocket.getOutputStream();
|
||||
} else {
|
||||
controlInputStream = null;
|
||||
controlOutputStream = null;
|
||||
}
|
||||
videoFd = videoSocket.getFileDescriptor();
|
||||
}
|
||||
|
||||
@@ -41,31 +46,35 @@ public final class DesktopConnection implements Closeable {
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
|
||||
public static DesktopConnection open(Device device, boolean tunnelForward, boolean control) throws IOException {
|
||||
LocalSocket videoSocket;
|
||||
LocalSocket controlSocket;
|
||||
LocalSocket controlSocket = null;
|
||||
if (tunnelForward) {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||
try {
|
||||
videoSocket = localServerSocket.accept();
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
if (control) {
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(SOCKET_NAME);
|
||||
try {
|
||||
controlSocket = connect(SOCKET_NAME);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
if (control) {
|
||||
try {
|
||||
controlSocket = connect(SOCKET_NAME);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +88,11 @@ public final class DesktopConnection implements Closeable {
|
||||
videoSocket.shutdownInput();
|
||||
videoSocket.shutdownOutput();
|
||||
videoSocket.close();
|
||||
controlSocket.shutdownInput();
|
||||
controlSocket.shutdownOutput();
|
||||
controlSocket.close();
|
||||
if (controlSocket != null) {
|
||||
controlSocket.shutdownInput();
|
||||
controlSocket.shutdownOutput();
|
||||
controlSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void send(String deviceName, int width, int height) throws IOException {
|
||||
|
||||
@@ -24,6 +24,10 @@ 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;
|
||||
|
||||
public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
|
||||
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
|
||||
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
|
||||
|
||||
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||
|
||||
@@ -164,7 +168,7 @@ public final class Device {
|
||||
return supportsInputEvents;
|
||||
}
|
||||
|
||||
public static boolean injectEvent(InputEvent inputEvent, int displayId) {
|
||||
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
|
||||
if (!supportsInputEvents(displayId)) {
|
||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||
}
|
||||
@@ -173,30 +177,31 @@ public final class Device {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode);
|
||||
}
|
||||
|
||||
public boolean injectEvent(InputEvent event) {
|
||||
return injectEvent(event, displayId);
|
||||
public boolean injectEvent(InputEvent event, int injectMode) {
|
||||
return injectEvent(event, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) {
|
||||
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
|
||||
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, displayId);
|
||||
return injectEvent(event, displayId, injectMode);
|
||||
}
|
||||
|
||||
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||
return injectKeyEvent(action, keyCode, repeat, metaState, displayId);
|
||||
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
|
||||
return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean pressReleaseKeycode(int keyCode, int displayId) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId);
|
||||
public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) {
|
||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode)
|
||||
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
|
||||
}
|
||||
|
||||
public boolean pressReleaseKeycode(int keyCode) {
|
||||
return pressReleaseKeycode(keyCode, displayId);
|
||||
public boolean pressReleaseKeycode(int keyCode, int injectMode) {
|
||||
return pressReleaseKeycode(keyCode, displayId, injectMode);
|
||||
}
|
||||
|
||||
public static boolean isScreenOn() {
|
||||
@@ -272,7 +277,7 @@ public final class Device {
|
||||
if (!isScreenOn()) {
|
||||
return true;
|
||||
}
|
||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId);
|
||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,15 +66,16 @@ public final class Server {
|
||||
Thread initThread = startInitThread(options);
|
||||
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
boolean control = options.getControl();
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward, control)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName());
|
||||
|
||||
Thread controllerThread = null;
|
||||
Thread deviceMessageSenderThread = null;
|
||||
if (options.getControl()) {
|
||||
final Controller controller = new Controller(device, connection);
|
||||
if (control) {
|
||||
final Controller controller = new Controller(device, connection, options.getClipboardAutosync());
|
||||
|
||||
// asynchronous
|
||||
controllerThread = startController(controller);
|
||||
|
||||
@@ -219,6 +219,7 @@ public class ControlMessageReaderTest {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
|
||||
dos.writeByte(ControlMessage.COPY_KEY_COPY);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@@ -226,6 +227,7 @@ public class ControlMessageReaderTest {
|
||||
ControlMessage event = reader.next();
|
||||
|
||||
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
|
||||
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user