Compare commits
19 commits
master
...
adb_serial
Author | SHA1 | Date | |
---|---|---|---|
e512b5bd69 | |||
b7328a75e3 | |||
7516c0d1c5 | |||
6b4761820f | |||
e83562a6fe | |||
921044e8a4 | |||
b49f4e795e | |||
3eb8202adf | |||
83f66ea06c | |||
3cc0921e61 | |||
d8358ce857 | |||
4bc5baeeb5 | |||
0672f7e405 | |||
6d6a436563 | |||
9cb14b5166 | |||
9cb8766220 | |||
fd4ec784e0 | |||
52cebe1597 | |||
6a27062f48 |
4
BUILD.md
4
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-1.0-0-dev
|
||||
libusb-1.0-0 libusb-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-1.0-0-dev
|
||||
libusb-dev
|
||||
|
||||
# server build dependencies
|
||||
sudo apt install openjdk-11-jdk
|
||||
|
|
|
@ -955,7 +955,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.20](README.zh-Hans.md)
|
||||
- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md)
|
||||
- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md)
|
||||
- [Turkish (Turkish, `tr`) - v1.18](README.tr.md)
|
||||
|
||||
|
|
|
@ -2,41 +2,27 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._
|
|||
|
||||
只有原版的[README](README.md)会保持最新。
|
||||
|
||||
Current version is based on [65b023a]
|
||||
本文根据[ed130e05]进行翻译。
|
||||
|
||||
本文根据[65b023a]进行翻译。
|
||||
[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md
|
||||
|
||||
[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" />
|
||||
# scrcpy (v1.17)
|
||||
|
||||
本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。
|
||||
|
||||
![screenshot](assets/screenshot-debian-600.jpg)
|
||||
|
||||
本应用专注于:
|
||||
它专注于:
|
||||
|
||||
- **轻量**: 原生,仅显示设备屏幕
|
||||
- **性能**: 30~120fps,取决于设备
|
||||
- **质量**: 分辨率可达 1920×1080 或更高
|
||||
- **低延迟**: [35~70ms][lowlatency]
|
||||
- **快速启动**: 最快 1 秒内即可显示第一帧
|
||||
- **无侵入性**: 不会在设备上遗留任何程序
|
||||
- **用户利益**: 无需帐号,无广告,无需联网
|
||||
- **自由**: 自由和开源软件
|
||||
- **轻量** (原生,仅显示设备屏幕)
|
||||
- **性能** (30~60fps)
|
||||
- **质量** (分辨率可达 1920×1080 或更高)
|
||||
- **低延迟** ([35~70ms][lowlatency])
|
||||
- **快速启动** (最快 1 秒内即可显示第一帧)
|
||||
- **无侵入性** (不会在设备上遗留任何程序)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
功能:
|
||||
- [屏幕录制](#屏幕录制)
|
||||
- 镜像时[关闭设备屏幕](#关闭设备屏幕)
|
||||
- 双向[复制粘贴](#复制粘贴)
|
||||
- [可配置显示质量](#采集设置)
|
||||
- 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux)
|
||||
- [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux)
|
||||
- 更多 ……
|
||||
|
||||
## 系统要求
|
||||
|
||||
|
@ -55,17 +41,6 @@ Current version is based on [65b023a]
|
|||
|
||||
<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) 上:
|
||||
|
@ -95,12 +70,13 @@ 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_simple])。
|
||||
您也可以[自行构建][BUILD] (不必担心,这并不困难)。
|
||||
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
|
||||
在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
|
@ -138,17 +114,13 @@ brew install scrcpy
|
|||
你还需要在 `PATH` 内有 `adb`。如果还没有:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
# Homebrew >= 2.6.0
|
||||
brew install --cask android-platform-tools
|
||||
|
||||
# Homebrew < 2.6.0
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
或者通过 [MacPorts],该方法同时设置好 adb:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
您也可以[自行构建][BUILD]。
|
||||
|
||||
|
||||
|
@ -168,7 +140,7 @@ scrcpy --help
|
|||
|
||||
## 功能介绍
|
||||
|
||||
### 采集设置
|
||||
### 捕获设置
|
||||
|
||||
#### 降低分辨率
|
||||
|
||||
|
@ -186,7 +158,7 @@ scrcpy -m 1024 # 简写
|
|||
|
||||
#### 修改码率
|
||||
|
||||
默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps):
|
||||
默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
|
@ -195,7 +167,7 @@ scrcpy -b 2M # 简写
|
|||
|
||||
#### 限制帧率
|
||||
|
||||
要限制采集的帧率:
|
||||
要限制捕获的帧率:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
|
@ -222,11 +194,10 @@ scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素
|
|||
要锁定镜像画面的方向:
|
||||
|
||||
```bash
|
||||
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°
|
||||
scrcpy --lock-video-orientation 0 # 自然方向
|
||||
scrcpy --lock-video-orientation 1 # 逆时针旋转 90°
|
||||
scrcpy --lock-video-orientation 2 # 180°
|
||||
scrcpy --lock-video-orientation 3 # 顺时针旋转 90°
|
||||
```
|
||||
|
||||
只影响录制的方向。
|
||||
|
@ -248,9 +219,7 @@ scrcpy --encoder OMX.qcom.video.encoder.avc
|
|||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### 采集
|
||||
|
||||
#### 屏幕录制
|
||||
### 屏幕录制
|
||||
|
||||
可以在镜像的同时录制视频:
|
||||
|
||||
|
@ -272,75 +241,6 @@ 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 毫秒的缓冲
|
||||
```
|
||||
|
||||
|
||||
### 连接
|
||||
|
||||
#### 无线
|
||||
|
@ -349,17 +249,16 @@ _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
|
||||
|
@ -428,7 +327,7 @@ scrcpy --force-adb-forward
|
|||
```
|
||||
|
||||
|
||||
类似地,对于无线连接,可能需要降低画面质量:
|
||||
类似无线网络连接,可能需要降低画面质量:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
|
@ -454,7 +353,7 @@ scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
|||
|
||||
#### 无边框
|
||||
|
||||
禁用窗口边框:
|
||||
关闭边框:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
|
@ -470,7 +369,7 @@ scrcpy --always-on-top
|
|||
|
||||
#### 全屏
|
||||
|
||||
您可以通过如下命令直接全屏启动 scrcpy:
|
||||
您可以通过如下命令直接全屏启动scrcpy:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
|
@ -495,7 +394,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>) 只旋转窗口的内容。这只影响显示,不影响录制。
|
||||
|
@ -505,7 +404,7 @@ scrcpy --rotation 1
|
|||
|
||||
#### 只读
|
||||
|
||||
禁用电脑对设备的控制 (任何可与设备交互的方式:如键盘输入、鼠标事件和文件拖放):
|
||||
禁用电脑对设备的控制 (如键盘输入、鼠标事件和文件拖放):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
|
@ -531,14 +430,14 @@ adb shell dumpsys display # 在输出中搜索 “mDisplayId=”
|
|||
|
||||
#### 保持常亮
|
||||
|
||||
阻止设备在连接时一段时间后休眠:
|
||||
阻止设备在连接时休眠:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
scrcpy 关闭时会恢复设备原来的设置。
|
||||
程序关闭时会恢复设备原来的设置。
|
||||
|
||||
|
||||
#### 关闭设备屏幕
|
||||
|
@ -552,7 +451,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>),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。
|
||||
|
||||
|
@ -563,17 +462,20 @@ scrcpy --turn-screen-off --stay-awake
|
|||
scrcpy -Sw
|
||||
```
|
||||
|
||||
#### 退出时息屏
|
||||
|
||||
scrcpy 退出时关闭设备屏幕:
|
||||
#### 渲染过期帧
|
||||
|
||||
默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。
|
||||
|
||||
强制渲染所有帧 (可能导致延迟变高):
|
||||
|
||||
```bash
|
||||
scrcpy --power-off-on-close
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
#### 显示触摸
|
||||
|
||||
在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。
|
||||
在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。
|
||||
|
||||
Android 在 _开发者选项_ 中提供了这项功能。
|
||||
|
||||
|
@ -636,32 +538,10 @@ 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]:
|
||||
- _按键事件_ ,代表一个按键被按下或松开。
|
||||
|
@ -677,15 +557,13 @@ 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
|
||||
|
||||
|
||||
#### 按键重复
|
||||
|
||||
默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这通常没有实际用途,且可能会导致性能问题。
|
||||
默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这可能会导致性能问题。
|
||||
|
||||
避免转发重复按键事件:
|
||||
|
||||
|
@ -693,11 +571,10 @@ scrcpy --prefer-text
|
|||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。
|
||||
|
||||
#### 右键和中键
|
||||
|
||||
默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
|
||||
默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
|
@ -710,27 +587,27 @@ scrcpy --forward-all-clicks
|
|||
|
||||
将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。
|
||||
|
||||
不会有视觉反馈,终端会输出一条日志。
|
||||
该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。
|
||||
|
||||
|
||||
#### 将文件推送至设备
|
||||
|
||||
要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
|
||||
要推送文件到设备的 `/sdcard/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
|
||||
|
||||
不会有视觉反馈,终端会输出一条日志。
|
||||
该操作没有可见的响应,只会在控制台输出日志。
|
||||
|
||||
在启动时可以修改目标目录:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
|
||||
### 音频转发
|
||||
|
||||
_Scrcpy_ 不支持音频。请使用 [sndcpy]。
|
||||
_Scrcpy_ 不支持音频。请使用 [sndcpy].
|
||||
|
||||
另见 [issue #14]。
|
||||
另外请阅读 [issue #14]。
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
@ -755,46 +632,36 @@ _<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> \| _第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)
|
||||
| 操作 | 快捷键 |
|
||||
| --------------------------------- | :------------------------------------------- |
|
||||
| 全屏 | <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>+_按住并移动鼠标_ |
|
||||
|
||||
_¹双击黑边可以去除黑边。_
|
||||
_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
|
||||
_³鼠标的第4键和第5键。_
|
||||
_⁴需要安卓版本 Android >= 7。_
|
||||
|
||||
有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”:
|
||||
|
||||
1. 按下 <kbd>MOD</kbd> 不放。
|
||||
2. 双击 <kbd>n</kbd>。
|
||||
3. 松开 <kbd>MOD</kbd>。
|
||||
_¹双击黑边可以去除黑边_
|
||||
_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
|
||||
_³需要安卓版本 Android >= 7。_
|
||||
|
||||
所有的 <kbd>Ctrl</kbd>+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。
|
||||
|
||||
|
@ -803,20 +670,18 @@ _⁴需要安卓版本 Android >= 7。_
|
|||
|
||||
要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`:
|
||||
|
||||
```bash
|
||||
ADB=/path/to/adb scrcpy
|
||||
```
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。
|
||||
|
||||
要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
|
||||
## 为什么叫 _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
|
||||
|
@ -824,12 +689,14 @@ ADB=/path/to/adb scrcpy
|
|||
|
||||
## 如何构建?
|
||||
|
||||
请查看 [BUILD]。
|
||||
请查看[BUILD]。
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
|
||||
## 常见问题
|
||||
|
||||
请查看 [FAQ](FAQ.md)。
|
||||
请查看[FAQ](FAQ.md)。
|
||||
|
||||
|
||||
## 开发者
|
||||
|
|
|
@ -111,7 +111,7 @@ show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
|
|||
|
||||
sc_pid
|
||||
adb_execute_p(const char *serial, const char *const adb_cmd[],
|
||||
size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr) {
|
||||
size_t len, sc_pipe *pout) {
|
||||
int i;
|
||||
sc_pid pid;
|
||||
|
||||
|
@ -132,7 +132,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
|
|||
memcpy(&argv[i], adb_cmd, len * sizeof(const char *));
|
||||
argv[len + i] = NULL;
|
||||
enum sc_process_result r =
|
||||
sc_process_execute_p(argv, &pid, pin, pout, perr);
|
||||
sc_process_execute_p(argv, &pid, NULL, pout, NULL);
|
||||
if (r != SC_PROCESS_SUCCESS) {
|
||||
show_adb_err_msg(r, argv);
|
||||
pid = SC_PROCESS_NONE;
|
||||
|
@ -144,7 +144,7 @@ adb_execute_p(const char *serial, const char *const adb_cmd[],
|
|||
|
||||
sc_pid
|
||||
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
|
||||
return adb_execute_p(serial, adb_cmd, len, NULL, NULL, NULL);
|
||||
return adb_execute_p(serial, adb_cmd, len, NULL);
|
||||
}
|
||||
|
||||
sc_pid
|
||||
|
@ -233,45 +233,8 @@ adb_install(const char *serial, const char *local) {
|
|||
return pid;
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
adb_execute_for_output(const char *serial, const char *const adb_cmd[],
|
||||
size_t adb_cmd_len, char *buf, size_t buf_len,
|
||||
const char *name) {
|
||||
sc_pipe pout;
|
||||
sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL);
|
||||
|
||||
ssize_t r = sc_pipe_read_all(pout, buf, buf_len);
|
||||
sc_pipe_close(pout);
|
||||
|
||||
if (!sc_process_check_success(pid, name, true)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
static size_t
|
||||
truncate_first_line(char *data, size_t len) {
|
||||
data[len - 1] = '\0';
|
||||
char *eol = strpbrk(data, "\r\n");
|
||||
if (eol) {
|
||||
*eol = '\0';
|
||||
len = eol - data;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
char *
|
||||
adb_get_serialno(void) {
|
||||
char buf[128];
|
||||
|
||||
sc_pid
|
||||
adb_get_serialno(sc_pipe *pout) {
|
||||
const char *const adb_cmd[] = {"get-serialno"};
|
||||
ssize_t r = adb_execute_for_output(NULL, adb_cmd, ARRAY_LEN(adb_cmd),
|
||||
buf, sizeof(buf), "get-serialno");
|
||||
if (r <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
truncate_first_line(buf, r);
|
||||
return strdup(buf);
|
||||
return adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), pout);
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ sc_pid
|
|||
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
|
||||
|
||||
sc_pid
|
||||
adb_execute_p(const char *serial, const char *const adb_cmd[],
|
||||
size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr);
|
||||
adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len,
|
||||
sc_pipe *pout);
|
||||
|
||||
sc_pid
|
||||
adb_forward(const char *serial, uint16_t local_port,
|
||||
|
@ -35,8 +35,8 @@ adb_push(const char *serial, const char *local, const char *remote);
|
|||
sc_pid
|
||||
adb_install(const char *serial, const char *local);
|
||||
|
||||
// Return the result of "adb get-serialno".
|
||||
char *
|
||||
adb_get_serialno(void);
|
||||
// Execute "adb get-serialno" and give a pipe to get the result
|
||||
sc_pid
|
||||
adb_get_serialno(sc_pipe *pout);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -394,8 +394,11 @@ scrcpy(struct scrcpy_options *options) {
|
|||
// It is necessarily initialized here, since the device is connected
|
||||
struct sc_server_info *info = &s->server.info;
|
||||
|
||||
const char *serial = s->server.params.serial;
|
||||
assert(serial);
|
||||
|
||||
if (options->display && options->control) {
|
||||
if (!file_handler_init(&s->file_handler, options->serial,
|
||||
if (!file_handler_init(&s->file_handler, serial,
|
||||
options->push_target)) {
|
||||
goto end;
|
||||
}
|
||||
|
@ -516,21 +519,7 @@ scrcpy(struct scrcpy_options *options) {
|
|||
#ifdef HAVE_AOA_HID
|
||||
bool aoa_hid_ok = false;
|
||||
|
||||
char *serialno = NULL;
|
||||
|
||||
const char *serial = options->serial;
|
||||
if (!serial) {
|
||||
serialno = adb_get_serialno();
|
||||
if (!serialno) {
|
||||
LOGE("Could not get device serial");
|
||||
goto aoa_hid_end;
|
||||
}
|
||||
serial = serialno;
|
||||
LOGI("Device serial: %s", serial);
|
||||
}
|
||||
|
||||
bool ok = sc_aoa_init(&s->aoa, serial);
|
||||
free(serialno);
|
||||
if (!ok) {
|
||||
goto aoa_hid_end;
|
||||
}
|
||||
|
|
|
@ -234,6 +234,12 @@ connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) {
|
|||
|
||||
net_close(socket);
|
||||
}
|
||||
|
||||
if (sc_intr_is_interrupted(&server->intr)) {
|
||||
// Stop immediately
|
||||
break;
|
||||
}
|
||||
|
||||
if (attempts) {
|
||||
sc_mutex_lock(&server->mutex);
|
||||
sc_tick deadline = sc_tick_now() + delay;
|
||||
|
@ -416,12 +422,59 @@ sc_server_on_terminated(void *userdata) {
|
|||
LOGD("Server terminated");
|
||||
}
|
||||
|
||||
static char *
|
||||
sc_server_get_serialno(struct sc_intr *intr) {
|
||||
sc_pipe pout;
|
||||
sc_pid pid = adb_get_serialno(&pout);
|
||||
if (pid == SC_PROCESS_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char buf[128];
|
||||
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf));
|
||||
sc_pipe_close(pout);
|
||||
|
||||
bool ok = sc_process_check_success_intr(intr, pid, "adb get-serialno");
|
||||
sc_process_close(pid);
|
||||
if (!ok) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sc_str_truncate(buf, r, " \r\n");
|
||||
|
||||
return strdup(buf);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_server_fill_serial(struct sc_server *server) {
|
||||
// Retrieve the actual device immediately if not provided, so that all
|
||||
// future adb commands are executed for this specific device, even if other
|
||||
// devices are connected afterwards (without "more than one
|
||||
// device/emulator" error)
|
||||
if (!server->params.serial) {
|
||||
// The serial is owned by sc_server_params, and will be freed on destroy
|
||||
server->params.serial = sc_server_get_serialno(&server->intr);
|
||||
if (!server->params.serial) {
|
||||
LOGE("Could not get device serial");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
run_server(void *data) {
|
||||
struct sc_server *server = data;
|
||||
|
||||
if (!sc_server_fill_serial(server)) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
const struct sc_server_params *params = &server->params;
|
||||
|
||||
LOGD("Device serial: %s", params->serial);
|
||||
|
||||
bool ok = push_server(&server->intr, params->serial);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
// <https://devblogs.microsoft.com/oldnewthing/20111216-00/?p=8873>
|
||||
#define _WIN32_WINNT 0x0600 // For extended process API
|
||||
|
||||
#include "util/process.h"
|
||||
|
||||
#include <processthreadsapi.h>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
@ -26,6 +31,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
|
|||
HANDLE *pin, HANDLE *pout, HANDLE *perr) {
|
||||
enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC;
|
||||
|
||||
// Add 1 per non-NULL pointer
|
||||
unsigned handle_count = !!pin + !!pout + !!perr;
|
||||
|
||||
SECURITY_ATTRIBUTES sa;
|
||||
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
|
||||
sa.lpSecurityDescriptor = NULL;
|
||||
|
@ -65,45 +73,94 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
|
|||
}
|
||||
}
|
||||
|
||||
STARTUPINFOW si;
|
||||
STARTUPINFOEXW si;
|
||||
PROCESS_INFORMATION pi;
|
||||
memset(&si, 0, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
if (pin || pout || perr) {
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.StartupInfo.cb = sizeof(si);
|
||||
HANDLE handles[3];
|
||||
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL;
|
||||
if (handle_count) {
|
||||
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
if (pin) {
|
||||
si.hStdInput = stdin_read_handle;
|
||||
si.StartupInfo.hStdInput = stdin_read_handle;
|
||||
}
|
||||
if (pout) {
|
||||
si.hStdOutput = stdout_write_handle;
|
||||
si.StartupInfo.hStdOutput = stdout_write_handle;
|
||||
}
|
||||
if (perr) {
|
||||
si.hStdError = stderr_write_handle;
|
||||
si.StartupInfo.hStdError = stderr_write_handle;
|
||||
}
|
||||
|
||||
SIZE_T size;
|
||||
// Call it once to know the required buffer size
|
||||
BOOL ok =
|
||||
InitializeProcThreadAttributeList(NULL, 1, 0, &size)
|
||||
|| GetLastError() == ERROR_INSUFFICIENT_BUFFER;
|
||||
if (!ok) {
|
||||
goto error_close_stderr;
|
||||
}
|
||||
|
||||
lpAttributeList = malloc(size);
|
||||
if (!lpAttributeList) {
|
||||
goto error_close_stderr;
|
||||
}
|
||||
|
||||
ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size);
|
||||
if (!ok) {
|
||||
free(lpAttributeList);
|
||||
goto error_close_stderr;
|
||||
}
|
||||
|
||||
// Explicitly pass the HANDLEs that must be inherited
|
||||
unsigned i = 0;
|
||||
if (pin) {
|
||||
handles[i++] = stdin_read_handle;
|
||||
}
|
||||
if (pout) {
|
||||
handles[i++] = stdout_write_handle;
|
||||
}
|
||||
if (perr) {
|
||||
handles[i++] = stderr_write_handle;
|
||||
}
|
||||
ok = UpdateProcThreadAttribute(lpAttributeList, 0,
|
||||
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
handles, handle_count * sizeof(HANDLE),
|
||||
NULL, NULL);
|
||||
if (!ok) {
|
||||
goto error_free_attribute_list;
|
||||
}
|
||||
|
||||
si.lpAttributeList = lpAttributeList;
|
||||
}
|
||||
|
||||
char *cmd = malloc(CMD_MAX_LEN);
|
||||
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) {
|
||||
*handle = NULL;
|
||||
goto error_close_stderr;
|
||||
goto error_free_attribute_list;
|
||||
}
|
||||
|
||||
wchar_t *wide = sc_str_to_wchars(cmd);
|
||||
free(cmd);
|
||||
if (!wide) {
|
||||
LOGC("Could not allocate wide char string");
|
||||
goto error_close_stderr;
|
||||
goto error_free_attribute_list;
|
||||
}
|
||||
|
||||
if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si,
|
||||
&pi)) {
|
||||
free(wide);
|
||||
*handle = NULL;
|
||||
|
||||
BOOL bInheritHandles = handle_count > 0;
|
||||
DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT : 0;
|
||||
BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles,
|
||||
dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi);
|
||||
free(wide);
|
||||
if (!ok) {
|
||||
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
|
||||
ret = SC_PROCESS_ERROR_MISSING_BINARY;
|
||||
}
|
||||
goto error_close_stderr;
|
||||
goto error_free_attribute_list;
|
||||
}
|
||||
|
||||
if (lpAttributeList) {
|
||||
DeleteProcThreadAttributeList(lpAttributeList);
|
||||
free(lpAttributeList);
|
||||
}
|
||||
|
||||
// These handles are used by the child process, close them for this process
|
||||
|
@ -117,11 +174,15 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle,
|
|||
CloseHandle(stderr_write_handle);
|
||||
}
|
||||
|
||||
free(wide);
|
||||
*handle = pi.hProcess;
|
||||
|
||||
return SC_PROCESS_SUCCESS;
|
||||
|
||||
error_free_attribute_list:
|
||||
if (lpAttributeList) {
|
||||
DeleteProcThreadAttributeList(lpAttributeList);
|
||||
free(lpAttributeList);
|
||||
}
|
||||
error_close_stderr:
|
||||
if (perr) {
|
||||
CloseHandle(*perr);
|
||||
|
@ -168,7 +229,7 @@ sc_process_close(HANDLE handle) {
|
|||
}
|
||||
|
||||
ssize_t
|
||||
sc_read_pipe(HANDLE pipe, char *data, size_t len) {
|
||||
sc_pipe_read(HANDLE pipe, char *data, size_t len) {
|
||||
DWORD r;
|
||||
if (!ReadFile(pipe, data, len, &r, NULL)) {
|
||||
return -1;
|
||||
|
@ -177,7 +238,7 @@ sc_read_pipe(HANDLE pipe, char *data, size_t len) {
|
|||
}
|
||||
|
||||
void
|
||||
sc_close_pipe(HANDLE pipe) {
|
||||
sc_pipe_close(HANDLE pipe) {
|
||||
if (!CloseHandle(pipe)) {
|
||||
LOGW("Cannot close pipe");
|
||||
}
|
||||
|
|
|
@ -14,3 +14,31 @@ sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
|
|||
sc_intr_set_process(intr, SC_PROCESS_NONE);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ssize_t
|
||||
sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data,
|
||||
size_t len) {
|
||||
if (!sc_intr_set_process(intr, pid)) {
|
||||
// Already interrupted
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t ret = sc_pipe_read(pipe, data, len);
|
||||
|
||||
sc_intr_set_process(intr, SC_PROCESS_NONE);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ssize_t
|
||||
sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe,
|
||||
char *data, size_t len) {
|
||||
if (!sc_intr_set_process(intr, pid)) {
|
||||
// Already interrupted
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t ret = sc_pipe_read_all(pipe, data, len);
|
||||
|
||||
sc_intr_set_process(intr, SC_PROCESS_NONE);
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -10,4 +10,12 @@ bool
|
|||
sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
|
||||
const char *name);
|
||||
|
||||
ssize_t
|
||||
sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data,
|
||||
size_t len);
|
||||
|
||||
ssize_t
|
||||
sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe,
|
||||
char *data, size_t len);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -291,3 +291,14 @@ error:
|
|||
free(buf.s);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t
|
||||
sc_str_truncate(char *data, size_t len, const char *endchars) {
|
||||
data[len - 1] = '\0';
|
||||
char *eol = strpbrk(data, endchars);
|
||||
if (eol) {
|
||||
*eol = '\0';
|
||||
len = eol - data;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
|
|
@ -103,4 +103,15 @@ sc_str_from_wchars(const wchar_t *s);
|
|||
char *
|
||||
sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent);
|
||||
|
||||
/**
|
||||
* Truncate the data after any of the characters from `endchars`
|
||||
*
|
||||
* An '\0' is always written at the end of the data, even if no newline
|
||||
* character is encountered.
|
||||
*
|
||||
* Return the size of the resulting line.
|
||||
*/
|
||||
size_t
|
||||
sc_str_truncate(char *data, size_t len, const char *endchars);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -337,6 +337,26 @@ static void test_wrap_lines(void) {
|
|||
free(formatted);
|
||||
}
|
||||
|
||||
static void test_truncate(void) {
|
||||
char s[] = "hello\nworld\n!";
|
||||
size_t line_len = sc_str_truncate(s, sizeof(s), "\n");
|
||||
|
||||
assert(line_len == 5);
|
||||
assert(!strcmp("hello", s));
|
||||
|
||||
char s2[] = "hello\r\nworkd\r\n!";
|
||||
line_len = sc_str_truncate(s2, sizeof(s2), "\n\r");
|
||||
|
||||
assert(line_len == 5);
|
||||
assert(!strcmp("hello", s));
|
||||
|
||||
char s3[] = "hello world\n!";
|
||||
line_len = sc_str_truncate(s3, sizeof(s3), " \n\r");
|
||||
|
||||
assert(line_len == 5);
|
||||
assert(!strcmp("hello", s3));
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
@ -356,5 +376,6 @@ int main(int argc, char *argv[]) {
|
|||
test_parse_integer_with_suffix();
|
||||
test_strlist_contains();
|
||||
test_wrap_lines();
|
||||
test_truncate();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
@ -166,14 +165,21 @@ public final class CleanUp {
|
|||
|
||||
if (config.disableShowTouches || config.restoreStayOn != -1) {
|
||||
ServiceManager serviceManager = new ServiceManager();
|
||||
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
if (config.disableShowTouches) {
|
||||
Ln.i("Disabling \"show touches\"");
|
||||
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
|
||||
Settings settings = new Settings(serviceManager);
|
||||
if (config.disableShowTouches) {
|
||||
Ln.i("Disabling \"show touches\"");
|
||||
try {
|
||||
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"show_touches\"", e);
|
||||
}
|
||||
if (config.restoreStayOn != -1) {
|
||||
Ln.i("Restoring \"stay awake\"");
|
||||
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
|
||||
}
|
||||
if (config.restoreStayOn != -1) {
|
||||
Ln.i("Restoring \"stay awake\"");
|
||||
try {
|
||||
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
33
server/src/main/java/com/genymobile/scrcpy/Command.java
Normal file
33
server/src/main/java/com/genymobile/scrcpy/Command.java
Normal file
|
@ -0,0 +1,33 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Scanner;
|
||||
|
||||
public final class Command {
|
||||
private Command() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void exec(String... cmd) throws IOException, InterruptedException {
|
||||
Process process = Runtime.getRuntime().exec(cmd);
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static String execReadLine(String... cmd) throws IOException, InterruptedException {
|
||||
String result = null;
|
||||
Process process = Runtime.getRuntime().exec(cmd);
|
||||
Scanner scanner = new Scanner(process.getInputStream());
|
||||
if (scanner.hasNextLine()) {
|
||||
result = scanner.nextLine();
|
||||
}
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
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;
|
||||
|
@ -29,6 +28,7 @@ public final class Device {
|
|||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||
|
||||
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
||||
private static final Settings SETTINGS = new Settings(SERVICE_MANAGER);
|
||||
|
||||
public interface RotationListener {
|
||||
void onRotationChanged(int rotation);
|
||||
|
@ -296,7 +296,7 @@ public final class Device {
|
|||
}
|
||||
}
|
||||
|
||||
public static ContentProvider createSettingsProvider() {
|
||||
return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
|
||||
public static Settings getSettings() {
|
||||
return SETTINGS;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,13 +57,20 @@ public final class Ln {
|
|||
}
|
||||
}
|
||||
|
||||
public static void w(String message) {
|
||||
public static void w(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.WARN)) {
|
||||
Log.w(TAG, message);
|
||||
Log.w(TAG, message, throwable);
|
||||
System.out.println(PREFIX + "WARN: " + message);
|
||||
if (throwable != null) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String message) {
|
||||
w(message, null);
|
||||
}
|
||||
|
||||
public static void e(String message, Throwable throwable) {
|
||||
if (isEnabled(Level.ERROR)) {
|
||||
Log.e(TAG, message, throwable);
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
|
@ -19,24 +17,25 @@ public final class Server {
|
|||
// not instantiable
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
private static void initAndCleanUp(Options options) {
|
||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||
int restoreStayOn = -1;
|
||||
if (options.getShowTouches() || options.getStayAwake()) {
|
||||
try (ContentProvider settings = Device.createSettingsProvider()) {
|
||||
if (options.getShowTouches()) {
|
||||
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1");
|
||||
Settings settings = Device.getSettings();
|
||||
if (options.getShowTouches()) {
|
||||
try {
|
||||
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
|
||||
// If "show touches" was disabled, it must be disabled back on clean up
|
||||
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"show_touches\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
if (options.getStayAwake()) {
|
||||
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
|
||||
try {
|
||||
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
|
||||
try {
|
||||
restoreStayOn = Integer.parseInt(oldValue);
|
||||
if (restoreStayOn == stayOn) {
|
||||
|
@ -46,11 +45,25 @@ public final class Server {
|
|||
} catch (NumberFormatException e) {
|
||||
restoreStayOn = 0;
|
||||
}
|
||||
} catch (SettingsException e) {
|
||||
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
|
||||
try {
|
||||
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not configure cleanup", e);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
Thread initThread = startInitThread(options);
|
||||
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
|
||||
|
@ -82,6 +95,7 @@ public final class Server {
|
|||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (controllerThread != null) {
|
||||
controllerThread.interrupt();
|
||||
}
|
||||
|
@ -92,6 +106,17 @@ public final class Server {
|
|||
}
|
||||
}
|
||||
|
||||
private static Thread startInitThread(final Options options) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initAndCleanUp(options);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
|
|
84
server/src/main/java/com/genymobile/scrcpy/Settings.java
Normal file
84
server/src/main/java/com/genymobile/scrcpy/Settings.java
Normal file
|
@ -0,0 +1,84 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.ContentProvider;
|
||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class Settings {
|
||||
|
||||
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
|
||||
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
|
||||
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
|
||||
|
||||
private final ServiceManager serviceManager;
|
||||
|
||||
public Settings(ServiceManager serviceManager) {
|
||||
this.serviceManager = serviceManager;
|
||||
}
|
||||
|
||||
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
|
||||
try {
|
||||
Command.exec("settings", "put", table, key, value);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new SettingsException("put", table, key, value, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String execSettingsGet(String table, String key) throws SettingsException {
|
||||
try {
|
||||
return Command.execReadLine("settings", "get", table, key);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new SettingsException("get", table, key, null, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getValue(String table, String key) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
return provider.getValue(table, key);
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
}
|
||||
|
||||
return execSettingsGet(table, key);
|
||||
}
|
||||
|
||||
public void putValue(String table, String key, String value) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
provider.putValue(table, key, value);
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
}
|
||||
|
||||
execSettingsPut(table, key, value);
|
||||
}
|
||||
|
||||
public String getAndPutValue(String table, String key, String value) throws SettingsException {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
|
||||
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
|
||||
String oldValue = provider.getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
provider.putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
} catch (SettingsException e) {
|
||||
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
|
||||
}
|
||||
}
|
||||
|
||||
String oldValue = getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
putValue(table, key, value);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
public class SettingsException extends Exception {
|
||||
private static String createMessage(String method, String table, String key, String value) {
|
||||
return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
|
||||
}
|
||||
|
||||
public SettingsException(String method, String table, String key, String value, Throwable cause) {
|
||||
super(createMessage(method, table, key, value), cause);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
import com.genymobile.scrcpy.SettingsException;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
|
@ -87,7 +88,8 @@ public class ContentProvider implements Closeable {
|
|||
return attributionSource;
|
||||
}
|
||||
|
||||
private Bundle call(String callMethod, String arg, Bundle extras) {
|
||||
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
||||
try {
|
||||
Method method = getCallMethod();
|
||||
Object[] args;
|
||||
|
@ -108,7 +110,7 @@ public class ContentProvider implements Closeable {
|
|||
return (Bundle) method.invoke(provider, args);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,30 +144,31 @@ public class ContentProvider implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
public String getValue(String table, String key) {
|
||||
public String getValue(String table, String key) throws SettingsException {
|
||||
String method = getGetMethod(table);
|
||||
Bundle arg = new Bundle();
|
||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||
Bundle bundle = call(method, key, arg);
|
||||
if (bundle == null) {
|
||||
return null;
|
||||
try {
|
||||
Bundle bundle = call(method, key, arg);
|
||||
if (bundle == null) {
|
||||
return null;
|
||||
}
|
||||
return bundle.getString("value");
|
||||
} catch (Exception e) {
|
||||
throw new SettingsException(table, "get", key, null, e);
|
||||
}
|
||||
return bundle.getString("value");
|
||||
|
||||
}
|
||||
|
||||
public void putValue(String table, String key, String value) {
|
||||
public void putValue(String table, String key, String value) throws SettingsException {
|
||||
String method = getPutMethod(table);
|
||||
Bundle arg = new Bundle();
|
||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||
call(method, key, arg);
|
||||
}
|
||||
|
||||
public String getAndPutValue(String table, String key, String value) {
|
||||
String oldValue = getValue(table, key);
|
||||
if (!value.equals(oldValue)) {
|
||||
putValue(table, key, value);
|
||||
try {
|
||||
call(method, key, arg);
|
||||
} catch (Exception e) {
|
||||
throw new SettingsException(table, "put", key, value, e);
|
||||
}
|
||||
return oldValue;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue