Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
9a7e8da12c | |||
dc7b60e619 | |||
1e4ee547b5 | |||
488d22d4e2 | |||
00d292b2f5 | |||
245999aec4 | |||
d91c5dcfd5 | |||
d202d7b205 | |||
08c0c64af6 | |||
a00a8763d6 | |||
8f314c74b0 | |||
6e1069a822 | |||
c4323df976 | |||
8ff07e0c88 | |||
e4efd75766 | |||
0e4a6f462b | |||
8b73c90427 | |||
ef91ab2841 | |||
44fa4a090e | |||
dcde578a50 | |||
93a5c5149d | |||
2ca8318b9d | |||
d499ee53c9 | |||
8f619f337b | |||
fc1dec0270 | |||
274b591d18 | |||
4bbabfb4ef | |||
ffc57512b3 | |||
c7a33fac36 | |||
81573d81a0 | |||
5c2cf88a1d | |||
8f46e18426 | |||
ee3882f8be | |||
a3ef461d73 | |||
3df63c579d | |||
56bff2f718 | |||
080a4ee365 | |||
c7155a1954 | |||
517dbd9c85 | |||
acc4ef31df | |||
73e722784d | |||
e1cd75792c | |||
ac4c8b4a3f | |||
fae3f9eeab | |||
f5aeecbc62 | |||
e40532a376 | |||
d860ad48e6 | |||
ec047b501e | |||
4c2e10fd74 | |||
6b1da2fcff | |||
2608b1dc62 | |||
31fa115655 | |||
82446b6b0d | |||
24c8039244 | |||
1a9429f3c7 | |||
0b4e484da0 |
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -21,5 +21,9 @@ assignees: ''
|
|||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
On errors, please provide the output of the console (and `adb logcat` if relevant).
|
On errors, please provide the output of the console (and `adb logcat` if relevant).
|
||||||
Format them between code blocks (delimited by ```).
|
|
||||||
|
```
|
||||||
|
Please paste terminal output in a code block.
|
||||||
|
```
|
||||||
|
|
||||||
Please do not post screenshots of your terminal, just post the content as text instead.
|
Please do not post screenshots of your terminal, just post the content as text instead.
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,8 @@
|
|||||||
build/
|
build/
|
||||||
/dist/
|
/dist/
|
||||||
|
/build-*/
|
||||||
|
/build_*/
|
||||||
|
/release-*/
|
||||||
.idea/
|
.idea/
|
||||||
.gradle/
|
.gradle/
|
||||||
/x/
|
/x/
|
||||||
|
6
BUILD.md
6
BUILD.md
@ -249,10 +249,10 @@ You can then [run](README.md#run) _scrcpy_.
|
|||||||
|
|
||||||
## Prebuilt server
|
## Prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v1.13`][direct-scrcpy-server]
|
- [`scrcpy-server-v1.14`][direct-scrcpy-server]
|
||||||
_(SHA-256: 5fee64ca1ccdc2f38550f31f5353c66de3de30c2e929a964e30fa2d005d5f885)_
|
_(SHA-256: 1d1b18a2b80e956771fd63b99b414d2d028713a8f12ddfa5a369709ad4295620)_
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-server-v1.13
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-server-v1.14
|
||||||
|
|
||||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
configuration:
|
configuration:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||||
|
|
||||||
# scrcpy (v1.11)
|
# scrcpy (v1.11)
|
||||||
|
|
||||||
This document will be updated frequently along with the original Readme file
|
This document will be updated frequently along with the original Readme file
|
||||||
|
56
README.md
56
README.md
@ -1,4 +1,4 @@
|
|||||||
# scrcpy (v1.13)
|
# scrcpy (v1.14)
|
||||||
|
|
||||||
This application provides display and control of Android devices connected on
|
This application provides display and control of Android devices connected on
|
||||||
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
|
||||||
@ -69,10 +69,10 @@ hard).
|
|||||||
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
||||||
(including `adb`) is available:
|
(including `adb`) is available:
|
||||||
|
|
||||||
- [`scrcpy-win64-v1.13.zip`][direct-win64]
|
- [`scrcpy-win64-v1.14.zip`][direct-win64]
|
||||||
_(SHA-256: 806aafc00d4db01513193addaa24f47858893ba5efe75770bfef6ae1ea987d27)_
|
_(SHA-256: 2be9139e46e29cf2f5f695848bb2b75a543b8f38be1133257dc5068252abc25f)_
|
||||||
|
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.13/scrcpy-win64-v1.13.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.14/scrcpy-win64-v1.14.zip
|
||||||
|
|
||||||
It is also available in [Chocolatey]:
|
It is also available in [Chocolatey]:
|
||||||
|
|
||||||
@ -289,6 +289,22 @@ From another terminal:
|
|||||||
scrcpy
|
scrcpy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To avoid enabling remote port forwarding, you could force a forward connection
|
||||||
|
instead (notice the `-L` instead of `-R`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb kill-server # kill the local adb server on 5037
|
||||||
|
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||||
|
# keep this open
|
||||||
|
```
|
||||||
|
|
||||||
|
From another terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --force-adb-forwrad
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Like for wireless connections, it may be useful to reduce quality:
|
Like for wireless connections, it may be useful to reduce quality:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -395,9 +411,13 @@ The list of display ids can be retrieved by:
|
|||||||
adb shell dumpsys display # search "mDisplayId=" in the output
|
adb shell dumpsys display # search "mDisplayId=" in the output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The secondary display may only be controlled if the device runs at least Android
|
||||||
|
10 (otherwise it is mirrored in read-only).
|
||||||
|
|
||||||
|
|
||||||
#### Stay awake
|
#### Stay awake
|
||||||
|
|
||||||
To prevent the device to sleep after some delay:
|
To prevent the device to sleep after some delay when the device is plugged in:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --stay-awake
|
scrcpy --stay-awake
|
||||||
@ -419,7 +439,7 @@ scrcpy -S
|
|||||||
|
|
||||||
Or by pressing `Ctrl`+`o` at any time.
|
Or by pressing `Ctrl`+`o` at any time.
|
||||||
|
|
||||||
To turn it back on, press `POWER` (or `Ctrl`+`p`).
|
To turn it back on, press `Ctrl`+`Shift`+`o` (or `POWER`, `Ctrl`+`p`).
|
||||||
|
|
||||||
It can be useful to also prevent the device to sleep:
|
It can be useful to also prevent the device to sleep:
|
||||||
|
|
||||||
@ -459,6 +479,17 @@ scrcpy -t
|
|||||||
Note that it only shows _physical_ touches (with the finger on the device).
|
Note that it only shows _physical_ touches (with the finger on the device).
|
||||||
|
|
||||||
|
|
||||||
|
#### Disable screensaver
|
||||||
|
|
||||||
|
By default, scrcpy does not prevent the screensaver to run on the computer.
|
||||||
|
|
||||||
|
To disable it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --disable-screensaver
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Input control
|
### Input control
|
||||||
|
|
||||||
#### Rotate device screen
|
#### Rotate device screen
|
||||||
@ -474,10 +505,14 @@ It is possible to synchronize clipboards between the computer and the device, in
|
|||||||
both directions:
|
both directions:
|
||||||
|
|
||||||
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
|
||||||
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
|
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard (and
|
||||||
|
pastes if the device runs Android >= 7);
|
||||||
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
|
||||||
breaks non-ASCII characters).
|
breaks non-ASCII characters).
|
||||||
|
|
||||||
|
Moreover, any time the Android clipboard changes, it is automatically
|
||||||
|
synchronized to the computer clipboard.
|
||||||
|
|
||||||
#### Text injection preference
|
#### Text injection preference
|
||||||
|
|
||||||
There are two kinds of [events][textevents] generated when typing text:
|
There are two kinds of [events][textevents] generated when typing text:
|
||||||
@ -537,7 +572,7 @@ Also see [issue #14].
|
|||||||
## Shortcuts
|
## Shortcuts
|
||||||
|
|
||||||
| Action | Shortcut | Shortcut (macOS)
|
| Action | Shortcut | Shortcut (macOS)
|
||||||
| -------------------------------------- |:----------------------------- |:-----------------------------
|
| ------------------------------------------- |:----------------------------- |:-----------------------------
|
||||||
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
|
| Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f`
|
||||||
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
|
| Rotate display left | `Ctrl`+`←` _(left)_ | `Cmd`+`←` _(left)_
|
||||||
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
|
| Rotate display right | `Ctrl`+`→` _(right)_ | `Cmd`+`→` _(right)_
|
||||||
@ -551,13 +586,14 @@ Also see [issue #14].
|
|||||||
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
||||||
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
| Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||||
| Power on | _Right-click²_ | _Right-click²_
|
| Power on | _Right-click²_ | _Right-click²_
|
||||||
| Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o`
|
| Turn device screen off (keep mirroring) | `Ctrl`+`o` | `Cmd`+`o`
|
||||||
|
| Turn device screen on | `Ctrl`+`Shift`+`o` | `Cmd`+`Shift`+`o`
|
||||||
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
| Rotate device screen | `Ctrl`+`r` | `Cmd`+`r`
|
||||||
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
| Expand notification panel | `Ctrl`+`n` | `Cmd`+`n`
|
||||||
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
| Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||||
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
| Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c`
|
||||||
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
|
| Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v`
|
||||||
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
| Copy computer clipboard to device and paste | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
||||||
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
||||||
|
|
||||||
_¹Double-click on black borders to remove them._
|
_¹Double-click on black borders to remove them._
|
||||||
|
531
README.pt-br.md
Normal file
531
README.pt-br.md
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||||
|
|
||||||
|
# scrcpy (v1.12.1)
|
||||||
|
|
||||||
|
Esta aplicação fornece visualização e controle de dispositivos Android conectados via
|
||||||
|
USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso root.
|
||||||
|
Funciona em _GNU/Linux_, _Windows_ e _macOS_.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Foco em:
|
||||||
|
|
||||||
|
- **leveza** (Nativo, mostra apenas a tela do dispositivo)
|
||||||
|
- **performance** (30~60fps)
|
||||||
|
- **qualidade** (1920×1080 ou acima)
|
||||||
|
- **baixa latência** ([35~70ms][lowlatency])
|
||||||
|
- **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem)
|
||||||
|
- **não intrusivo** (nada é deixado instalado no dispositivo)
|
||||||
|
|
||||||
|
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||||
|
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
O Dispositivo Android requer pelo menos a API 21 (Android 5.0).
|
||||||
|
|
||||||
|
|
||||||
|
Tenha certeza de ter [ativado a depuração USB][enable-adb] no(s) seu(s) dispositivo(s).
|
||||||
|
|
||||||
|
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||||
|
|
||||||
|
|
||||||
|
Em alguns dispositivos, você também precisará ativar [uma opção adicional][control] para controlá-lo usando o teclado e mouse.
|
||||||
|
|
||||||
|
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||||
|
|
||||||
|
|
||||||
|
## Obtendo o app
|
||||||
|
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
No Debian (_em testes_ e _sid_ por enquanto):
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
O pacote [Snap] está disponível: [`scrcpy`][snap-link].
|
||||||
|
|
||||||
|
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||||
|
|
||||||
|
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||||
|
|
||||||
|
Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link].
|
||||||
|
|
||||||
|
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||||
|
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||||
|
|
||||||
|
Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link].
|
||||||
|
|
||||||
|
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||||
|
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||||
|
|
||||||
|
|
||||||
|
Você também pode [compilar a aplicação manualmente][BUILD] (não se preocupe, não é tão difícil).
|
||||||
|
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Para Windows, para simplicidade, um arquivo pré-compilado com todas as dependências
|
||||||
|
(incluindo `adb`) está disponível:
|
||||||
|
|
||||||
|
- [`scrcpy-win64-v1.12.1.zip`][direct-win64]
|
||||||
|
_(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_
|
||||||
|
|
||||||
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip
|
||||||
|
|
||||||
|
Também disponível em [Chocolatey]:
|
||||||
|
|
||||||
|
[Chocolatey]: https://chocolatey.org/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
choco install scrcpy
|
||||||
|
choco install adb # se você ainda não o tem
|
||||||
|
```
|
||||||
|
|
||||||
|
E no [Scoop]:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scoop install scrcpy
|
||||||
|
scoop install adb # se você ainda não o tem
|
||||||
|
```
|
||||||
|
|
||||||
|
[Scoop]: https://scoop.sh
|
||||||
|
|
||||||
|
Você também pode [compilar a aplicação manualmente][BUILD].
|
||||||
|
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
A aplicação está disponível em [Homebrew]. Apenas a instale:
|
||||||
|
|
||||||
|
[Homebrew]: https://brew.sh/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Você precisa do `adb`, acessível através do seu `PATH`. Se você ainda não o tem:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew cask install android-platform-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Você também pode [compilar a aplicação manualmente][BUILD].
|
||||||
|
|
||||||
|
|
||||||
|
## Executar
|
||||||
|
|
||||||
|
Plugue um dispositivo Android e execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Também aceita argumentos de linha de comando, listados por:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Funcionalidades
|
||||||
|
|
||||||
|
### Configuração de captura
|
||||||
|
|
||||||
|
#### Redução de tamanho
|
||||||
|
|
||||||
|
Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para
|
||||||
|
aumentar performance.
|
||||||
|
|
||||||
|
Para limitar ambos(largura e altura) para algum valor (ex: 1024):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --max-size 1024
|
||||||
|
scrcpy -m 1024 # versão reduzida
|
||||||
|
```
|
||||||
|
|
||||||
|
A outra dimensão é calculada para que a proporção do dispositivo seja preservada.
|
||||||
|
Dessa forma, um dispositivo em 1920x1080 será espelhado em 1024x576.
|
||||||
|
|
||||||
|
|
||||||
|
#### Mudanças no bit-rate
|
||||||
|
|
||||||
|
O Padrão de bit-rate é 8 mbps. Para mudar o bitrate do vídeo (ex: para 2 Mbps):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --bit-rate 2M
|
||||||
|
scrcpy -b 2M # versão reduzida
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Limitar frame rates
|
||||||
|
|
||||||
|
Em dispositivos com Android >= 10, a captura de frame rate pode ser limitada:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --max-fps 15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cortar
|
||||||
|
|
||||||
|
A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela.
|
||||||
|
|
||||||
|
Isso é útil por exemplo, ao espelhar apenas um olho do Oculus Go:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Se `--max-size` também for especificado, redimensionar é aplicado após os cortes.
|
||||||
|
|
||||||
|
|
||||||
|
### Gravando
|
||||||
|
|
||||||
|
É possível gravar a tela enquanto ocorre o espelhamento:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --record file.mp4
|
||||||
|
scrcpy -r file.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
Para desativar o espelhamento durante a gravação:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --no-display --record file.mp4
|
||||||
|
scrcpy -Nr file.mkv
|
||||||
|
# interrompe a gravação com Ctrl+C
|
||||||
|
# Ctrl+C não encerrar propriamente no Windows, então desconecte o dispositivo
|
||||||
|
```
|
||||||
|
|
||||||
|
"Frames pulados" são gravados, mesmo que não sejam mostrado em tempo real (por motivos de performance).
|
||||||
|
Frames tem seu _horário_ _carimbado_ no dispositivo, então [Variação de atraso nos pacotes] não impacta na gravação do arquivo.
|
||||||
|
|
||||||
|
[Variação de atraso de pacote]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||||
|
|
||||||
|
|
||||||
|
### Conexão
|
||||||
|
|
||||||
|
#### Wireless/Sem fio
|
||||||
|
|
||||||
|
_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se] à um dispositivo via TCP/IP:
|
||||||
|
|
||||||
|
1. Conecte o dispositivo a mesma rede Wi-Fi do seu computador.
|
||||||
|
2. Pegue o endereço de IP do seu dispositivo (Em Configurações → Sobre o Telefone → Status).
|
||||||
|
3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`.
|
||||||
|
4. Desplugue seu dispositivo.
|
||||||
|
5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua o `DEVICE_IP`)_.
|
||||||
|
6. Execute `scrcpy` como de costume.
|
||||||
|
|
||||||
|
Pode ser útil diminuir o bit-rate e a resolução:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --bit-rate 2M --max-size 800
|
||||||
|
scrcpy -b2M -m800 # versão reduzida
|
||||||
|
```
|
||||||
|
|
||||||
|
[conectar-se]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||||
|
|
||||||
|
|
||||||
|
#### N-dispositivos
|
||||||
|
|
||||||
|
Se alguns dispositivos estão listados em `adb devices`, você precisa especificar o _serial_:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --serial 0123456789abcdef
|
||||||
|
scrcpy -s 0123456789abcdef # versão reduzida
|
||||||
|
```
|
||||||
|
|
||||||
|
Se o dispositivo está conectado via TCP/IP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --serial 192.168.0.1:5555
|
||||||
|
scrcpy -s 192.168.0.1:5555 # versão reduzida
|
||||||
|
```
|
||||||
|
|
||||||
|
Você pode iniciar algumas instâncias do _scrcpy_ para alguns dispositivos.
|
||||||
|
|
||||||
|
#### Conexão via SSH
|
||||||
|
|
||||||
|
Para conectar-se à um dispositivo remoto, é possível se conectar um cliente local `adb` à um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo _adb_):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb kill-server # encerra o servidor local na 5037
|
||||||
|
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||||
|
# mantém isso aberto
|
||||||
|
```
|
||||||
|
|
||||||
|
De outro terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Igual para conexões sem fio, pode ser útil reduzir a qualidade:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy -b2M -m800 --max-fps 15
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configurações de Janela
|
||||||
|
|
||||||
|
#### Título
|
||||||
|
|
||||||
|
Por padrão, o título da janela é o modelo do dispositivo. Isto pode ser mudado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-title 'Meu dispositivo'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Posição e tamanho
|
||||||
|
|
||||||
|
A posição e tamanho iniciais da janela podem ser especificados:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sem bordas
|
||||||
|
|
||||||
|
Para desativar decorações da janela:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --window-borderless
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sempre visível
|
||||||
|
|
||||||
|
Para manter a janela do scrcpy sempre visível:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --always-on-top
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tela cheia
|
||||||
|
|
||||||
|
A aplicação pode ser iniciada diretamente em tela cheia:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --fullscreen
|
||||||
|
scrcpy -f # versão reduzida
|
||||||
|
```
|
||||||
|
|
||||||
|
Tela cheia pode ser alternada dinamicamente com `Ctrl`+`f`.
|
||||||
|
|
||||||
|
|
||||||
|
### Outras opções de espelhamento
|
||||||
|
|
||||||
|
#### Apenas leitura
|
||||||
|
|
||||||
|
Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada, eventos de mouse, arrastar e soltar arquivos):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --no-control
|
||||||
|
scrcpy -n
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desligar a tela
|
||||||
|
|
||||||
|
É possível desligar a tela do dispositivo durante o início do espelhamento com uma opção de linha de comando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --turn-screen-off
|
||||||
|
scrcpy -S
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou apertando `Ctrl`+`o` durante qualquer momento.
|
||||||
|
|
||||||
|
Para ligar novamente, pressione `POWER` (ou `Ctrl`+`p`).
|
||||||
|
|
||||||
|
#### Frames expirados de renderização
|
||||||
|
|
||||||
|
Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado disponível e descarta o anterior.
|
||||||
|
|
||||||
|
Para forçar a renderização de todos os frames ( com o custo de aumento de latência), use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --render-expired-frames
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mostrar toques
|
||||||
|
|
||||||
|
Para apresentações, pode ser útil mostrar toques físicos(dispositivo físico).
|
||||||
|
|
||||||
|
Android fornece esta funcionalidade nas _Opções do Desenvolvedor_.
|
||||||
|
|
||||||
|
_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e desativar no encerramento:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --show-touches
|
||||||
|
scrcpy -t
|
||||||
|
```
|
||||||
|
|
||||||
|
Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo).
|
||||||
|
|
||||||
|
|
||||||
|
### Controle de entrada
|
||||||
|
|
||||||
|
#### Rotacionar a tela do dispositivo
|
||||||
|
|
||||||
|
Pressione `Ctrl`+`r` para mudar entre os modos Retrato e Paisagem.
|
||||||
|
|
||||||
|
Note que só será rotacionado se a aplicação em primeiro plano tiver suporte para o modo requisitado.
|
||||||
|
|
||||||
|
#### Copiar-Colar
|
||||||
|
|
||||||
|
É possível sincronizar áreas de transferência entre computador e o dispositivo,
|
||||||
|
para ambas direções:
|
||||||
|
|
||||||
|
- `Ctrl`+`c` copia a área de transferência do dispositivo para a área de trasferência do computador;
|
||||||
|
- `Ctrl`+`Shift`+`v` copia a área de transferência do computador para a área de transferência do dispositivo;
|
||||||
|
- `Ctrl`+`v` _cola_ a área de transferência do computador como uma sequência de eventos de texto (mas
|
||||||
|
quebra caracteres não-ASCII).
|
||||||
|
|
||||||
|
#### Preferências de injeção de texto
|
||||||
|
|
||||||
|
Existe dois tipos de [eventos][textevents] gerados ao digitar um texto:
|
||||||
|
- _eventos de teclas_, sinalizando que a tecla foi pressionada ou solta;
|
||||||
|
- _eventos de texto_, sinalizando que o texto foi inserido.
|
||||||
|
|
||||||
|
Por padrão, letras são injetadas usando eventos de teclas, assim teclados comportam-se
|
||||||
|
como esperado em jogos (normalmente para tecladas WASD)
|
||||||
|
|
||||||
|
Mas isto pode [causar problemas][prefertext]. Se você encontrar tal problema,
|
||||||
|
pode evitá-lo usando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --prefer-text
|
||||||
|
```
|
||||||
|
|
||||||
|
(mas isto vai quebrar o comportamento do teclado em jogos)
|
||||||
|
|
||||||
|
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||||
|
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||||
|
|
||||||
|
|
||||||
|
### Transferência de arquivo
|
||||||
|
|
||||||
|
#### Instalar APK
|
||||||
|
|
||||||
|
Para instalar um APK, arraste e solte o arquivo APK(com extensão `.apk`) na janela _scrcpy_.
|
||||||
|
|
||||||
|
Não existe feedback visual, um log é imprimido no console.
|
||||||
|
|
||||||
|
|
||||||
|
#### Enviar arquivo para o dispositivo
|
||||||
|
|
||||||
|
Para enviar um arquivo para o diretório `/sdcard/` no dispositivo, arraste e solte um arquivo não APK para a janela do
|
||||||
|
_scrcpy_.
|
||||||
|
|
||||||
|
Não existe feedback visual, um log é imprimido no console.
|
||||||
|
|
||||||
|
O diretório alvo pode ser mudado ao iniciar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --push-target /sdcard/foo/bar/
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Encaminhamento de áudio
|
||||||
|
|
||||||
|
Áudio não é encaminhando pelo _scrcpy_. Use [USBaudio] (Apenas linux).
|
||||||
|
|
||||||
|
Também veja [issue #14].
|
||||||
|
|
||||||
|
[USBaudio]: https://github.com/rom1v/usbaudio
|
||||||
|
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||||
|
|
||||||
|
|
||||||
|
## Atalhos
|
||||||
|
|
||||||
|
| Ação | Atalho | Atalho (macOS)
|
||||||
|
| ------------------------------------------------------------- |:------------------------------- |:-----------------------------
|
||||||
|
| Alternar para modo de tela cheia | `Ctrl`+`f` | `Cmd`+`f`
|
||||||
|
| Redimensionar janela para pixel-perfect(Escala 1:1) | `Ctrl`+`g` | `Cmd`+`g`
|
||||||
|
| Redimensionar janela para tirar as bordas pretas | `Ctrl`+`x` \| _Clique-duplo¹_ | `Cmd`+`x` \| _Clique-duplo¹_
|
||||||
|
| Clicar em `HOME` | `Ctrl`+`h` \| _Clique-central_ | `Ctrl`+`h` \| _Clique-central_
|
||||||
|
| Clicar em `BACK` | `Ctrl`+`b` \| _Clique-direito²_ | `Cmd`+`b` \| _Clique-direito²_
|
||||||
|
| Clicar em `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s`
|
||||||
|
| Clicar em `MENU` | `Ctrl`+`m` | `Ctrl`+`m`
|
||||||
|
| Clicar em `VOLUME_UP` | `Ctrl`+`↑` _(cima)_ | `Cmd`+`↑` _(cima)_
|
||||||
|
| Clicar em `VOLUME_DOWN` | `Ctrl`+`↓` _(baixo)_ | `Cmd`+`↓` _(baixo)_
|
||||||
|
| Clicar em `POWER` | `Ctrl`+`p` | `Cmd`+`p`
|
||||||
|
| Ligar | _Clique-direito²_ | _Clique-direito²_
|
||||||
|
| Desligar a tela do dispositivo | `Ctrl`+`o` | `Cmd`+`o`
|
||||||
|
| Rotacionar tela do dispositivo | `Ctrl`+`r` | `Cmd`+`r`
|
||||||
|
| Expandir painel de notificação | `Ctrl`+`n` | `Cmd`+`n`
|
||||||
|
| Esconder painel de notificação | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||||
|
| Copiar área de transferência do dispositivo para o computador | `Ctrl`+`c` | `Cmd`+`c`
|
||||||
|
| Colar área de transferência do computador para o dispositivo | `Ctrl`+`v` | `Cmd`+`v`
|
||||||
|
| Copiar área de transferência do computador para dispositivo | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
||||||
|
| Ativar/desativar contador de FPS(Frames por segundo) | `Ctrl`+`i` | `Cmd`+`i`
|
||||||
|
|
||||||
|
_¹Clique-duplo em bordas pretas para removê-las._
|
||||||
|
_²Botão direito liga a tela se ela estiver desligada, clique BACK para o contrário._
|
||||||
|
|
||||||
|
|
||||||
|
## Caminhos personalizados
|
||||||
|
|
||||||
|
Para usar um binário específico _adb_, configure seu caminho na variável de ambiente `ADB`:
|
||||||
|
|
||||||
|
ADB=/caminho/para/adb scrcpy
|
||||||
|
|
||||||
|
Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em
|
||||||
|
`SCRCPY_SERVER_PATH`.
|
||||||
|
|
||||||
|
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||||
|
|
||||||
|
|
||||||
|
## Por quê _scrcpy_?
|
||||||
|
|
||||||
|
Um colega me desafiou a encontrar um nome impronunciável como [gnirehtet].
|
||||||
|
|
||||||
|
[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een.
|
||||||
|
|
||||||
|
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||||
|
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||||
|
|
||||||
|
|
||||||
|
## Como compilar?
|
||||||
|
|
||||||
|
Veja [BUILD].
|
||||||
|
|
||||||
|
[BUILD]: BUILD.md
|
||||||
|
|
||||||
|
|
||||||
|
## Problemas comuns
|
||||||
|
|
||||||
|
Veja [FAQ](FAQ.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Desenvolvedores
|
||||||
|
|
||||||
|
Leia a [developers page].
|
||||||
|
|
||||||
|
[developers page]: DEVELOP.md
|
||||||
|
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
Copyright (C) 2018 Genymobile
|
||||||
|
Copyright (C) 2018-2020 Romain Vimont
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
## Artigos
|
||||||
|
|
||||||
|
- [Introducing scrcpy][article-intro]
|
||||||
|
- [Scrcpy now works wirelessly][article-tcpip]
|
||||||
|
|
||||||
|
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||||
|
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
@ -164,12 +164,12 @@ if get_option('buildtype') == 'debug'
|
|||||||
'src/cli.c',
|
'src/cli.c',
|
||||||
'src/util/str_util.c',
|
'src/util/str_util.c',
|
||||||
]],
|
]],
|
||||||
['test_control_event_serialize', [
|
['test_control_msg_serialize', [
|
||||||
'tests/test_control_msg_serialize.c',
|
'tests/test_control_msg_serialize.c',
|
||||||
'src/control_msg.c',
|
'src/control_msg.c',
|
||||||
'src/util/str_util.c',
|
'src/util/str_util.c',
|
||||||
]],
|
]],
|
||||||
['test_device_event_deserialize', [
|
['test_device_msg_deserialize', [
|
||||||
'tests/test_device_msg_deserialize.c',
|
'tests/test_device_msg_deserialize.c',
|
||||||
'src/device_msg.c',
|
'src/device_msg.c',
|
||||||
]],
|
]],
|
||||||
|
35
app/scrcpy.1
35
app/scrcpy.1
@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are
|
|||||||
|
|
||||||
Default is 8000000.
|
Default is 8000000.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-codec\-options " key[:type]=value[,...]
|
||||||
|
Set a list of comma-separated key:type=value options for the device encoder.
|
||||||
|
|
||||||
|
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||||
|
|
||||||
|
The list of possible codec options is available in the Android documentation
|
||||||
|
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||||
|
.UE .
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||||
Crop the device screen on the server.
|
Crop the device screen on the server.
|
||||||
@ -33,6 +43,10 @@ The values are expressed in the device natural orientation (typically, portrait
|
|||||||
.B \-\-max\-size
|
.B \-\-max\-size
|
||||||
value is computed on the cropped size.
|
value is computed on the cropped size.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-disable-screensaver"
|
||||||
|
Disable screensaver while scrcpy is running.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-display " id
|
.BI "\-\-display " id
|
||||||
Specify the display id to mirror.
|
Specify the display id to mirror.
|
||||||
@ -42,6 +56,10 @@ The list of possible display ids can be listed by "adb shell dumpsys display"
|
|||||||
|
|
||||||
Default is 0.
|
Default is 0.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-\-force\-adb\-forward
|
||||||
|
Do not attempt to use "adb reverse" to connect to the device.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-f, \-\-fullscreen
|
.B \-f, \-\-fullscreen
|
||||||
Start in fullscreen.
|
Start in fullscreen.
|
||||||
@ -115,6 +133,7 @@ Force recording format (either mp4 or mkv).
|
|||||||
Request SDL to use the given render driver (this is just a hint).
|
Request SDL to use the given render driver (this is just a hint).
|
||||||
|
|
||||||
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
|
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
|
||||||
|
|
||||||
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
||||||
.UE
|
.UE
|
||||||
|
|
||||||
@ -136,7 +155,7 @@ Turn the device screen off immediately.
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-t, \-\-show\-touches
|
.B \-t, \-\-show\-touches
|
||||||
Enable "show touches" on start, restore the initial value on exit..
|
Enable "show touches" on start, restore the initial value on exit.
|
||||||
|
|
||||||
It only shows physical touches (not clicks from scrcpy).
|
It only shows physical touches (not clicks from scrcpy).
|
||||||
|
|
||||||
@ -144,9 +163,15 @@ It only shows physical touches (not clicks from scrcpy).
|
|||||||
.B \-v, \-\-version
|
.B \-v, \-\-version
|
||||||
Print the version of scrcpy.
|
Print the version of scrcpy.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-V, \-\-verbosity " value
|
||||||
|
Set the log level ("debug", "info", "warn" or "error").
|
||||||
|
|
||||||
|
Default is "info" for release builds, "debug" for debug builds.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-w, \-\-stay-awake
|
.B \-w, \-\-stay-awake
|
||||||
Keep the device on while scrcpy is running.
|
Keep the device on while scrcpy is running, when the device is plugged in.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-window\-borderless
|
.B \-\-window\-borderless
|
||||||
@ -238,6 +263,10 @@ Turn screen on
|
|||||||
.B Ctrl+o
|
.B Ctrl+o
|
||||||
Turn device screen off (keep mirroring)
|
Turn device screen off (keep mirroring)
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B Ctrl+Shift+o
|
||||||
|
Turn device screen on
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+r
|
.B Ctrl+r
|
||||||
Rotate device screen
|
Rotate device screen
|
||||||
@ -260,7 +289,7 @@ Paste computer clipboard to device
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+Shift+v
|
.B Ctrl+Shift+v
|
||||||
Copy computer clipboard to device
|
Copy computer clipboard to device (and paste if the device runs Android >= 7)
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B Ctrl+i
|
.B Ctrl+i
|
||||||
|
@ -30,12 +30,24 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
" Default is %d.\n"
|
" Default is %d.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --codec-options key[:type]=value[,...]\n"
|
||||||
|
" Set a list of comma-separated key:type=value options for the\n"
|
||||||
|
" device encoder.\n"
|
||||||
|
" The possible values for 'type' are 'int' (default), 'long',\n"
|
||||||
|
" 'float' and 'string'.\n"
|
||||||
|
" The list of possible codec options is available in the\n"
|
||||||
|
" Android documentation:\n"
|
||||||
|
" <https://d.android.com/reference/android/media/MediaFormat>\n"
|
||||||
|
"\n"
|
||||||
" --crop width:height:x:y\n"
|
" --crop width:height:x:y\n"
|
||||||
" Crop the device screen on the server.\n"
|
" Crop the device screen on the server.\n"
|
||||||
" The values are expressed in the device natural orientation\n"
|
" The values are expressed in the device natural orientation\n"
|
||||||
" (typically, portrait for a phone, landscape for a tablet).\n"
|
" (typically, portrait for a phone, landscape for a tablet).\n"
|
||||||
" Any --max-size value is computed on the cropped size.\n"
|
" Any --max-size value is computed on the cropped size.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --disable-screensaver\n"
|
||||||
|
" Disable screensaver while scrcpy is running.\n"
|
||||||
|
"\n"
|
||||||
" --display id\n"
|
" --display id\n"
|
||||||
" Specify the display id to mirror.\n"
|
" Specify the display id to mirror.\n"
|
||||||
"\n"
|
"\n"
|
||||||
@ -45,6 +57,10 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" Default is 0.\n"
|
" Default is 0.\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" --force-adb-forward\n"
|
||||||
|
" Do not attempt to use \"adb reverse\" to connect to the\n"
|
||||||
|
" the device.\n"
|
||||||
|
"\n"
|
||||||
" -f, --fullscreen\n"
|
" -f, --fullscreen\n"
|
||||||
" Start in fullscreen.\n"
|
" Start in fullscreen.\n"
|
||||||
"\n"
|
"\n"
|
||||||
@ -136,9 +152,18 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
"\n"
|
"\n"
|
||||||
" -v, --version\n"
|
" -v, --version\n"
|
||||||
" Print the version of scrcpy.\n"
|
" Print the version of scrcpy.\n"
|
||||||
|
"\n"
|
||||||
|
" -V, --verbosity value\n"
|
||||||
|
" Set the log level (debug, info, warn or error).\n"
|
||||||
|
#ifndef NDEBUG
|
||||||
|
" Default is debug.\n"
|
||||||
|
#else
|
||||||
|
" Default is info.\n"
|
||||||
|
#endif
|
||||||
"\n"
|
"\n"
|
||||||
" -w, --stay-awake\n"
|
" -w, --stay-awake\n"
|
||||||
" Keep the device on while scrcpy is running.\n"
|
" Keep the device on while scrcpy is running, when the device\n"
|
||||||
|
" is plugged in.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --window-borderless\n"
|
" --window-borderless\n"
|
||||||
" Disable window decorations (display borderless window).\n"
|
" Disable window decorations (display borderless window).\n"
|
||||||
@ -210,6 +235,9 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" " CTRL_OR_CMD "+o\n"
|
" " CTRL_OR_CMD "+o\n"
|
||||||
" Turn device screen off (keep mirroring)\n"
|
" Turn device screen off (keep mirroring)\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
" " CTRL_OR_CMD "+Shift+o\n"
|
||||||
|
" Turn device screen on\n"
|
||||||
|
"\n"
|
||||||
" " CTRL_OR_CMD "+r\n"
|
" " CTRL_OR_CMD "+r\n"
|
||||||
" Rotate device screen\n"
|
" Rotate device screen\n"
|
||||||
"\n"
|
"\n"
|
||||||
@ -226,7 +254,8 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" Paste computer clipboard to device\n"
|
" Paste computer clipboard to device\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+Shift+v\n"
|
" " CTRL_OR_CMD "+Shift+v\n"
|
||||||
" Copy computer clipboard to device\n"
|
" Copy computer clipboard to device (and paste if the device\n"
|
||||||
|
" runs Android >= 7)\n"
|
||||||
"\n"
|
"\n"
|
||||||
" " CTRL_OR_CMD "+i\n"
|
" " CTRL_OR_CMD "+i\n"
|
||||||
" Enable/disable FPS counter (print frames/second in logs)\n"
|
" Enable/disable FPS counter (print frames/second in logs)\n"
|
||||||
@ -424,6 +453,32 @@ parse_display_id(const char *s, uint16_t *display_id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
parse_log_level(const char *s, enum sc_log_level *log_level) {
|
||||||
|
if (!strcmp(s, "debug")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_DEBUG;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "info")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_INFO;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "warn")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_WARN;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "error")) {
|
||||||
|
*log_level = SC_LOG_LEVEL_ERROR;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGE("Could not parse log level: %s", s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_record_format(const char *optarg, enum recorder_format *format) {
|
parse_record_format(const char *optarg, enum recorder_format *format) {
|
||||||
if (!strcmp(optarg, "mp4")) {
|
if (!strcmp(optarg, "mp4")) {
|
||||||
@ -472,14 +527,22 @@ guess_record_format(const char *filename) {
|
|||||||
#define OPT_ROTATION 1015
|
#define OPT_ROTATION 1015
|
||||||
#define OPT_RENDER_DRIVER 1016
|
#define OPT_RENDER_DRIVER 1016
|
||||||
#define OPT_NO_MIPMAPS 1017
|
#define OPT_NO_MIPMAPS 1017
|
||||||
|
#define OPT_CODEC_OPTIONS 1018
|
||||||
|
#define OPT_FORCE_ADB_FORWARD 1019
|
||||||
|
#define OPT_DISABLE_SCREENSAVER 1020
|
||||||
|
|
||||||
bool
|
bool
|
||||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
static const struct option long_options[] = {
|
static const struct option long_options[] = {
|
||||||
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
{"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
|
||||||
{"bit-rate", required_argument, NULL, 'b'},
|
{"bit-rate", required_argument, NULL, 'b'},
|
||||||
|
{"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
|
||||||
{"crop", required_argument, NULL, OPT_CROP},
|
{"crop", required_argument, NULL, OPT_CROP},
|
||||||
|
{"disable-screensaver", no_argument, NULL,
|
||||||
|
OPT_DISABLE_SCREENSAVER},
|
||||||
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
{"display", required_argument, NULL, OPT_DISPLAY_ID},
|
||||||
|
{"force-adb-forward", no_argument, NULL,
|
||||||
|
OPT_FORCE_ADB_FORWARD},
|
||||||
{"fullscreen", no_argument, NULL, 'f'},
|
{"fullscreen", no_argument, NULL, 'f'},
|
||||||
{"help", no_argument, NULL, 'h'},
|
{"help", no_argument, NULL, 'h'},
|
||||||
{"lock-video-orientation", required_argument, NULL,
|
{"lock-video-orientation", required_argument, NULL,
|
||||||
@ -502,6 +565,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
{"show-touches", no_argument, NULL, 't'},
|
{"show-touches", no_argument, NULL, 't'},
|
||||||
{"stay-awake", no_argument, NULL, 'w'},
|
{"stay-awake", no_argument, NULL, 'w'},
|
||||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||||
|
{"verbosity", required_argument, NULL, 'V'},
|
||||||
{"version", no_argument, NULL, 'v'},
|
{"version", no_argument, NULL, 'v'},
|
||||||
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
||||||
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
{"window-x", required_argument, NULL, OPT_WINDOW_X},
|
||||||
@ -518,8 +582,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
optind = 0; // reset to start from the first argument in tests
|
optind = 0; // reset to start from the first argument in tests
|
||||||
|
|
||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvw", long_options,
|
while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
|
||||||
NULL)) != -1) {
|
long_options, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'b':
|
case 'b':
|
||||||
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
||||||
@ -598,6 +662,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case 'v':
|
case 'v':
|
||||||
args->version = true;
|
args->version = true;
|
||||||
break;
|
break;
|
||||||
|
case 'V':
|
||||||
|
if (!parse_log_level(optarg, &opts->log_level)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'w':
|
case 'w':
|
||||||
opts->stay_awake = true;
|
opts->stay_awake = true;
|
||||||
break;
|
break;
|
||||||
@ -647,6 +716,15 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case OPT_NO_MIPMAPS:
|
case OPT_NO_MIPMAPS:
|
||||||
opts->mipmaps = false;
|
opts->mipmaps = false;
|
||||||
break;
|
break;
|
||||||
|
case OPT_CODEC_OPTIONS:
|
||||||
|
opts->codec_options = optarg;
|
||||||
|
break;
|
||||||
|
case OPT_FORCE_ADB_FORWARD:
|
||||||
|
opts->force_adb_forward = true;
|
||||||
|
break;
|
||||||
|
case OPT_DISABLE_SCREENSAVER:
|
||||||
|
opts->disable_screensaver = true;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
|
@ -20,9 +20,9 @@ write_position(uint8_t *buf, const struct position *position) {
|
|||||||
static size_t
|
static size_t
|
||||||
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
|
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
|
||||||
size_t len = utf8_truncation_index(utf8, max_len);
|
size_t len = utf8_truncation_index(utf8, max_len);
|
||||||
buffer_write16be(buf, (uint16_t) len);
|
buffer_write32be(buf, len);
|
||||||
memcpy(&buf[2], utf8, len);
|
memcpy(&buf[4], utf8, len);
|
||||||
return 2 + len;
|
return 4 + len;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint16_t
|
static uint16_t
|
||||||
@ -42,8 +42,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
|||||||
case CONTROL_MSG_TYPE_INJECT_KEYCODE:
|
case CONTROL_MSG_TYPE_INJECT_KEYCODE:
|
||||||
buf[1] = msg->inject_keycode.action;
|
buf[1] = msg->inject_keycode.action;
|
||||||
buffer_write32be(&buf[2], msg->inject_keycode.keycode);
|
buffer_write32be(&buf[2], msg->inject_keycode.keycode);
|
||||||
buffer_write32be(&buf[6], msg->inject_keycode.metastate);
|
buffer_write32be(&buf[6], msg->inject_keycode.repeat);
|
||||||
return 10;
|
buffer_write32be(&buf[10], msg->inject_keycode.metastate);
|
||||||
|
return 14;
|
||||||
case CONTROL_MSG_TYPE_INJECT_TEXT: {
|
case CONTROL_MSG_TYPE_INJECT_TEXT: {
|
||||||
size_t len =
|
size_t len =
|
||||||
write_string(msg->inject_text.text,
|
write_string(msg->inject_text.text,
|
||||||
@ -67,10 +68,11 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
|||||||
(uint32_t) msg->inject_scroll_event.vscroll);
|
(uint32_t) msg->inject_scroll_event.vscroll);
|
||||||
return 21;
|
return 21;
|
||||||
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
|
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
|
||||||
size_t len = write_string(msg->inject_text.text,
|
buf[1] = !!msg->set_clipboard.paste;
|
||||||
|
size_t len = write_string(msg->set_clipboard.text,
|
||||||
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
|
CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
|
||||||
&buf[1]);
|
&buf[2]);
|
||||||
return 1 + len;
|
return 2 + len;
|
||||||
}
|
}
|
||||||
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||||
buf[1] = msg->set_screen_power_mode.mode;
|
buf[1] = msg->set_screen_power_mode.mode;
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
#include "android/keycodes.h"
|
#include "android/keycodes.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
|
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
|
||||||
|
|
||||||
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
|
#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
|
||||||
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093
|
// type: 1 byte; paste flag: 1 byte; length: 4 bytes
|
||||||
#define CONTROL_MSG_SERIALIZED_MAX_SIZE \
|
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
|
||||||
(3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH)
|
|
||||||
|
|
||||||
#define POINTER_ID_MOUSE UINT64_C(-1);
|
#define POINTER_ID_MOUSE UINT64_C(-1);
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ struct control_msg {
|
|||||||
struct {
|
struct {
|
||||||
enum android_keyevent_action action;
|
enum android_keyevent_action action;
|
||||||
enum android_keycode keycode;
|
enum android_keycode keycode;
|
||||||
|
uint32_t repeat;
|
||||||
enum android_metastate metastate;
|
enum android_metastate metastate;
|
||||||
} inject_keycode;
|
} inject_keycode;
|
||||||
struct {
|
struct {
|
||||||
@ -62,6 +64,7 @@ struct control_msg {
|
|||||||
} inject_scroll_event;
|
} inject_scroll_event;
|
||||||
struct {
|
struct {
|
||||||
char *text; // owned, to be freed by SDL_free()
|
char *text; // owned, to be freed by SDL_free()
|
||||||
|
bool paste;
|
||||||
} set_clipboard;
|
} set_clipboard;
|
||||||
struct {
|
struct {
|
||||||
enum screen_power_mode mode;
|
enum screen_power_mode mode;
|
||||||
@ -69,7 +72,7 @@ struct control_msg {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE
|
// buf size must be at least CONTROL_MSG_MAX_SIZE
|
||||||
// return the number of bytes written
|
// return the number of bytes written
|
||||||
size_t
|
size_t
|
||||||
control_msg_serialize(const struct control_msg *msg, unsigned char *buf);
|
control_msg_serialize(const struct control_msg *msg, unsigned char *buf);
|
||||||
|
@ -60,7 +60,7 @@ controller_push_msg(struct controller *controller,
|
|||||||
static bool
|
static bool
|
||||||
process_msg(struct controller *controller,
|
process_msg(struct controller *controller,
|
||||||
const struct control_msg *msg) {
|
const struct control_msg *msg) {
|
||||||
unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE];
|
||||||
int length = control_msg_serialize(msg, serialized_msg);
|
int length = control_msg_serialize(msg, serialized_msg);
|
||||||
if (!length) {
|
if (!length) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
ssize_t
|
ssize_t
|
||||||
device_msg_deserialize(const unsigned char *buf, size_t len,
|
device_msg_deserialize(const unsigned char *buf, size_t len,
|
||||||
struct device_msg *msg) {
|
struct device_msg *msg) {
|
||||||
if (len < 3) {
|
if (len < 5) {
|
||||||
// at least type + empty string length
|
// at least type + empty string length
|
||||||
return 0; // not available
|
return 0; // not available
|
||||||
}
|
}
|
||||||
@ -17,8 +17,8 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
|
|||||||
msg->type = buf[0];
|
msg->type = buf[0];
|
||||||
switch (msg->type) {
|
switch (msg->type) {
|
||||||
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
||||||
uint16_t clipboard_len = buffer_read16be(&buf[1]);
|
size_t clipboard_len = buffer_read32be(&buf[1]);
|
||||||
if (clipboard_len > len - 3) {
|
if (clipboard_len > len - 5) {
|
||||||
return 0; // not available
|
return 0; // not available
|
||||||
}
|
}
|
||||||
char *text = SDL_malloc(clipboard_len + 1);
|
char *text = SDL_malloc(clipboard_len + 1);
|
||||||
@ -27,12 +27,12 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (clipboard_len) {
|
if (clipboard_len) {
|
||||||
memcpy(text, &buf[3], clipboard_len);
|
memcpy(text, &buf[5], clipboard_len);
|
||||||
}
|
}
|
||||||
text[clipboard_len] = '\0';
|
text[clipboard_len] = '\0';
|
||||||
|
|
||||||
msg->clipboard.text = text;
|
msg->clipboard.text = text;
|
||||||
return 3 + clipboard_len;
|
return 5 + clipboard_len;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
LOGW("Unknown device message type: %d", (int) msg->type);
|
LOGW("Unknown device message type: %d", (int) msg->type);
|
||||||
|
@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#define DEVICE_MSG_TEXT_MAX_LENGTH 4093
|
#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k
|
||||||
#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH)
|
// type: 1 byte; length: 4 bytes
|
||||||
|
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
|
||||||
|
|
||||||
enum device_msg_type {
|
enum device_msg_type {
|
||||||
DEVICE_MSG_TYPE_CLIPBOARD,
|
DEVICE_MSG_TYPE_CLIPBOARD,
|
||||||
|
@ -7,33 +7,6 @@
|
|||||||
#include "util/lock.h"
|
#include "util/lock.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
// Convert window coordinates (as provided by SDL_GetMouseState() to renderer
|
|
||||||
// coordinates (as provided in SDL mouse events)
|
|
||||||
//
|
|
||||||
// See my question:
|
|
||||||
// <https://stackoverflow.com/questions/49111054/how-to-get-mouse-position-on-mouse-wheel-event>
|
|
||||||
static void
|
|
||||||
convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) {
|
|
||||||
SDL_Rect viewport;
|
|
||||||
float scale_x, scale_y;
|
|
||||||
SDL_RenderGetViewport(renderer, &viewport);
|
|
||||||
SDL_RenderGetScale(renderer, &scale_x, &scale_y);
|
|
||||||
*x = (int) (*x / scale_x) - viewport.x;
|
|
||||||
*y = (int) (*y / scale_y) - viewport.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
static struct point
|
|
||||||
get_mouse_point(struct screen *screen) {
|
|
||||||
int x;
|
|
||||||
int y;
|
|
||||||
SDL_GetMouseState(&x, &y);
|
|
||||||
convert_to_renderer_coordinates(screen->renderer, &x, &y);
|
|
||||||
return (struct point) {
|
|
||||||
.x = x,
|
|
||||||
.y = y,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static const int ACTION_DOWN = 1;
|
static const int ACTION_DOWN = 1;
|
||||||
static const int ACTION_UP = 1 << 1;
|
static const int ACTION_UP = 1 << 1;
|
||||||
|
|
||||||
@ -139,7 +112,7 @@ request_device_clipboard(struct controller *controller) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
set_device_clipboard(struct controller *controller) {
|
set_device_clipboard(struct controller *controller, bool paste) {
|
||||||
char *text = SDL_GetClipboardText();
|
char *text = SDL_GetClipboardText();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
LOGW("Could not get clipboard text: %s", SDL_GetError());
|
||||||
@ -154,6 +127,7 @@ set_device_clipboard(struct controller *controller) {
|
|||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
|
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
|
||||||
msg.set_clipboard.text = text;
|
msg.set_clipboard.text = text;
|
||||||
|
msg.set_clipboard.paste = paste;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
SDL_free(text);
|
SDL_free(text);
|
||||||
@ -260,7 +234,7 @@ input_manager_process_text_input(struct input_manager *im,
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
||||||
bool prefer_text) {
|
bool prefer_text, uint32_t repeat) {
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
|
||||||
|
|
||||||
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
|
||||||
@ -273,6 +247,7 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
to->inject_keycode.repeat = repeat;
|
||||||
to->inject_keycode.metastate = convert_meta_state(mod);
|
to->inject_keycode.metastate = convert_meta_state(mod);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -347,8 +322,11 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_o:
|
case SDLK_o:
|
||||||
if (control && cmd && !shift && down) {
|
if (control && cmd && !repeat && down) {
|
||||||
set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF);
|
enum screen_power_mode mode = shift
|
||||||
|
? SCREEN_POWER_MODE_NORMAL
|
||||||
|
: SCREEN_POWER_MODE_OFF;
|
||||||
|
set_screen_power_mode(controller, mode);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_DOWN:
|
case SDLK_DOWN:
|
||||||
@ -364,12 +342,12 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_LEFT:
|
case SDLK_LEFT:
|
||||||
if (cmd && !shift && down) {
|
if (cmd && !shift && !repeat && down) {
|
||||||
rotate_client_left(im->screen);
|
rotate_client_left(im->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_RIGHT:
|
case SDLK_RIGHT:
|
||||||
if (cmd && !shift && down) {
|
if (cmd && !shift && !repeat && down) {
|
||||||
rotate_client_right(im->screen);
|
rotate_client_right(im->screen);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -381,8 +359,8 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
case SDLK_v:
|
case SDLK_v:
|
||||||
if (control && cmd && !repeat && down) {
|
if (control && cmd && !repeat && down) {
|
||||||
if (shift) {
|
if (shift) {
|
||||||
// store the text in the device clipboard
|
// store the text in the device clipboard and paste
|
||||||
set_device_clipboard(controller);
|
set_device_clipboard(controller, true);
|
||||||
} else {
|
} else {
|
||||||
// inject the text as input events
|
// inject the text as input events
|
||||||
clipboard_paste(controller);
|
clipboard_paste(controller);
|
||||||
@ -434,8 +412,14 @@ input_manager_process_key(struct input_manager *im,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event->repeat) {
|
||||||
|
++im->repeat;
|
||||||
|
} else {
|
||||||
|
im->repeat = 0;
|
||||||
|
}
|
||||||
|
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
if (convert_input_key(event, &msg, im->prefer_text)) {
|
if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) {
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (!controller_push_msg(controller, &msg)) {
|
||||||
LOGW("Could not request 'inject keycode'");
|
LOGW("Could not request 'inject keycode'");
|
||||||
}
|
}
|
||||||
@ -487,11 +471,17 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
|
|||||||
|
|
||||||
to->inject_touch_event.pointer_id = from->fingerId;
|
to->inject_touch_event.pointer_id = from->fingerId;
|
||||||
to->inject_touch_event.position.screen_size = screen->frame_size;
|
to->inject_touch_event.position.screen_size = screen->frame_size;
|
||||||
|
|
||||||
|
int ww;
|
||||||
|
int wh;
|
||||||
|
SDL_GL_GetDrawableSize(screen->window, &ww, &wh);
|
||||||
|
|
||||||
// SDL touch event coordinates are normalized in the range [0; 1]
|
// SDL touch event coordinates are normalized in the range [0; 1]
|
||||||
float x = from->x * screen->content_size.width;
|
int32_t x = from->x * ww;
|
||||||
float y = from->y * screen->content_size.height;
|
int32_t y = from->y * wh;
|
||||||
to->inject_touch_event.position.point =
|
to->inject_touch_event.position.point =
|
||||||
screen_convert_to_frame_coords(screen, x, y);
|
screen_convert_to_frame_coords(screen, x, y);
|
||||||
|
|
||||||
to->inject_touch_event.pressure = from->pressure;
|
to->inject_touch_event.pressure = from->pressure;
|
||||||
to->inject_touch_event.buttons = 0;
|
to->inject_touch_event.buttons = 0;
|
||||||
return true;
|
return true;
|
||||||
@ -508,13 +498,6 @@ input_manager_process_touch(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
is_outside_device_screen(struct input_manager *im, int x, int y)
|
|
||||||
{
|
|
||||||
return x < 0 || x >= im->screen->content_size.width ||
|
|
||||||
y < 0 || y >= im->screen->content_size.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
|
||||||
struct control_msg *to) {
|
struct control_msg *to) {
|
||||||
@ -552,10 +535,15 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// double-click on black borders resize to fit the device screen
|
// double-click on black borders resize to fit the device screen
|
||||||
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
||||||
bool outside =
|
int32_t x = event->x;
|
||||||
is_outside_device_screen(im, event->x, event->y);
|
int32_t y = event->y;
|
||||||
|
screen_hidpi_scale_coords(im->screen, &x, &y);
|
||||||
|
SDL_Rect *r = &im->screen->rect;
|
||||||
|
bool outside = x < r->x || x >= r->x + r->w
|
||||||
|
|| y < r->y || y >= r->y + r->h;
|
||||||
if (outside) {
|
if (outside) {
|
||||||
screen_resize_to_fit(im->screen);
|
screen_resize_to_fit(im->screen);
|
||||||
return;
|
return;
|
||||||
@ -579,9 +567,15 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
static bool
|
static bool
|
||||||
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
|
||||||
struct control_msg *to) {
|
struct control_msg *to) {
|
||||||
|
|
||||||
|
// mouse_x and mouse_y are expressed in pixels relative to the window
|
||||||
|
int mouse_x;
|
||||||
|
int mouse_y;
|
||||||
|
SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||||
|
|
||||||
struct position position = {
|
struct position position = {
|
||||||
.screen_size = screen->frame_size,
|
.screen_size = screen->frame_size,
|
||||||
.point = get_mouse_point(screen),
|
.point = screen_convert_to_frame_coords(screen, mouse_x, mouse_y),
|
||||||
};
|
};
|
||||||
|
|
||||||
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
|
||||||
|
@ -14,6 +14,11 @@ struct input_manager {
|
|||||||
struct controller *controller;
|
struct controller *controller;
|
||||||
struct video_buffer *video_buffer;
|
struct video_buffer *video_buffer;
|
||||||
struct screen *screen;
|
struct screen *screen;
|
||||||
|
|
||||||
|
// SDL reports repeated events as a boolean, but Android expects the actual
|
||||||
|
// number of repetitions. This variable keeps track of the count.
|
||||||
|
unsigned repeat;
|
||||||
|
|
||||||
bool prefer_text;
|
bool prefer_text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,6 +29,24 @@ print_version(void) {
|
|||||||
LIBAVUTIL_VERSION_MICRO);
|
LIBAVUTIL_VERSION_MICRO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SDL_LogPriority
|
||||||
|
convert_log_level_to_sdl(enum sc_log_level level) {
|
||||||
|
switch (level) {
|
||||||
|
case SC_LOG_LEVEL_DEBUG:
|
||||||
|
return SDL_LOG_PRIORITY_DEBUG;
|
||||||
|
case SC_LOG_LEVEL_INFO:
|
||||||
|
return SDL_LOG_PRIORITY_INFO;
|
||||||
|
case SC_LOG_LEVEL_WARN:
|
||||||
|
return SDL_LOG_PRIORITY_WARN;
|
||||||
|
case SC_LOG_LEVEL_ERROR:
|
||||||
|
return SDL_LOG_PRIORITY_ERROR;
|
||||||
|
default:
|
||||||
|
assert(!"unexpected log level");
|
||||||
|
return SDL_LOG_PRIORITY_INFO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char *argv[]) {
|
main(int argc, char *argv[]) {
|
||||||
#ifdef __WINDOWS__
|
#ifdef __WINDOWS__
|
||||||
@ -38,20 +56,23 @@ main(int argc, char *argv[]) {
|
|||||||
setbuf(stderr, NULL);
|
setbuf(stderr, NULL);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef NDEBUG
|
|
||||||
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct scrcpy_cli_args args = {
|
struct scrcpy_cli_args args = {
|
||||||
.opts = SCRCPY_OPTIONS_DEFAULT,
|
.opts = SCRCPY_OPTIONS_DEFAULT,
|
||||||
.help = false,
|
.help = false,
|
||||||
.version = false,
|
.version = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
args.opts.log_level = SC_LOG_LEVEL_DEBUG;
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!scrcpy_parse_args(&args, argc, argv)) {
|
if (!scrcpy_parse_args(&args, argc, argv)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_LogPriority sdl_log = convert_log_level_to_sdl(args.opts.log_level);
|
||||||
|
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
|
||||||
|
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
scrcpy_print_usage(argv[0]);
|
scrcpy_print_usage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -60,28 +60,29 @@ static int
|
|||||||
run_receiver(void *data) {
|
run_receiver(void *data) {
|
||||||
struct receiver *receiver = data;
|
struct receiver *receiver = data;
|
||||||
|
|
||||||
unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE];
|
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
|
||||||
size_t head = 0;
|
size_t head = 0;
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE);
|
assert(head < DEVICE_MSG_MAX_SIZE);
|
||||||
ssize_t r = net_recv(receiver->control_socket, buf,
|
ssize_t r = net_recv(receiver->control_socket, buf + head,
|
||||||
DEVICE_MSG_SERIALIZED_MAX_SIZE - head);
|
DEVICE_MSG_MAX_SIZE - head);
|
||||||
if (r <= 0) {
|
if (r <= 0) {
|
||||||
LOGD("Receiver stopped");
|
LOGD("Receiver stopped");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t consumed = process_msgs(buf, r);
|
head += r;
|
||||||
|
ssize_t consumed = process_msgs(buf, head);
|
||||||
if (consumed == -1) {
|
if (consumed == -1) {
|
||||||
// an error occurred
|
// an error occurred
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (consumed) {
|
if (consumed) {
|
||||||
|
head -= consumed;
|
||||||
// shift the remaining data in the buffer
|
// shift the remaining data in the buffer
|
||||||
memmove(buf, &buf[consumed], r - consumed);
|
memmove(buf, &buf[consumed], head);
|
||||||
head = r - consumed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ static struct input_manager input_manager = {
|
|||||||
.controller = &controller,
|
.controller = &controller,
|
||||||
.video_buffer = &video_buffer,
|
.video_buffer = &video_buffer,
|
||||||
.screen = &screen,
|
.screen = &screen,
|
||||||
|
.repeat = 0,
|
||||||
.prefer_text = false, // initialized later
|
.prefer_text = false, // initialized later
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,7 +64,8 @@ BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
|
|||||||
|
|
||||||
// init SDL and set appropriate hints
|
// init SDL and set appropriate hints
|
||||||
static bool
|
static bool
|
||||||
sdl_init_and_configure(bool display, const char *render_driver) {
|
sdl_init_and_configure(bool display, const char *render_driver,
|
||||||
|
bool disable_screensaver) {
|
||||||
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
|
uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
|
||||||
if (SDL_Init(flags)) {
|
if (SDL_Init(flags)) {
|
||||||
LOGC("Could not initialize SDL: %s", SDL_GetError());
|
LOGC("Could not initialize SDL: %s", SDL_GetError());
|
||||||
@ -112,8 +114,13 @@ sdl_init_and_configure(bool display, const char *render_driver) {
|
|||||||
LOGW("Could not disable minimize on focus loss");
|
LOGW("Could not disable minimize on focus loss");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not disable the screensaver when scrcpy is running
|
if (disable_screensaver) {
|
||||||
|
LOGD("Screensaver disabled");
|
||||||
|
SDL_DisableScreenSaver();
|
||||||
|
} else {
|
||||||
|
LOGD("Screensaver enabled");
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -136,7 +143,7 @@ event_watcher(void *data, SDL_Event *event) {
|
|||||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||||
// In practice, it seems to always be called from the same thread in
|
// In practice, it seems to always be called from the same thread in
|
||||||
// that specific case. Anyway, it's just a workaround.
|
// that specific case. Anyway, it's just a workaround.
|
||||||
screen_render(&screen);
|
screen_render(&screen, true);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -293,6 +300,7 @@ bool
|
|||||||
scrcpy(const struct scrcpy_options *options) {
|
scrcpy(const struct scrcpy_options *options) {
|
||||||
bool record = !!options->record_filename;
|
bool record = !!options->record_filename;
|
||||||
struct server_params params = {
|
struct server_params params = {
|
||||||
|
.log_level = options->log_level,
|
||||||
.crop = options->crop,
|
.crop = options->crop,
|
||||||
.port_range = options->port_range,
|
.port_range = options->port_range,
|
||||||
.max_size = options->max_size,
|
.max_size = options->max_size,
|
||||||
@ -303,6 +311,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
.display_id = options->display_id,
|
.display_id = options->display_id,
|
||||||
.show_touches = options->show_touches,
|
.show_touches = options->show_touches,
|
||||||
.stay_awake = options->stay_awake,
|
.stay_awake = options->stay_awake,
|
||||||
|
.codec_options = options->codec_options,
|
||||||
|
.force_adb_forward = options->force_adb_forward,
|
||||||
};
|
};
|
||||||
if (!server_start(&server, options->serial, ¶ms)) {
|
if (!server_start(&server, options->serial, ¶ms)) {
|
||||||
return false;
|
return false;
|
||||||
@ -318,7 +328,8 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
bool controller_initialized = false;
|
bool controller_initialized = false;
|
||||||
bool controller_started = false;
|
bool controller_started = false;
|
||||||
|
|
||||||
if (!sdl_init_and_configure(options->display, options->render_driver)) {
|
if (!sdl_init_and_configure(options->display, options->render_driver,
|
||||||
|
options->disable_screensaver)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
#include "recorder.h"
|
#include "recorder.h"
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
struct scrcpy_options {
|
struct scrcpy_options {
|
||||||
const char *serial;
|
const char *serial;
|
||||||
@ -16,6 +17,8 @@ struct scrcpy_options {
|
|||||||
const char *window_title;
|
const char *window_title;
|
||||||
const char *push_target;
|
const char *push_target;
|
||||||
const char *render_driver;
|
const char *render_driver;
|
||||||
|
const char *codec_options;
|
||||||
|
enum sc_log_level log_level;
|
||||||
enum recorder_format record_format;
|
enum recorder_format record_format;
|
||||||
struct port_range port_range;
|
struct port_range port_range;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
@ -39,6 +42,8 @@ struct scrcpy_options {
|
|||||||
bool window_borderless;
|
bool window_borderless;
|
||||||
bool mipmaps;
|
bool mipmaps;
|
||||||
bool stay_awake;
|
bool stay_awake;
|
||||||
|
bool force_adb_forward;
|
||||||
|
bool disable_screensaver;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SCRCPY_OPTIONS_DEFAULT { \
|
#define SCRCPY_OPTIONS_DEFAULT { \
|
||||||
@ -48,6 +53,8 @@ struct scrcpy_options {
|
|||||||
.window_title = NULL, \
|
.window_title = NULL, \
|
||||||
.push_target = NULL, \
|
.push_target = NULL, \
|
||||||
.render_driver = NULL, \
|
.render_driver = NULL, \
|
||||||
|
.codec_options = NULL, \
|
||||||
|
.log_level = SC_LOG_LEVEL_INFO, \
|
||||||
.record_format = RECORDER_FORMAT_AUTO, \
|
.record_format = RECORDER_FORMAT_AUTO, \
|
||||||
.port_range = { \
|
.port_range = { \
|
||||||
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
|
||||||
@ -74,6 +81,8 @@ struct scrcpy_options {
|
|||||||
.window_borderless = false, \
|
.window_borderless = false, \
|
||||||
.mipmaps = true, \
|
.mipmaps = true, \
|
||||||
.stay_awake = false, \
|
.stay_awake = false, \
|
||||||
|
.force_adb_forward = false, \
|
||||||
|
.disable_screensaver = false, \
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
308
app/src/screen.c
308
app/src/screen.c
@ -30,10 +30,10 @@ get_rotated_size(struct size size, int rotation) {
|
|||||||
|
|
||||||
// get the window size in a struct size
|
// get the window size in a struct size
|
||||||
static struct size
|
static struct size
|
||||||
get_window_size(SDL_Window *window) {
|
get_window_size(const struct screen *screen) {
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
SDL_GetWindowSize(window, &width, &height);
|
SDL_GetWindowSize(screen->window, &width, &height);
|
||||||
|
|
||||||
struct size size;
|
struct size size;
|
||||||
size.width = width;
|
size.width = width;
|
||||||
@ -41,31 +41,12 @@ get_window_size(SDL_Window *window) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the windowed window size
|
|
||||||
static struct size
|
|
||||||
get_windowed_window_size(const struct screen *screen) {
|
|
||||||
if (screen->fullscreen || screen->maximized) {
|
|
||||||
return screen->windowed_window_size;
|
|
||||||
}
|
|
||||||
return get_window_size(screen->window);
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply the windowed window size if fullscreen and maximized are disabled
|
|
||||||
static void
|
|
||||||
apply_windowed_size(struct screen *screen) {
|
|
||||||
if (!screen->fullscreen && !screen->maximized) {
|
|
||||||
SDL_SetWindowSize(screen->window, screen->windowed_window_size.width,
|
|
||||||
screen->windowed_window_size.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the window size to be applied when fullscreen is disabled
|
// set the window size to be applied when fullscreen is disabled
|
||||||
static void
|
static void
|
||||||
set_window_size(struct screen *screen, struct size new_size) {
|
set_window_size(struct screen *screen, struct size new_size) {
|
||||||
// setting the window size during fullscreen is implementation defined,
|
assert(!screen->fullscreen);
|
||||||
// so apply the resize only after fullscreen is disabled
|
assert(!screen->maximized);
|
||||||
screen->windowed_window_size = new_size;
|
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
|
||||||
apply_windowed_size(screen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
||||||
@ -87,6 +68,16 @@ get_preferred_display_bounds(struct size *bounds) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_optimal_size(struct size current_size, struct size content_size) {
|
||||||
|
// The size is optimal if we can recompute one dimension of the current
|
||||||
|
// size from the other
|
||||||
|
return current_size.height == current_size.width * content_size.height
|
||||||
|
/ content_size.width
|
||||||
|
|| current_size.width == current_size.height * content_size.width
|
||||||
|
/ content_size.height;
|
||||||
|
}
|
||||||
|
|
||||||
// return the optimal size of the window, with the following constraints:
|
// return the optimal size of the window, with the following constraints:
|
||||||
// - it attempts to keep at least one dimension of the current_size (i.e. it
|
// - it attempts to keep at least one dimension of the current_size (i.e. it
|
||||||
// crops the black borders)
|
// crops the black borders)
|
||||||
@ -99,47 +90,43 @@ get_optimal_size(struct size current_size, struct size content_size) {
|
|||||||
return current_size;
|
return current_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct size display_size;
|
struct size window_size;
|
||||||
// 32 bits because we need to multiply two 16 bits values
|
|
||||||
uint32_t w;
|
|
||||||
uint32_t h;
|
|
||||||
|
|
||||||
|
struct size display_size;
|
||||||
if (!get_preferred_display_bounds(&display_size)) {
|
if (!get_preferred_display_bounds(&display_size)) {
|
||||||
// could not get display bounds, do not constraint the size
|
// could not get display bounds, do not constraint the size
|
||||||
w = current_size.width;
|
window_size.width = current_size.width;
|
||||||
h = current_size.height;
|
window_size.height = current_size.height;
|
||||||
} else {
|
} else {
|
||||||
w = MIN(current_size.width, display_size.width);
|
window_size.width = MIN(current_size.width, display_size.width);
|
||||||
h = MIN(current_size.height, display_size.height);
|
window_size.height = MIN(current_size.height, display_size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (h == w * content_size.height / content_size.width
|
if (is_optimal_size(window_size, content_size)) {
|
||||||
|| w == h * content_size.width / content_size.height) {
|
return window_size;
|
||||||
// The size is already optimal, if we ignore rounding errors due to
|
|
||||||
// integer window dimensions
|
|
||||||
return (struct size) {w, h};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool keep_width = content_size.width * h > content_size.height * w;
|
bool keep_width = content_size.width * window_size.height
|
||||||
|
> content_size.height * window_size.width;
|
||||||
if (keep_width) {
|
if (keep_width) {
|
||||||
// remove black borders on top and bottom
|
// remove black borders on top and bottom
|
||||||
h = content_size.height * w / content_size.width;
|
window_size.height = content_size.height * window_size.width
|
||||||
|
/ content_size.width;
|
||||||
} else {
|
} else {
|
||||||
// remove black borders on left and right (or none at all if it already
|
// remove black borders on left and right (or none at all if it already
|
||||||
// fits)
|
// fits)
|
||||||
w = content_size.width * h / content_size.height;
|
window_size.width = content_size.width * window_size.height
|
||||||
|
/ content_size.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// w and h must fit into 16 bits
|
return window_size;
|
||||||
assert(w < 0x10000 && h < 0x10000);
|
|
||||||
return (struct size) {w, h};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// same as get_optimal_size(), but read the current size from the window
|
// same as get_optimal_size(), but read the current size from the window
|
||||||
static inline struct size
|
static inline struct size
|
||||||
get_optimal_window_size(const struct screen *screen, struct size content_size) {
|
get_optimal_window_size(const struct screen *screen, struct size content_size) {
|
||||||
struct size windowed_size = get_windowed_window_size(screen);
|
struct size window_size = get_window_size(screen);
|
||||||
return get_optimal_size(windowed_size, content_size);
|
return get_optimal_size(window_size, content_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// initially, there is no current size, so use the frame size as current size
|
// initially, there is no current size, so use the frame size as current size
|
||||||
@ -169,6 +156,43 @@ get_initial_optimal_size(struct size content_size, uint16_t req_width,
|
|||||||
return window_size;
|
return window_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
screen_update_content_rect(struct screen *screen) {
|
||||||
|
int dw;
|
||||||
|
int dh;
|
||||||
|
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
|
||||||
|
|
||||||
|
struct size content_size = screen->content_size;
|
||||||
|
// The drawable size is the window size * the HiDPI scale
|
||||||
|
struct size drawable_size = {dw, dh};
|
||||||
|
|
||||||
|
SDL_Rect *rect = &screen->rect;
|
||||||
|
|
||||||
|
if (is_optimal_size(drawable_size, content_size)) {
|
||||||
|
rect->x = 0;
|
||||||
|
rect->y = 0;
|
||||||
|
rect->w = drawable_size.width;
|
||||||
|
rect->h = drawable_size.height;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool keep_width = content_size.width * drawable_size.height
|
||||||
|
> content_size.height * drawable_size.width;
|
||||||
|
if (keep_width) {
|
||||||
|
rect->x = 0;
|
||||||
|
rect->w = drawable_size.width;
|
||||||
|
rect->h = drawable_size.width * content_size.height
|
||||||
|
/ content_size.width;
|
||||||
|
rect->y = (drawable_size.height - rect->h) / 2;
|
||||||
|
} else {
|
||||||
|
rect->y = 0;
|
||||||
|
rect->h = drawable_size.height;
|
||||||
|
rect->w = drawable_size.height * content_size.width
|
||||||
|
/ content_size.height;
|
||||||
|
rect->x = (drawable_size.width - rect->w) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_init(struct screen *screen) {
|
screen_init(struct screen *screen) {
|
||||||
*screen = (struct screen) SCREEN_INITIALIZER;
|
*screen = (struct screen) SCREEN_INITIALIZER;
|
||||||
@ -193,7 +217,7 @@ create_texture(struct screen *screen) {
|
|||||||
// Enable trilinear filtering for downscaling
|
// Enable trilinear filtering for downscaling
|
||||||
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
||||||
GL_LINEAR_MIPMAP_LINEAR);
|
GL_LINEAR_MIPMAP_LINEAR);
|
||||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -.5f);
|
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
||||||
|
|
||||||
SDL_GL_UnbindTexture(texture);
|
SDL_GL_UnbindTexture(texture);
|
||||||
}
|
}
|
||||||
@ -258,13 +282,6 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
const char *renderer_name = r ? NULL : renderer_info.name;
|
const char *renderer_name = r ? NULL : renderer_info.name;
|
||||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||||
|
|
||||||
if (SDL_RenderSetLogicalSize(screen->renderer, content_size.width,
|
|
||||||
content_size.height)) {
|
|
||||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
|
||||||
screen_destroy(screen);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// starts with "opengl"
|
// starts with "opengl"
|
||||||
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||||
if (screen->use_opengl) {
|
if (screen->use_opengl) {
|
||||||
@ -288,7 +305,7 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
LOGI("Trilinear filtering disabled");
|
LOGI("Trilinear filtering disabled");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOGW("Trilinear filtering disabled (not an OpenGL renderer)");
|
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Surface *icon = read_xpm(icon_xpm);
|
SDL_Surface *icon = read_xpm(icon_xpm);
|
||||||
@ -308,7 +325,12 @@ screen_init_rendering(struct screen *screen, const char *window_title,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->windowed_window_size = window_size;
|
// Reset the window size to trigger a SIZE_CHANGED event, to workaround
|
||||||
|
// HiDPI issues with some SDL renderers when several displays having
|
||||||
|
// different HiDPI scaling are connected
|
||||||
|
SDL_SetWindowSize(screen->window, window_size.width, window_size.height);
|
||||||
|
|
||||||
|
screen_update_content_rect(screen);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -331,6 +353,45 @@ screen_destroy(struct screen *screen) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
resize_for_content(struct screen *screen, struct size old_content_size,
|
||||||
|
struct size new_content_size) {
|
||||||
|
struct size window_size = get_window_size(screen);
|
||||||
|
struct size target_size = {
|
||||||
|
.width = (uint32_t) window_size.width * new_content_size.width
|
||||||
|
/ old_content_size.width,
|
||||||
|
.height = (uint32_t) window_size.height * new_content_size.height
|
||||||
|
/ old_content_size.height,
|
||||||
|
};
|
||||||
|
target_size = get_optimal_size(target_size, new_content_size);
|
||||||
|
set_window_size(screen, target_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
set_content_size(struct screen *screen, struct size new_content_size) {
|
||||||
|
if (!screen->fullscreen && !screen->maximized) {
|
||||||
|
resize_for_content(screen, screen->content_size, new_content_size);
|
||||||
|
} else if (!screen->resize_pending) {
|
||||||
|
// Store the windowed size to be able to compute the optimal size once
|
||||||
|
// fullscreen and maximized are disabled
|
||||||
|
screen->windowed_content_size = screen->content_size;
|
||||||
|
screen->resize_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
screen->content_size = new_content_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
apply_pending_resize(struct screen *screen) {
|
||||||
|
assert(!screen->fullscreen);
|
||||||
|
assert(!screen->maximized);
|
||||||
|
if (screen->resize_pending) {
|
||||||
|
resize_for_content(screen, screen->windowed_content_size,
|
||||||
|
screen->content_size);
|
||||||
|
screen->resize_pending = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_set_rotation(struct screen *screen, unsigned rotation) {
|
screen_set_rotation(struct screen *screen, unsigned rotation) {
|
||||||
assert(rotation < 4);
|
assert(rotation < 4);
|
||||||
@ -338,32 +399,15 @@ screen_set_rotation(struct screen *screen, unsigned rotation) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct size old_content_size = screen->content_size;
|
|
||||||
struct size new_content_size =
|
struct size new_content_size =
|
||||||
get_rotated_size(screen->frame_size, rotation);
|
get_rotated_size(screen->frame_size, rotation);
|
||||||
|
|
||||||
if (SDL_RenderSetLogicalSize(screen->renderer,
|
set_content_size(screen, new_content_size);
|
||||||
new_content_size.width,
|
|
||||||
new_content_size.height)) {
|
|
||||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct size windowed_size = get_windowed_window_size(screen);
|
|
||||||
struct size target_size = {
|
|
||||||
.width = (uint32_t) windowed_size.width * new_content_size.width
|
|
||||||
/ old_content_size.width,
|
|
||||||
.height = (uint32_t) windowed_size.height * new_content_size.height
|
|
||||||
/ old_content_size.height,
|
|
||||||
};
|
|
||||||
target_size = get_optimal_size(target_size, new_content_size);
|
|
||||||
set_window_size(screen, target_size);
|
|
||||||
|
|
||||||
screen->content_size = new_content_size;
|
|
||||||
screen->rotation = rotation;
|
screen->rotation = rotation;
|
||||||
LOGI("Display rotation set to %u", rotation);
|
LOGI("Display rotation set to %u", rotation);
|
||||||
|
|
||||||
screen_render(screen);
|
screen_render(screen, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate the texture and resize the window if the frame size has changed
|
// recreate the texture and resize the window if the frame size has changed
|
||||||
@ -371,31 +415,16 @@ static bool
|
|||||||
prepare_for_frame(struct screen *screen, struct size new_frame_size) {
|
prepare_for_frame(struct screen *screen, struct size new_frame_size) {
|
||||||
if (screen->frame_size.width != new_frame_size.width
|
if (screen->frame_size.width != new_frame_size.width
|
||||||
|| screen->frame_size.height != new_frame_size.height) {
|
|| screen->frame_size.height != new_frame_size.height) {
|
||||||
struct size new_content_size =
|
|
||||||
get_rotated_size(new_frame_size, screen->rotation);
|
|
||||||
if (SDL_RenderSetLogicalSize(screen->renderer,
|
|
||||||
new_content_size.width,
|
|
||||||
new_content_size.height)) {
|
|
||||||
LOGE("Could not set renderer logical size: %s", SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// frame dimension changed, destroy texture
|
// frame dimension changed, destroy texture
|
||||||
SDL_DestroyTexture(screen->texture);
|
SDL_DestroyTexture(screen->texture);
|
||||||
|
|
||||||
struct size content_size = screen->content_size;
|
|
||||||
struct size windowed_size = get_windowed_window_size(screen);
|
|
||||||
struct size target_size = {
|
|
||||||
(uint32_t) windowed_size.width * new_content_size.width
|
|
||||||
/ content_size.width,
|
|
||||||
(uint32_t) windowed_size.height * new_content_size.height
|
|
||||||
/ content_size.height,
|
|
||||||
};
|
|
||||||
target_size = get_optimal_size(target_size, new_content_size);
|
|
||||||
set_window_size(screen, target_size);
|
|
||||||
|
|
||||||
screen->frame_size = new_frame_size;
|
screen->frame_size = new_frame_size;
|
||||||
screen->content_size = new_content_size;
|
|
||||||
|
struct size new_content_size =
|
||||||
|
get_rotated_size(new_frame_size, screen->rotation);
|
||||||
|
set_content_size(screen, new_content_size);
|
||||||
|
|
||||||
|
screen_update_content_rect(screen);
|
||||||
|
|
||||||
LOGI("New texture: %" PRIu16 "x%" PRIu16,
|
LOGI("New texture: %" PRIu16 "x%" PRIu16,
|
||||||
screen->frame_size.width, screen->frame_size.height);
|
screen->frame_size.width, screen->frame_size.height);
|
||||||
@ -437,15 +466,19 @@ screen_update_frame(struct screen *screen, struct video_buffer *vb) {
|
|||||||
update_texture(screen, frame);
|
update_texture(screen, frame);
|
||||||
mutex_unlock(vb->mutex);
|
mutex_unlock(vb->mutex);
|
||||||
|
|
||||||
screen_render(screen);
|
screen_render(screen, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_render(struct screen *screen) {
|
screen_render(struct screen *screen, bool update_content_rect) {
|
||||||
|
if (update_content_rect) {
|
||||||
|
screen_update_content_rect(screen);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_RenderClear(screen->renderer);
|
SDL_RenderClear(screen->renderer);
|
||||||
if (screen->rotation == 0) {
|
if (screen->rotation == 0) {
|
||||||
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
|
SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
|
||||||
} else {
|
} else {
|
||||||
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
||||||
// counterclockwise (to be consistent with --lock-video-orientation)
|
// counterclockwise (to be consistent with --lock-video-orientation)
|
||||||
@ -455,12 +488,14 @@ screen_render(struct screen *screen) {
|
|||||||
SDL_Rect *dstrect = NULL;
|
SDL_Rect *dstrect = NULL;
|
||||||
SDL_Rect rect;
|
SDL_Rect rect;
|
||||||
if (screen->rotation & 1) {
|
if (screen->rotation & 1) {
|
||||||
struct size size = screen->content_size;
|
rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
|
||||||
rect.x = (size.width - size.height) / 2;
|
rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
|
||||||
rect.y = (size.height - size.width) / 2;
|
rect.w = screen->rect.h;
|
||||||
rect.w = size.height;
|
rect.h = screen->rect.w;
|
||||||
rect.h = size.width;
|
|
||||||
dstrect = ▭
|
dstrect = ▭
|
||||||
|
} else {
|
||||||
|
assert(screen->rotation == 2);
|
||||||
|
dstrect = &screen->rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
|
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
|
||||||
@ -478,23 +513,20 @@ screen_switch_fullscreen(struct screen *screen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
screen->fullscreen = !screen->fullscreen;
|
screen->fullscreen = !screen->fullscreen;
|
||||||
apply_windowed_size(screen);
|
if (!screen->fullscreen && !screen->maximized) {
|
||||||
|
apply_pending_resize(screen);
|
||||||
|
}
|
||||||
|
|
||||||
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
||||||
screen_render(screen);
|
screen_render(screen, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_resize_to_fit(struct screen *screen) {
|
screen_resize_to_fit(struct screen *screen) {
|
||||||
if (screen->fullscreen) {
|
if (screen->fullscreen || screen->maximized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screen->maximized) {
|
|
||||||
SDL_RestoreWindow(screen->window);
|
|
||||||
screen->maximized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct size optimal_size =
|
struct size optimal_size =
|
||||||
get_optimal_window_size(screen, screen->content_size);
|
get_optimal_window_size(screen, screen->content_size);
|
||||||
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
|
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
|
||||||
@ -524,39 +556,24 @@ screen_handle_window_event(struct screen *screen,
|
|||||||
const SDL_WindowEvent *event) {
|
const SDL_WindowEvent *event) {
|
||||||
switch (event->event) {
|
switch (event->event) {
|
||||||
case SDL_WINDOWEVENT_EXPOSED:
|
case SDL_WINDOWEVENT_EXPOSED:
|
||||||
screen_render(screen);
|
screen_render(screen, true);
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||||
if (!screen->fullscreen && !screen->maximized) {
|
screen_render(screen, true);
|
||||||
// Backup the previous size: if we receive the MAXIMIZED event,
|
|
||||||
// then the new size must be ignored (it's the maximized size).
|
|
||||||
// We could not rely on the window flags due to race conditions
|
|
||||||
// (they could be updated asynchronously, at least on X11).
|
|
||||||
screen->windowed_window_size_backup =
|
|
||||||
screen->windowed_window_size;
|
|
||||||
|
|
||||||
// Save the windowed size, so that it is available once the
|
|
||||||
// window is maximized or fullscreen is enabled.
|
|
||||||
screen->windowed_window_size = get_window_size(screen->window);
|
|
||||||
}
|
|
||||||
screen_render(screen);
|
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT_MAXIMIZED:
|
case SDL_WINDOWEVENT_MAXIMIZED:
|
||||||
// The backup size must be non-nul.
|
|
||||||
assert(screen->windowed_window_size_backup.width);
|
|
||||||
assert(screen->windowed_window_size_backup.height);
|
|
||||||
// Revert the last size, it was updated while screen was maximized.
|
|
||||||
screen->windowed_window_size = screen->windowed_window_size_backup;
|
|
||||||
#ifdef DEBUG
|
|
||||||
// Reset the backup to invalid values to detect unexpected usage
|
|
||||||
screen->windowed_window_size_backup.width = 0;
|
|
||||||
screen->windowed_window_size_backup.height = 0;
|
|
||||||
#endif
|
|
||||||
screen->maximized = true;
|
screen->maximized = true;
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT_RESTORED:
|
case SDL_WINDOWEVENT_RESTORED:
|
||||||
|
if (screen->fullscreen) {
|
||||||
|
// On Windows, in maximized+fullscreen, disabling fullscreen
|
||||||
|
// mode unexpectedly triggers the "restored" then "maximized"
|
||||||
|
// events, leaving the window in a weird state (maximized
|
||||||
|
// according to the events, but not maximized visually).
|
||||||
|
break;
|
||||||
|
}
|
||||||
screen->maximized = false;
|
screen->maximized = false;
|
||||||
apply_windowed_size(screen);
|
apply_pending_resize(screen);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -568,6 +585,13 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
|
|||||||
|
|
||||||
int32_t w = screen->content_size.width;
|
int32_t w = screen->content_size.width;
|
||||||
int32_t h = screen->content_size.height;
|
int32_t h = screen->content_size.height;
|
||||||
|
|
||||||
|
screen_hidpi_scale_coords(screen, &x, &y);
|
||||||
|
|
||||||
|
x = (int64_t) (x - screen->rect.x) * w / screen->rect.w;
|
||||||
|
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
|
||||||
|
|
||||||
|
// rotate
|
||||||
struct point result;
|
struct point result;
|
||||||
switch (rotation) {
|
switch (rotation) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -590,3 +614,15 @@ screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) {
|
||||||
|
// take the HiDPI scaling (dw/ww and dh/wh) into account
|
||||||
|
int ww, wh, dw, dh;
|
||||||
|
SDL_GetWindowSize(screen->window, &ww, &wh);
|
||||||
|
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
|
||||||
|
|
||||||
|
// scale for HiDPI (64 bits for intermediate multiplications)
|
||||||
|
*x = (int64_t) *x * dw / ww;
|
||||||
|
*y = (int64_t) *y * dh / wh;
|
||||||
|
}
|
||||||
|
@ -21,13 +21,16 @@ struct screen {
|
|||||||
struct sc_opengl gl;
|
struct sc_opengl gl;
|
||||||
struct size frame_size;
|
struct size frame_size;
|
||||||
struct size content_size; // rotated frame_size
|
struct size content_size; // rotated frame_size
|
||||||
// The window size the last time it was not maximized or fullscreen.
|
|
||||||
struct size windowed_window_size;
|
bool resize_pending; // resize requested while fullscreen or maximized
|
||||||
// Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be
|
// The content size the last time the window was not maximized or
|
||||||
// able to revert the size to its non-maximized value.
|
// fullscreen (meaningful only when resize_pending is true)
|
||||||
struct size windowed_window_size_backup;
|
struct size windowed_content_size;
|
||||||
|
|
||||||
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
|
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
|
||||||
unsigned rotation;
|
unsigned rotation;
|
||||||
|
// rectangle of the content (excluding black borders)
|
||||||
|
struct SDL_Rect rect;
|
||||||
bool has_frame;
|
bool has_frame;
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool maximized;
|
bool maximized;
|
||||||
@ -49,15 +52,18 @@ struct screen {
|
|||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.windowed_window_size = { \
|
.resize_pending = false, \
|
||||||
.width = 0, \
|
.windowed_content_size = { \
|
||||||
.height = 0, \
|
|
||||||
}, \
|
|
||||||
.windowed_window_size_backup = { \
|
|
||||||
.width = 0, \
|
.width = 0, \
|
||||||
.height = 0, \
|
.height = 0, \
|
||||||
}, \
|
}, \
|
||||||
.rotation = 0, \
|
.rotation = 0, \
|
||||||
|
.rect = { \
|
||||||
|
.x = 0, \
|
||||||
|
.y = 0, \
|
||||||
|
.w = 0, \
|
||||||
|
.h = 0, \
|
||||||
|
}, \
|
||||||
.has_frame = false, \
|
.has_frame = false, \
|
||||||
.fullscreen = false, \
|
.fullscreen = false, \
|
||||||
.maximized = false, \
|
.maximized = false, \
|
||||||
@ -91,8 +97,11 @@ bool
|
|||||||
screen_update_frame(struct screen *screen, struct video_buffer *vb);
|
screen_update_frame(struct screen *screen, struct video_buffer *vb);
|
||||||
|
|
||||||
// render the texture to the renderer
|
// render the texture to the renderer
|
||||||
|
//
|
||||||
|
// Set the update_content_rect flag if the window or content size may have
|
||||||
|
// changed, so that the content rectangle is recomputed
|
||||||
void
|
void
|
||||||
screen_render(struct screen *screen);
|
screen_render(struct screen *screen, bool update_content_rect);
|
||||||
|
|
||||||
// switch the fullscreen mode
|
// switch the fullscreen mode
|
||||||
void
|
void
|
||||||
@ -119,4 +128,11 @@ screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
|
|||||||
struct point
|
struct point
|
||||||
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y);
|
screen_convert_to_frame_coords(struct screen *screen, int32_t x, int32_t y);
|
||||||
|
|
||||||
|
// Convert coordinates from window to drawable.
|
||||||
|
// Events are expressed in window coordinates, but content is expressed in
|
||||||
|
// drawable coordinates. They are the same if HiDPI scaling is 1, but differ
|
||||||
|
// otherwise.
|
||||||
|
void
|
||||||
|
screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -217,24 +217,46 @@ enable_tunnel_forward_any_port(struct server *server,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
enable_tunnel_any_port(struct server *server, struct port_range port_range) {
|
enable_tunnel_any_port(struct server *server, struct port_range port_range,
|
||||||
|
bool force_adb_forward) {
|
||||||
|
if (!force_adb_forward) {
|
||||||
|
// Attempt to use "adb reverse"
|
||||||
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
if (enable_tunnel_reverse_any_port(server, port_range)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
|
// if "adb reverse" does not work (e.g. over "adb connect"), it
|
||||||
// "adb forward", so the app socket is the client
|
// fallbacks to "adb forward", so the app socket is the client
|
||||||
|
|
||||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||||
|
}
|
||||||
|
|
||||||
return enable_tunnel_forward_any_port(server, port_range);
|
return enable_tunnel_forward_any_port(server, port_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
log_level_to_server_string(enum sc_log_level level) {
|
||||||
|
switch (level) {
|
||||||
|
case SC_LOG_LEVEL_DEBUG:
|
||||||
|
return "debug";
|
||||||
|
case SC_LOG_LEVEL_INFO:
|
||||||
|
return "info";
|
||||||
|
case SC_LOG_LEVEL_WARN:
|
||||||
|
return "warn";
|
||||||
|
case SC_LOG_LEVEL_ERROR:
|
||||||
|
return "error";
|
||||||
|
default:
|
||||||
|
assert(!"unexpected log level");
|
||||||
|
return "(unknown)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static process_t
|
static process_t
|
||||||
execute_server(struct server *server, const struct server_params *params) {
|
execute_server(struct server *server, const struct server_params *params) {
|
||||||
char max_size_string[6];
|
char max_size_string[6];
|
||||||
char bit_rate_string[11];
|
char bit_rate_string[11];
|
||||||
char max_fps_string[6];
|
char max_fps_string[6];
|
||||||
char lock_video_orientation_string[3];
|
char lock_video_orientation_string[5];
|
||||||
char display_id_string[6];
|
char display_id_string[6];
|
||||||
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
sprintf(max_size_string, "%"PRIu16, params->max_size);
|
||||||
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
|
||||||
@ -259,6 +281,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
"/", // unused
|
"/", // unused
|
||||||
"com.genymobile.scrcpy.Server",
|
"com.genymobile.scrcpy.Server",
|
||||||
SCRCPY_VERSION,
|
SCRCPY_VERSION,
|
||||||
|
log_level_to_server_string(params->log_level),
|
||||||
max_size_string,
|
max_size_string,
|
||||||
bit_rate_string,
|
bit_rate_string,
|
||||||
max_fps_string,
|
max_fps_string,
|
||||||
@ -270,6 +293,7 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||||||
display_id_string,
|
display_id_string,
|
||||||
params->show_touches ? "true" : "false",
|
params->show_touches ? "true" : "false",
|
||||||
params->stay_awake ? "true" : "false",
|
params->stay_awake ? "true" : "false",
|
||||||
|
params->codec_options ? params->codec_options : "-",
|
||||||
};
|
};
|
||||||
#ifdef SERVER_DEBUGGER
|
#ifdef SERVER_DEBUGGER
|
||||||
LOGI("Server debugger waiting for a client on device port "
|
LOGI("Server debugger waiting for a client on device port "
|
||||||
@ -365,7 +389,8 @@ server_start(struct server *server, const char *serial,
|
|||||||
goto error1;
|
goto error1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enable_tunnel_any_port(server, params->port_range)) {
|
if (!enable_tunnel_any_port(server, params->port_range,
|
||||||
|
params->force_adb_forward)) {
|
||||||
goto error1;
|
goto error1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "command.h"
|
#include "command.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
|
|
||||||
struct server {
|
struct server {
|
||||||
@ -43,7 +44,9 @@ struct server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct server_params {
|
struct server_params {
|
||||||
|
enum sc_log_level log_level;
|
||||||
const char *crop;
|
const char *crop;
|
||||||
|
const char *codec_options;
|
||||||
struct port_range port_range;
|
struct port_range port_range;
|
||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t bit_rate;
|
||||||
@ -53,6 +56,7 @@ struct server_params {
|
|||||||
uint16_t display_id;
|
uint16_t display_id;
|
||||||
bool show_touches;
|
bool show_touches;
|
||||||
bool stay_awake;
|
bool stay_awake;
|
||||||
|
bool force_adb_forward;
|
||||||
};
|
};
|
||||||
|
|
||||||
// init default values
|
// init default values
|
||||||
|
@ -3,6 +3,13 @@
|
|||||||
|
|
||||||
#include <SDL2/SDL_log.h>
|
#include <SDL2/SDL_log.h>
|
||||||
|
|
||||||
|
enum sc_log_level {
|
||||||
|
SC_LOG_LEVEL_DEBUG,
|
||||||
|
SC_LOG_LEVEL_INFO,
|
||||||
|
SC_LOG_LEVEL_WARN,
|
||||||
|
SC_LOG_LEVEL_ERROR,
|
||||||
|
};
|
||||||
|
|
||||||
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
#define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
|
||||||
|
@ -9,18 +9,20 @@ static void test_serialize_inject_keycode(void) {
|
|||||||
.inject_keycode = {
|
.inject_keycode = {
|
||||||
.action = AKEY_EVENT_ACTION_UP,
|
.action = AKEY_EVENT_ACTION_UP,
|
||||||
.keycode = AKEYCODE_ENTER,
|
.keycode = AKEYCODE_ENTER,
|
||||||
|
.repeat = 5,
|
||||||
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
|
.metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 10);
|
assert(size == 14);
|
||||||
|
|
||||||
const unsigned char expected[] = {
|
const unsigned char expected[] = {
|
||||||
CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
||||||
0x01, // AKEY_EVENT_ACTION_UP
|
0x01, // AKEY_EVENT_ACTION_UP
|
||||||
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
|
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
|
||||||
|
0x00, 0x00, 0x00, 0X05, // repeat
|
||||||
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
|
0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON
|
||||||
};
|
};
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
@ -34,13 +36,13 @@ static void test_serialize_inject_text(void) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 16);
|
assert(size == 18);
|
||||||
|
|
||||||
const unsigned char expected[] = {
|
const unsigned char expected[] = {
|
||||||
CONTROL_MSG_TYPE_INJECT_TEXT,
|
CONTROL_MSG_TYPE_INJECT_TEXT,
|
||||||
0x00, 0x0d, // text length
|
0x00, 0x00, 0x00, 0x0d, // text length
|
||||||
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
|
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
|
||||||
};
|
};
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
@ -54,15 +56,17 @@ static void test_serialize_inject_text_long(void) {
|
|||||||
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
|
text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
|
||||||
msg.inject_text.text = text;
|
msg.inject_text.text = text;
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||||
|
|
||||||
unsigned char expected[3 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
|
unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
|
||||||
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
|
expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT;
|
||||||
expected[1] = 0x01;
|
expected[1] = 0x00;
|
||||||
expected[2] = 0x2c; // text length (16 bits)
|
expected[2] = 0x00;
|
||||||
memset(&expected[3], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
expected[3] = 0x01;
|
||||||
|
expected[4] = 0x2c; // text length (32 bits)
|
||||||
|
memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
||||||
|
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
}
|
}
|
||||||
@ -88,7 +92,7 @@ static void test_serialize_inject_touch_event(void) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 28);
|
assert(size == 28);
|
||||||
|
|
||||||
@ -123,7 +127,7 @@ static void test_serialize_inject_scroll_event(void) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 21);
|
assert(size == 21);
|
||||||
|
|
||||||
@ -142,7 +146,7 @@ static void test_serialize_back_or_screen_on(void) {
|
|||||||
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 1);
|
assert(size == 1);
|
||||||
|
|
||||||
@ -157,7 +161,7 @@ static void test_serialize_expand_notification_panel(void) {
|
|||||||
.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
|
.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 1);
|
assert(size == 1);
|
||||||
|
|
||||||
@ -172,7 +176,7 @@ static void test_serialize_collapse_notification_panel(void) {
|
|||||||
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
|
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 1);
|
assert(size == 1);
|
||||||
|
|
||||||
@ -187,7 +191,7 @@ static void test_serialize_get_clipboard(void) {
|
|||||||
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
.type = CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 1);
|
assert(size == 1);
|
||||||
|
|
||||||
@ -200,18 +204,20 @@ static void test_serialize_get_clipboard(void) {
|
|||||||
static void test_serialize_set_clipboard(void) {
|
static void test_serialize_set_clipboard(void) {
|
||||||
struct control_msg msg = {
|
struct control_msg msg = {
|
||||||
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
.type = CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||||
.inject_text = {
|
.set_clipboard = {
|
||||||
|
.paste = true,
|
||||||
.text = "hello, world!",
|
.text = "hello, world!",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 16);
|
assert(size == 19);
|
||||||
|
|
||||||
const unsigned char expected[] = {
|
const unsigned char expected[] = {
|
||||||
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||||
0x00, 0x0d, // text length
|
1, // paste
|
||||||
|
0x00, 0x00, 0x00, 0x0d, // text length
|
||||||
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
|
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
|
||||||
};
|
};
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
@ -225,7 +231,7 @@ static void test_serialize_set_screen_power_mode(void) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 2);
|
assert(size == 2);
|
||||||
|
|
||||||
@ -241,7 +247,7 @@ static void test_serialize_rotate_device(void) {
|
|||||||
.type = CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
.type = CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
int size = control_msg_serialize(&msg, buf);
|
int size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 1);
|
assert(size == 1);
|
||||||
|
|
||||||
|
@ -4,16 +4,17 @@
|
|||||||
#include "device_msg.h"
|
#include "device_msg.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
static void test_deserialize_clipboard(void) {
|
static void test_deserialize_clipboard(void) {
|
||||||
const unsigned char input[] = {
|
const unsigned char input[] = {
|
||||||
DEVICE_MSG_TYPE_CLIPBOARD,
|
DEVICE_MSG_TYPE_CLIPBOARD,
|
||||||
0x00, 0x03, // text length
|
0x00, 0x00, 0x00, 0x03, // text length
|
||||||
0x41, 0x42, 0x43, // "ABC"
|
0x41, 0x42, 0x43, // "ABC"
|
||||||
};
|
};
|
||||||
|
|
||||||
struct device_msg msg;
|
struct device_msg msg;
|
||||||
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
|
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
|
||||||
assert(r == 6);
|
assert(r == 8);
|
||||||
|
|
||||||
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
|
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
|
||||||
assert(msg.clipboard.text);
|
assert(msg.clipboard.text);
|
||||||
@ -22,7 +23,30 @@ static void test_deserialize_clipboard(void) {
|
|||||||
device_msg_destroy(&msg);
|
device_msg_destroy(&msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_deserialize_clipboard_big(void) {
|
||||||
|
unsigned char input[DEVICE_MSG_MAX_SIZE];
|
||||||
|
input[0] = DEVICE_MSG_TYPE_CLIPBOARD;
|
||||||
|
input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24;
|
||||||
|
input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16;
|
||||||
|
input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8;
|
||||||
|
input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu;
|
||||||
|
|
||||||
|
memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH);
|
||||||
|
|
||||||
|
struct device_msg msg;
|
||||||
|
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg);
|
||||||
|
assert(r == DEVICE_MSG_MAX_SIZE);
|
||||||
|
|
||||||
|
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
|
||||||
|
assert(msg.clipboard.text);
|
||||||
|
assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH);
|
||||||
|
assert(msg.clipboard.text[0] == 'a');
|
||||||
|
|
||||||
|
device_msg_destroy(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_deserialize_clipboard();
|
test_deserialize_clipboard();
|
||||||
|
test_deserialize_clipboard_big();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
project('scrcpy', 'c',
|
project('scrcpy', 'c',
|
||||||
version: '1.13',
|
version: '1.14',
|
||||||
meson_version: '>= 0.48',
|
meson_version: '>= 0.48',
|
||||||
default_options: [
|
default_options: [
|
||||||
'c_std=c11',
|
'c_std=c11',
|
||||||
|
@ -35,6 +35,6 @@ prepare-sdl2:
|
|||||||
SDL2-2.0.12
|
SDL2-2.0.12
|
||||||
|
|
||||||
prepare-adb:
|
prepare-adb:
|
||||||
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \
|
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.0-windows.zip \
|
||||||
2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \
|
854305f9a702f5ea2c3de73edde402bd26afa0ee944c9b0c4380420f5a862e0d \
|
||||||
platform-tools
|
platform-tools
|
||||||
|
@ -6,8 +6,8 @@ android {
|
|||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 15
|
versionCode 16
|
||||||
versionName "1.13"
|
versionName "1.14"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRCPY_DEBUG=false
|
SCRCPY_DEBUG=false
|
||||||
SCRCPY_VERSION_NAME=1.13
|
SCRCPY_VERSION_NAME=1.14
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-29}
|
PLATFORM=${ANDROID_PLATFORM:-29}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2008, The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package android.content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@hide}
|
||||||
|
*/
|
||||||
|
oneway interface IOnPrimaryClipChangedListener {
|
||||||
|
void dispatchPrimaryClipChanged();
|
||||||
|
}
|
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
112
server/src/main/java/com/genymobile/scrcpy/CodecOption.java
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CodecOption {
|
||||||
|
private String key;
|
||||||
|
private Object value;
|
||||||
|
|
||||||
|
public CodecOption(String key, Object value) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CodecOption> parse(String codecOptions) {
|
||||||
|
if ("-".equals(codecOptions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CodecOption> result = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean escape = false;
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
|
||||||
|
for (char c : codecOptions.toCharArray()) {
|
||||||
|
switch (c) {
|
||||||
|
case '\\':
|
||||||
|
if (escape) {
|
||||||
|
buf.append('\\');
|
||||||
|
escape = false;
|
||||||
|
} else {
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ',':
|
||||||
|
if (escape) {
|
||||||
|
buf.append(',');
|
||||||
|
escape = false;
|
||||||
|
} else {
|
||||||
|
// This comma is a separator between codec options
|
||||||
|
String codecOption = buf.toString();
|
||||||
|
result.add(parseOption(codecOption));
|
||||||
|
// Clear buf
|
||||||
|
buf.setLength(0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
buf.append(c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.length() > 0) {
|
||||||
|
String codecOption = buf.toString();
|
||||||
|
result.add(parseOption(codecOption));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CodecOption parseOption(String option) {
|
||||||
|
int equalSignIndex = option.indexOf('=');
|
||||||
|
if (equalSignIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("'=' expected");
|
||||||
|
}
|
||||||
|
String keyAndType = option.substring(0, equalSignIndex);
|
||||||
|
if (keyAndType.length() == 0) {
|
||||||
|
throw new IllegalArgumentException("Key may not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String key;
|
||||||
|
String type;
|
||||||
|
|
||||||
|
int colonIndex = keyAndType.indexOf(':');
|
||||||
|
if (colonIndex != -1) {
|
||||||
|
key = keyAndType.substring(0, colonIndex);
|
||||||
|
type = keyAndType.substring(colonIndex + 1);
|
||||||
|
} else {
|
||||||
|
key = keyAndType;
|
||||||
|
type = "int"; // assume int by default
|
||||||
|
}
|
||||||
|
|
||||||
|
Object value;
|
||||||
|
String valueString = option.substring(equalSignIndex + 1);
|
||||||
|
switch (type) {
|
||||||
|
case "int":
|
||||||
|
value = Integer.parseInt(valueString);
|
||||||
|
break;
|
||||||
|
case "long":
|
||||||
|
value = Long.parseLong(valueString);
|
||||||
|
break;
|
||||||
|
case "float":
|
||||||
|
value = Float.parseFloat(valueString);
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
value = valueString;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CodecOption(key, value);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,8 @@ public final class ControlMessage {
|
|||||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
|
||||||
public static final int TYPE_ROTATE_DEVICE = 10;
|
public static final int TYPE_ROTATE_DEVICE = 10;
|
||||||
|
|
||||||
|
public static final int FLAGS_PASTE = 1;
|
||||||
|
|
||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
private int metaState; // KeyEvent.META_*
|
private int metaState; // KeyEvent.META_*
|
||||||
@ -28,15 +30,18 @@ public final class ControlMessage {
|
|||||||
private Position position;
|
private Position position;
|
||||||
private int hScroll;
|
private int hScroll;
|
||||||
private int vScroll;
|
private int vScroll;
|
||||||
|
private int flags;
|
||||||
|
private int repeat;
|
||||||
|
|
||||||
private ControlMessage() {
|
private ControlMessage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) {
|
public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_INJECT_KEYCODE;
|
msg.type = TYPE_INJECT_KEYCODE;
|
||||||
msg.action = action;
|
msg.action = action;
|
||||||
msg.keycode = keycode;
|
msg.keycode = keycode;
|
||||||
|
msg.repeat = repeat;
|
||||||
msg.metaState = metaState;
|
msg.metaState = metaState;
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
@ -68,10 +73,13 @@ public final class ControlMessage {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ControlMessage createSetClipboard(String text) {
|
public static ControlMessage createSetClipboard(String text, boolean paste) {
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_SET_CLIPBOARD;
|
msg.type = TYPE_SET_CLIPBOARD;
|
||||||
msg.text = text;
|
msg.text = text;
|
||||||
|
if (paste) {
|
||||||
|
msg.flags = FLAGS_PASTE;
|
||||||
|
}
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,4 +142,12 @@ public final class ControlMessage {
|
|||||||
public int getVScroll() {
|
public int getVScroll() {
|
||||||
return vScroll;
|
return vScroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFlags() {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRepeat() {
|
||||||
|
return repeat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,18 +8,19 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
public class ControlMessageReader {
|
public class ControlMessageReader {
|
||||||
|
|
||||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9;
|
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||||
|
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
|
||||||
|
|
||||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||||
|
|
||||||
|
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes
|
||||||
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
public static final int INJECT_TEXT_MAX_LENGTH = 300;
|
||||||
private static final int RAW_BUFFER_SIZE = 1024;
|
|
||||||
|
|
||||||
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
|
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||||
private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH];
|
|
||||||
|
|
||||||
public ControlMessageReader() {
|
public ControlMessageReader() {
|
||||||
// invariant: the buffer is always in "get" mode
|
// invariant: the buffer is always in "get" mode
|
||||||
@ -97,20 +98,23 @@ public class ControlMessageReader {
|
|||||||
}
|
}
|
||||||
int action = toUnsigned(buffer.get());
|
int action = toUnsigned(buffer.get());
|
||||||
int keycode = buffer.getInt();
|
int keycode = buffer.getInt();
|
||||||
|
int repeat = buffer.getInt();
|
||||||
int metaState = buffer.getInt();
|
int metaState = buffer.getInt();
|
||||||
return ControlMessage.createInjectKeycode(action, keycode, metaState);
|
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String parseString() {
|
private String parseString() {
|
||||||
if (buffer.remaining() < 2) {
|
if (buffer.remaining() < 4) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int len = toUnsigned(buffer.getShort());
|
int len = buffer.getInt();
|
||||||
if (buffer.remaining() < len) {
|
if (buffer.remaining() < len) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
buffer.get(textBuffer, 0, len);
|
int position = buffer.position();
|
||||||
return new String(textBuffer, 0, len, StandardCharsets.UTF_8);
|
// Move the buffer position to consume the text
|
||||||
|
buffer.position(position + len);
|
||||||
|
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseInjectText() {
|
private ControlMessage parseInjectText() {
|
||||||
@ -147,11 +151,15 @@ public class ControlMessageReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetClipboard() {
|
private ControlMessage parseSetClipboard() {
|
||||||
|
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean parse = buffer.get() != 0;
|
||||||
String text = parseString();
|
String text = parseString();
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return ControlMessage.createSetClipboard(text);
|
return ControlMessage.createSetClipboard(text, parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetScreenPowerMode() {
|
private ControlMessage parseSetScreenPowerMode() {
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
import android.os.Build;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
|
||||||
import android.view.KeyCharacterMap;
|
import android.view.KeyCharacterMap;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@ -50,7 +48,7 @@ public class Controller {
|
|||||||
public void control() throws IOException {
|
public void control() throws IOException {
|
||||||
// on start, power on the device
|
// on start, power on the device
|
||||||
if (!device.isScreenOn()) {
|
if (!device.isScreenOn()) {
|
||||||
injectKeycode(KeyEvent.KEYCODE_POWER);
|
device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||||
|
|
||||||
// dirty hack
|
// dirty hack
|
||||||
// After POWER is injected, the device is powered on asynchronously.
|
// After POWER is injected, the device is powered on asynchronously.
|
||||||
@ -76,7 +74,7 @@ public class Controller {
|
|||||||
switch (msg.getType()) {
|
switch (msg.getType()) {
|
||||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState());
|
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_INJECT_TEXT:
|
case ControlMessage.TYPE_INJECT_TEXT:
|
||||||
@ -107,14 +105,21 @@ public class Controller {
|
|||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
String clipboardText = device.getClipboardText();
|
String clipboardText = device.getClipboardText();
|
||||||
|
if (clipboardText != null) {
|
||||||
sender.pushClipboardText(clipboardText);
|
sender.pushClipboardText(clipboardText);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
device.setClipboardText(msg.getText());
|
boolean paste = (msg.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
||||||
|
setClipboard(msg.getText(), paste);
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
device.setScreenPowerMode(msg.getAction());
|
int mode = msg.getAction();
|
||||||
|
boolean setPowerModeOk = device.setScreenPowerMode(mode);
|
||||||
|
if (setPowerModeOk) {
|
||||||
|
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
@ -125,8 +130,8 @@ public class Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectKeycode(int action, int keycode, int metaState) {
|
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||||
return injectKeyEvent(action, keycode, 0, metaState);
|
return device.injectKeyEvent(action, keycode, repeat, metaState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectChar(char c) {
|
private boolean injectChar(char c) {
|
||||||
@ -137,7 +142,7 @@ public class Controller {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (KeyEvent event : events) {
|
for (KeyEvent event : events) {
|
||||||
if (!injectEvent(event)) {
|
if (!device.injectEvent(event)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,7 +198,7 @@ public class Controller {
|
|||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||||
return injectEvent(event);
|
return device.injectEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
private boolean injectScroll(Position position, int hScroll, int vScroll) {
|
||||||
@ -216,26 +221,25 @@ public class Controller {
|
|||||||
MotionEvent event = MotionEvent
|
MotionEvent event = MotionEvent
|
||||||
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0,
|
||||||
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
InputDevice.SOURCE_TOUCHSCREEN, 0);
|
||||||
return injectEvent(event);
|
return device.injectEvent(event);
|
||||||
}
|
|
||||||
|
|
||||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
|
||||||
long now = SystemClock.uptimeMillis();
|
|
||||||
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
|
||||||
InputDevice.SOURCE_KEYBOARD);
|
|
||||||
return injectEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean injectKeycode(int keyCode) {
|
|
||||||
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean injectEvent(InputEvent event) {
|
|
||||||
return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pressBackOrTurnScreenOn() {
|
private boolean pressBackOrTurnScreenOn() {
|
||||||
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
||||||
return injectKeycode(keycode);
|
return device.injectKeycode(keycode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean setClipboard(String text, boolean paste) {
|
||||||
|
boolean ok = device.setClipboardText(text);
|
||||||
|
if (ok) {
|
||||||
|
Ln.i("Device clipboard set");
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Android >= 7, also press the PASTE key if requested
|
||||||
|
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
|
||||||
|
device.injectKeycode(KeyEvent.KEYCODE_PASTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,18 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
import com.genymobile.scrcpy.wrappers.WindowManager;
|
import com.genymobile.scrcpy.wrappers.WindowManager;
|
||||||
|
|
||||||
|
import android.content.IOnPrimaryClipChangedListener;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.SystemClock;
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
|
import android.view.InputDevice;
|
||||||
import android.view.InputEvent;
|
import android.view.InputEvent;
|
||||||
|
import android.view.KeyCharacterMap;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public final class Device {
|
public final class Device {
|
||||||
|
|
||||||
@ -22,10 +28,16 @@ public final class Device {
|
|||||||
void onRotationChanged(int rotation);
|
void onRotationChanged(int rotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ClipboardListener {
|
||||||
|
void onClipboardTextChanged(String text);
|
||||||
|
}
|
||||||
|
|
||||||
private final ServiceManager serviceManager = new ServiceManager();
|
private final ServiceManager serviceManager = new ServiceManager();
|
||||||
|
|
||||||
private ScreenInfo screenInfo;
|
private ScreenInfo screenInfo;
|
||||||
private RotationListener rotationListener;
|
private RotationListener rotationListener;
|
||||||
|
private ClipboardListener clipboardListener;
|
||||||
|
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logical display identifier
|
* Logical display identifier
|
||||||
@ -52,9 +64,9 @@ public final class Device {
|
|||||||
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
|
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation());
|
||||||
layerStack = displayInfo.getLayerStack();
|
layerStack = displayInfo.getLayerStack();
|
||||||
|
|
||||||
registerRotationWatcher(new IRotationWatcher.Stub() {
|
serviceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
|
||||||
@Override
|
@Override
|
||||||
public void onRotationChanged(int rotation) throws RemoteException {
|
public void onRotationChanged(int rotation) {
|
||||||
synchronized (Device.this) {
|
synchronized (Device.this) {
|
||||||
screenInfo = screenInfo.withDeviceRotation(rotation);
|
screenInfo = screenInfo.withDeviceRotation(rotation);
|
||||||
|
|
||||||
@ -66,6 +78,27 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
}, displayId);
|
}, displayId);
|
||||||
|
|
||||||
|
if (options.getControl()) {
|
||||||
|
// If control is enabled, synchronize Android clipboard to the computer automatically
|
||||||
|
serviceManager.getClipboardManager().addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
|
||||||
|
@Override
|
||||||
|
public void dispatchPrimaryClipChanged() {
|
||||||
|
if (isSettingClipboard.get()) {
|
||||||
|
// This is a notification for the change we are currently applying, ignore it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (Device.this) {
|
||||||
|
if (clipboardListener != null) {
|
||||||
|
String text = getClipboardText();
|
||||||
|
if (text != null) {
|
||||||
|
clipboardListener.onClipboardTextChanged(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||||
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
|
||||||
}
|
}
|
||||||
@ -118,7 +151,7 @@ public final class Device {
|
|||||||
return supportsInputEvents;
|
return supportsInputEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
|
public boolean injectEvent(InputEvent inputEvent, int mode) {
|
||||||
if (!supportsInputEvents()) {
|
if (!supportsInputEvents()) {
|
||||||
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
|
||||||
}
|
}
|
||||||
@ -130,18 +163,33 @@ public final class Device {
|
|||||||
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
return serviceManager.getInputManager().injectInputEvent(inputEvent, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean injectEvent(InputEvent event) {
|
||||||
|
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
|
||||||
|
long now = SystemClock.uptimeMillis();
|
||||||
|
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
|
||||||
|
InputDevice.SOURCE_KEYBOARD);
|
||||||
|
return injectEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean injectKeycode(int keyCode) {
|
||||||
|
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isScreenOn() {
|
public boolean isScreenOn() {
|
||||||
return serviceManager.getPowerManager().isScreenOn();
|
return serviceManager.getPowerManager().isScreenOn();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
|
|
||||||
serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void setRotationListener(RotationListener rotationListener) {
|
public synchronized void setRotationListener(RotationListener rotationListener) {
|
||||||
this.rotationListener = rotationListener;
|
this.rotationListener = rotationListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
|
||||||
|
this.clipboardListener = clipboardListener;
|
||||||
|
}
|
||||||
|
|
||||||
public void expandNotificationPanel() {
|
public void expandNotificationPanel() {
|
||||||
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
serviceManager.getStatusBarManager().expandNotificationsPanel();
|
||||||
}
|
}
|
||||||
@ -158,26 +206,23 @@ public final class Device {
|
|||||||
return s.toString();
|
return s.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setClipboardText(String text) {
|
public boolean setClipboardText(String text) {
|
||||||
|
isSettingClipboard.set(true);
|
||||||
boolean ok = serviceManager.getClipboardManager().setText(text);
|
boolean ok = serviceManager.getClipboardManager().setText(text);
|
||||||
if (ok) {
|
isSettingClipboard.set(false);
|
||||||
Ln.i("Device clipboard set");
|
return ok;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
* @param mode one of the {@code SCREEN_POWER_MODE_*} constants
|
||||||
*/
|
*/
|
||||||
public void setScreenPowerMode(int mode) {
|
public boolean setScreenPowerMode(int mode) {
|
||||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||||
if (d == null) {
|
if (d == null) {
|
||||||
Ln.e("Could not get built-in display");
|
Ln.e("Could not get built-in display");
|
||||||
return;
|
return false;
|
||||||
}
|
|
||||||
boolean ok = SurfaceControl.setDisplayPowerMode(d, mode);
|
|
||||||
if (ok) {
|
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
|
||||||
}
|
}
|
||||||
|
return SurfaceControl.setDisplayPowerMode(d, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -7,10 +7,10 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
public class DeviceMessageWriter {
|
public class DeviceMessageWriter {
|
||||||
|
|
||||||
public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093;
|
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
|
||||||
private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3;
|
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
|
||||||
|
|
||||||
private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE];
|
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
|
||||||
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
|
||||||
|
|
||||||
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
|
||||||
@ -21,7 +21,7 @@ public class DeviceMessageWriter {
|
|||||||
String text = msg.getText();
|
String text = msg.getText();
|
||||||
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
|
||||||
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
|
||||||
buffer.putShort((short) len);
|
buffer.putInt(len);
|
||||||
buffer.put(raw, 0, len);
|
buffer.put(raw, 0, len);
|
||||||
output.write(rawBuffer, 0, buffer.position());
|
output.write(rawBuffer, 0, buffer.position());
|
||||||
break;
|
break;
|
||||||
|
@ -15,14 +15,25 @@ public final class Ln {
|
|||||||
DEBUG, INFO, WARN, ERROR
|
DEBUG, INFO, WARN, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO;
|
private static Level threshold = Level.INFO;
|
||||||
|
|
||||||
private Ln() {
|
private Ln() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the log level.
|
||||||
|
* <p>
|
||||||
|
* Must be called before starting any new thread.
|
||||||
|
*
|
||||||
|
* @param level the log level
|
||||||
|
*/
|
||||||
|
public static void initLogLevel(Level level) {
|
||||||
|
threshold = level;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isEnabled(Level level) {
|
public static boolean isEnabled(Level level) {
|
||||||
return level.ordinal() >= THRESHOLD.ordinal();
|
return level.ordinal() >= threshold.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void d(String message) {
|
public static void d(String message) {
|
||||||
|
@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
|
|||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
|
||||||
public class Options {
|
public class Options {
|
||||||
|
private Ln.Level logLevel;
|
||||||
private int maxSize;
|
private int maxSize;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
@ -14,6 +15,15 @@ public class Options {
|
|||||||
private int displayId;
|
private int displayId;
|
||||||
private boolean showTouches;
|
private boolean showTouches;
|
||||||
private boolean stayAwake;
|
private boolean stayAwake;
|
||||||
|
private String codecOptions;
|
||||||
|
|
||||||
|
public Ln.Level getLogLevel() {
|
||||||
|
return logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogLevel(Ln.Level logLevel) {
|
||||||
|
this.logLevel = logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMaxSize() {
|
public int getMaxSize() {
|
||||||
return maxSize;
|
return maxSize;
|
||||||
@ -102,4 +112,12 @@ public class Options {
|
|||||||
public void setStayAwake(boolean stayAwake) {
|
public void setStayAwake(boolean stayAwake) {
|
||||||
this.stayAwake = stayAwake;
|
this.stayAwake = stayAwake;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getCodecOptions() {
|
||||||
|
return codecOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCodecOptions(String codecOptions) {
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import android.view.Surface;
|
|||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class ScreenEncoder implements Device.RotationListener {
|
public class ScreenEncoder implements Device.RotationListener {
|
||||||
@ -25,15 +26,17 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
|
private List<CodecOption> codecOptions;
|
||||||
private int bitRate;
|
private int bitRate;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private boolean sendFrameMeta;
|
private boolean sendFrameMeta;
|
||||||
private long ptsOrigin;
|
private long ptsOrigin;
|
||||||
|
|
||||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) {
|
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
this.bitRate = bitRate;
|
this.bitRate = bitRate;
|
||||||
this.maxFps = maxFps;
|
this.maxFps = maxFps;
|
||||||
|
this.codecOptions = codecOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -61,7 +64,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||||
MediaFormat format = createFormat(bitRate, maxFps);
|
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
@ -151,7 +154,24 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
return MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(int bitRate, int maxFps) {
|
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||||
|
String key = codecOption.getKey();
|
||||||
|
Object value = codecOption.getValue();
|
||||||
|
|
||||||
|
if (value instanceof Integer) {
|
||||||
|
format.setInteger(key, (Integer) value);
|
||||||
|
} else if (value instanceof Long) {
|
||||||
|
format.setLong(key, (Long) value);
|
||||||
|
} else if (value instanceof Float) {
|
||||||
|
format.setFloat(key, (Float) value);
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
format.setString(key, (String) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||||
MediaFormat format = new MediaFormat();
|
MediaFormat format = new MediaFormat();
|
||||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||||
@ -167,6 +187,13 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
|
||||||
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (codecOptions != null) {
|
||||||
|
for (CodecOption option : codecOptions) {
|
||||||
|
setCodecOption(format, option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return format;
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ import android.os.BatteryManager;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public final class Server {
|
public final class Server {
|
||||||
|
|
||||||
@ -19,6 +21,7 @@ public final class Server {
|
|||||||
private static void scrcpy(Options options) throws IOException {
|
private static void scrcpy(Options options) throws IOException {
|
||||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
final Device device = new Device(options);
|
final Device device = new Device(options);
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse(options.getCodecOptions());
|
||||||
|
|
||||||
boolean mustDisableShowTouchesOnCleanUp = false;
|
boolean mustDisableShowTouchesOnCleanUp = false;
|
||||||
int restoreStayOn = -1;
|
int restoreStayOn = -1;
|
||||||
@ -49,15 +52,23 @@ public final class Server {
|
|||||||
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn);
|
||||||
|
|
||||||
boolean tunnelForward = options.isTunnelForward();
|
boolean tunnelForward = options.isTunnelForward();
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions);
|
||||||
|
|
||||||
if (options.getControl()) {
|
if (options.getControl()) {
|
||||||
Controller controller = new Controller(device, connection);
|
final Controller controller = new Controller(device, connection);
|
||||||
|
|
||||||
// asynchronous
|
// asynchronous
|
||||||
startController(controller);
|
startController(controller);
|
||||||
startDeviceMessageSender(controller.getSender());
|
startDeviceMessageSender(controller.getSender());
|
||||||
|
|
||||||
|
device.setClipboardListener(new Device.ClipboardListener() {
|
||||||
|
@Override
|
||||||
|
public void onClipboardTextChanged(String text) {
|
||||||
|
controller.getSender().pushClipboardText(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -109,47 +120,53 @@ public final class Server {
|
|||||||
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
final int expectedParameters = 12;
|
final int expectedParameters = 14;
|
||||||
if (args.length != expectedParameters) {
|
if (args.length != expectedParameters) {
|
||||||
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
Options options = new Options();
|
Options options = new Options();
|
||||||
|
|
||||||
int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8
|
Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH));
|
||||||
|
options.setLogLevel(level);
|
||||||
|
|
||||||
|
int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8
|
||||||
options.setMaxSize(maxSize);
|
options.setMaxSize(maxSize);
|
||||||
|
|
||||||
int bitRate = Integer.parseInt(args[2]);
|
int bitRate = Integer.parseInt(args[3]);
|
||||||
options.setBitRate(bitRate);
|
options.setBitRate(bitRate);
|
||||||
|
|
||||||
int maxFps = Integer.parseInt(args[3]);
|
int maxFps = Integer.parseInt(args[4]);
|
||||||
options.setMaxFps(maxFps);
|
options.setMaxFps(maxFps);
|
||||||
|
|
||||||
int lockedVideoOrientation = Integer.parseInt(args[4]);
|
int lockedVideoOrientation = Integer.parseInt(args[5]);
|
||||||
options.setLockedVideoOrientation(lockedVideoOrientation);
|
options.setLockedVideoOrientation(lockedVideoOrientation);
|
||||||
|
|
||||||
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
|
||||||
boolean tunnelForward = Boolean.parseBoolean(args[5]);
|
boolean tunnelForward = Boolean.parseBoolean(args[6]);
|
||||||
options.setTunnelForward(tunnelForward);
|
options.setTunnelForward(tunnelForward);
|
||||||
|
|
||||||
Rect crop = parseCrop(args[6]);
|
Rect crop = parseCrop(args[7]);
|
||||||
options.setCrop(crop);
|
options.setCrop(crop);
|
||||||
|
|
||||||
boolean sendFrameMeta = Boolean.parseBoolean(args[7]);
|
boolean sendFrameMeta = Boolean.parseBoolean(args[8]);
|
||||||
options.setSendFrameMeta(sendFrameMeta);
|
options.setSendFrameMeta(sendFrameMeta);
|
||||||
|
|
||||||
boolean control = Boolean.parseBoolean(args[8]);
|
boolean control = Boolean.parseBoolean(args[9]);
|
||||||
options.setControl(control);
|
options.setControl(control);
|
||||||
|
|
||||||
int displayId = Integer.parseInt(args[9]);
|
int displayId = Integer.parseInt(args[10]);
|
||||||
options.setDisplayId(displayId);
|
options.setDisplayId(displayId);
|
||||||
|
|
||||||
boolean showTouches = Boolean.parseBoolean(args[10]);
|
boolean showTouches = Boolean.parseBoolean(args[11]);
|
||||||
options.setShowTouches(showTouches);
|
options.setShowTouches(showTouches);
|
||||||
|
|
||||||
boolean stayAwake = Boolean.parseBoolean(args[11]);
|
boolean stayAwake = Boolean.parseBoolean(args[12]);
|
||||||
options.setStayAwake(stayAwake);
|
options.setStayAwake(stayAwake);
|
||||||
|
|
||||||
|
String codecOptions = args[13];
|
||||||
|
options.setCodecOptions(codecOptions);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +219,9 @@ public final class Server {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Options options = createOptions(args);
|
Options options = createOptions(args);
|
||||||
|
|
||||||
|
Ln.initLogLevel(options.getLogLevel());
|
||||||
|
|
||||||
scrcpy(options);
|
scrcpy(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
|
|||||||
import com.genymobile.scrcpy.Ln;
|
import com.genymobile.scrcpy.Ln;
|
||||||
|
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
|
import android.content.IOnPrimaryClipChangedListener;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IInterface;
|
import android.os.IInterface;
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ public class ClipboardManager {
|
|||||||
private final IInterface manager;
|
private final IInterface manager;
|
||||||
private Method getPrimaryClipMethod;
|
private Method getPrimaryClipMethod;
|
||||||
private Method setPrimaryClipMethod;
|
private Method setPrimaryClipMethod;
|
||||||
|
private Method addPrimaryClipChangedListener;
|
||||||
|
|
||||||
public ClipboardManager(IInterface manager) {
|
public ClipboardManager(IInterface manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -81,4 +83,37 @@ public class ClipboardManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
|
||||||
|
throws InvocationTargetException, IllegalAccessException {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
||||||
|
} else {
|
||||||
|
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||||
|
if (addPrimaryClipChangedListener == null) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
|
||||||
|
} else {
|
||||||
|
addPrimaryClipChangedListener = manager.getClass()
|
||||||
|
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addPrimaryClipChangedListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||||
|
try {
|
||||||
|
Method method = getAddPrimaryClipChangedListener();
|
||||||
|
addPrimaryClipChangedListener(method, manager, listener);
|
||||||
|
return true;
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
Ln.e("Could not invoke method", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
114
server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CodecOptionsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntegerImplicit() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key=5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertEquals(5, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInteger() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:int=5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||||
|
Assert.assertEquals(5, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLong() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:long=5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Long);
|
||||||
|
Assert.assertEquals(5L, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFloat() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:float=4.5");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Float);
|
||||||
|
Assert.assertEquals(4.5f, option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testString() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=some_value");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof String);
|
||||||
|
Assert.assertEquals("some_value", option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStringEscaped() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key");
|
||||||
|
|
||||||
|
Assert.assertEquals(1, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("some_key", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof String);
|
||||||
|
Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testList() {
|
||||||
|
List<CodecOption> codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c");
|
||||||
|
|
||||||
|
Assert.assertEquals(5, codecOptions.size());
|
||||||
|
|
||||||
|
CodecOption option;
|
||||||
|
|
||||||
|
option = codecOptions.get(0);
|
||||||
|
Assert.assertEquals("a", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||||
|
Assert.assertEquals(1, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(1);
|
||||||
|
Assert.assertEquals("b", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Integer);
|
||||||
|
Assert.assertEquals(2, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(2);
|
||||||
|
Assert.assertEquals("c", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Long);
|
||||||
|
Assert.assertEquals(3L, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(3);
|
||||||
|
Assert.assertEquals("d", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof Float);
|
||||||
|
Assert.assertEquals(4.5f, option.getValue());
|
||||||
|
|
||||||
|
option = codecOptions.get(4);
|
||||||
|
Assert.assertEquals("e", option.getKey());
|
||||||
|
Assert.assertTrue(option.getValue() instanceof String);
|
||||||
|
Assert.assertEquals("a,b=c", option.getValue());
|
||||||
|
}
|
||||||
|
}
|
@ -25,6 +25,7 @@ public class ControlMessageReaderTest {
|
|||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(KeyEvent.ACTION_UP);
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||||
|
dos.writeInt(5); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||||
|
Assert.assertEquals(5, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ public class ControlMessageReaderTest {
|
|||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||||
dos.writeShort(text.length);
|
dos.writeInt(text.length);
|
||||||
dos.write(text);
|
dos.write(text);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
@ -68,7 +70,7 @@ public class ControlMessageReaderTest {
|
|||||||
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
|
||||||
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
|
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
|
||||||
Arrays.fill(text, (byte) 'a');
|
Arrays.fill(text, (byte) 'a');
|
||||||
dos.writeShort(text.length);
|
dos.writeInt(text.length);
|
||||||
dos.write(text);
|
dos.write(text);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
@ -216,8 +218,9 @@ public class ControlMessageReaderTest {
|
|||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||||
|
dos.writeByte(1); // paste
|
||||||
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
|
||||||
dos.writeShort(text.length);
|
dos.writeInt(text.length);
|
||||||
dos.write(text);
|
dos.write(text);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
@ -227,6 +230,37 @@ public class ControlMessageReaderTest {
|
|||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||||
Assert.assertEquals("testé", event.getText());
|
Assert.assertEquals("testé", event.getText());
|
||||||
|
|
||||||
|
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
||||||
|
Assert.assertTrue(parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseBigSetClipboardEvent() throws IOException {
|
||||||
|
ControlMessageReader reader = new ControlMessageReader();
|
||||||
|
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
|
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
|
||||||
|
|
||||||
|
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
|
||||||
|
dos.writeByte(1); // paste
|
||||||
|
Arrays.fill(rawText, (byte) 'a');
|
||||||
|
String text = new String(rawText, 0, rawText.length);
|
||||||
|
|
||||||
|
dos.writeInt(rawText.length);
|
||||||
|
dos.write(rawText);
|
||||||
|
|
||||||
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
|
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
|
||||||
|
Assert.assertEquals(text, event.getText());
|
||||||
|
|
||||||
|
boolean parse = (event.getFlags() & ControlMessage.FLAGS_PASTE) != 0;
|
||||||
|
Assert.assertTrue(parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -276,11 +310,13 @@ public class ControlMessageReaderTest {
|
|||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(KeyEvent.ACTION_UP);
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||||
|
dos.writeInt(0); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
|
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(MotionEvent.ACTION_DOWN);
|
dos.writeByte(MotionEvent.ACTION_DOWN);
|
||||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||||
|
dos.writeInt(1); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
@ -290,12 +326,14 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||||
|
Assert.assertEquals(0, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
|
|
||||||
event = reader.next();
|
event = reader.next();
|
||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||||
|
Assert.assertEquals(1, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,6 +347,7 @@ public class ControlMessageReaderTest {
|
|||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
dos.writeByte(KeyEvent.ACTION_UP);
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
dos.writeInt(KeyEvent.KEYCODE_ENTER);
|
||||||
|
dos.writeInt(4); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
|
|
||||||
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
|
||||||
@ -321,6 +360,7 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
|
||||||
|
Assert.assertEquals(4, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
|
|
||||||
event = reader.next();
|
event = reader.next();
|
||||||
@ -328,6 +368,7 @@ public class ControlMessageReaderTest {
|
|||||||
|
|
||||||
bos.reset();
|
bos.reset();
|
||||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||||
|
dos.writeInt(5); // repeat
|
||||||
dos.writeInt(KeyEvent.META_CTRL_ON);
|
dos.writeInt(KeyEvent.META_CTRL_ON);
|
||||||
packet = bos.toByteArray();
|
packet = bos.toByteArray();
|
||||||
reader.readFrom(new ByteArrayInputStream(packet));
|
reader.readFrom(new ByteArrayInputStream(packet));
|
||||||
@ -337,6 +378,7 @@ public class ControlMessageReaderTest {
|
|||||||
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
|
||||||
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
|
||||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
|
||||||
|
Assert.assertEquals(5, event.getRepeat());
|
||||||
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ public class DeviceMessageWriterTest {
|
|||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
|
||||||
dos.writeShort(data.length);
|
dos.writeInt(data.length);
|
||||||
dos.write(data);
|
dos.write(data);
|
||||||
|
|
||||||
byte[] expected = bos.toByteArray();
|
byte[] expected = bos.toByteArray();
|
||||||
|
Reference in New Issue
Block a user