mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-03-15 08:34:42 +01:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d298ad577 | ||
|
|
0e34f4fbf7 | ||
|
|
b13aece7a1 |
10
BUILD.md
10
BUILD.md
@@ -260,7 +260,7 @@ set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk
|
||||
Then, build:
|
||||
|
||||
```bash
|
||||
meson setup x --buildtype=release --strip -Db_lto=true
|
||||
meson x --buildtype=release --strip -Db_lto=true
|
||||
ninja -Cx # DO NOT RUN AS ROOT
|
||||
```
|
||||
|
||||
@@ -272,16 +272,16 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v1.25`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
|
||||
- [`scrcpy-server-v1.24`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
|
||||
```bash
|
||||
meson setup x --buildtype=release --strip -Db_lto=true \
|
||||
meson x --buildtype=release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=/path/to/scrcpy-server
|
||||
ninja -Cx # DO NOT RUN AS ROOT
|
||||
```
|
||||
|
||||
@@ -277,7 +277,7 @@ The server is pushed to the device by the client on startup.
|
||||
To debug it, enable the server debugger during configuration:
|
||||
|
||||
```bash
|
||||
meson setup x -Dserver_debugger=true
|
||||
meson x -Dserver_debugger=true
|
||||
# or, if x is already configured
|
||||
meson configure x -Dserver_debugger=true
|
||||
```
|
||||
@@ -286,7 +286,7 @@ If your device runs Android 8 or below, set the `server_debugger_method` to
|
||||
`old` in addition:
|
||||
|
||||
```bash
|
||||
meson setup x -Dserver_debugger=true -Dserver_debugger_method=old
|
||||
meson x -Dserver_debugger=true -Dserver_debugger_method=old
|
||||
# or, if x is already configured
|
||||
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
|
||||
```
|
||||
|
||||
235
FAQ.it.md
Normal file
235
FAQ.it.md
Normal file
@@ -0,0 +1,235 @@
|
||||
_Apri le [FAQ](FAQ.md) originali e sempre aggiornate._
|
||||
|
||||
# Domande Frequenti (FAQ)
|
||||
|
||||
Questi sono i problemi più comuni riportati e i loro stati.
|
||||
|
||||
|
||||
## Problemi di `adb`
|
||||
|
||||
`scrcpy` esegue comandi `adb` per inizializzare la connessione con il dispositivo. Se `adb` fallisce, scrcpy non funzionerà.
|
||||
|
||||
In questo caso sarà stampato questo errore:
|
||||
|
||||
> ERROR: "adb push" returned with value 1
|
||||
|
||||
Questo solitamente non è un bug di _scrcpy_, ma un problema del tuo ambiente.
|
||||
|
||||
Per trovare la causa, esegui:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
### `adb` not found (`adb` non trovato)
|
||||
|
||||
È necessario che `adb` sia accessibile dal tuo `PATH`.
|
||||
|
||||
In Windows, la cartella corrente è nel tuo `PATH` e `adb.exe` è incluso nella release, perciò dovrebbe già essere pronto all'uso.
|
||||
|
||||
|
||||
### Device unauthorized (Dispositivo non autorizzato)
|
||||
|
||||
Controlla [stackoverflow][device-unauthorized] (in inglese).
|
||||
|
||||
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
|
||||
|
||||
|
||||
### Device not detected (Dispositivo non rilevato)
|
||||
|
||||
> adb: error: failed to get feature set: no devices/emulators found
|
||||
|
||||
Controlla di aver abilitato correttamente il [debug con adb][enable-adb] (link in inglese).
|
||||
|
||||
Se il tuo dispositivo non è rilevato, potresti avere bisogno dei [driver][drivers] (link in inglese) (in Windows).
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||
|
||||
|
||||
### Più dispositivi connessi
|
||||
|
||||
Se più dispositivi sono connessi, riscontrerai questo errore:
|
||||
|
||||
> adb: error: failed to get feature set: more than one device/emulator
|
||||
|
||||
l'identificatore del tuo dispositivo deve essere fornito:
|
||||
|
||||
```bash
|
||||
scrcpy -s 01234567890abcdef
|
||||
```
|
||||
|
||||
Notare che se il tuo dispositivo è connesso mediante TCP/IP, riscontrerai questo messaggio:
|
||||
|
||||
> adb: error: more than one device/emulator
|
||||
> ERROR: "adb reverse" returned with value 1
|
||||
> WARN: 'adb reverse' failed, fallback to 'adb forward'
|
||||
|
||||
Questo è un problema atteso (a causa di un bug di una vecchia versione di Android, vedi [#5] (link in inglese)), ma in quel caso scrcpy ripiega su un metodo differente, il quale dovrebbe funzionare.
|
||||
|
||||
[#5]: https://github.com/Genymobile/scrcpy/issues/5
|
||||
|
||||
|
||||
### Conflitti tra versioni di adb
|
||||
|
||||
> adb server version (41) doesn't match this client (39); killing...
|
||||
|
||||
L'errore compare quando usi più versioni di `adb` simultaneamente. Devi trovare il programma che sta utilizzando una versione differente di `adb` e utilizzare la stessa versione dappertutto.
|
||||
|
||||
Puoi sovrascrivere i binari di `adb` nell'altro programma, oppure chiedere a _scrcpy_ di usare un binario specifico di `adb`, impostando la variabile d'ambiente `ADB`:
|
||||
|
||||
```bash
|
||||
set ADB=/path/to/your/adb
|
||||
scrcpy
|
||||
```
|
||||
|
||||
|
||||
### Device disconnected (Dispositivo disconnesso)
|
||||
|
||||
Se _scrcpy_ si interrompe con l'avviso "Device disconnected", allora la connessione `adb` è stata chiusa.
|
||||
|
||||
Prova con un altro cavo USB o inseriscilo in un'altra porta USB. Vedi [#281] (in inglese) e [#283] (in inglese).
|
||||
|
||||
[#281]: https://github.com/Genymobile/scrcpy/issues/281
|
||||
[#283]: https://github.com/Genymobile/scrcpy/issues/283
|
||||
|
||||
|
||||
|
||||
## Problemi di controllo
|
||||
|
||||
### Mouse e tastiera non funzionano
|
||||
|
||||
Su alcuni dispositivi potresti dover abilitare un opzione che permette l'[input simulato][simulating input] (link in inglese). Nelle opzioni sviluppatore, abilita:
|
||||
|
||||
> **Debug USB (Impostazioni di sicurezza)**
|
||||
> _Permetti la concessione dei permessi e la simulazione degli input mediante il debug USB_
|
||||
<!--- Ho tradotto personalmente il testo sopra, non conosco esattamente il testo reale --->
|
||||
|
||||
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
### I caratteri speciali non funzionano
|
||||
|
||||
Iniettare del testo in input è [limitato ai caratteri ASCII][text-input] (link in inglese). Un trucco permette di iniettare dei [caratteri accentati][accented-characters] (link in inglese), ma questo è tutto. Vedi [#37] (link in inglese).
|
||||
|
||||
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
||||
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
||||
[#37]: https://github.com/Genymobile/scrcpy/issues/37
|
||||
|
||||
|
||||
## Problemi del client
|
||||
|
||||
### La qualità è bassa
|
||||
|
||||
Se la definizione della finestra del tuo client è minore di quella del tuo dispositivo, allora potresti avere una bassa qualità di visualizzazione, specialmente individuabile nei testi (vedi [#40] (link in inglese)).
|
||||
|
||||
[#40]: https://github.com/Genymobile/scrcpy/issues/40
|
||||
|
||||
Per migliorare la qualità di ridimensionamento (downscaling), il filtro trilineare è applicato automaticamente se il renderizzatore è OpenGL e se supporta la creazione di mipmap.
|
||||
|
||||
In Windows, potresti voler forzare OpenGL:
|
||||
|
||||
```
|
||||
scrcpy --render-driver=opengl
|
||||
```
|
||||
|
||||
Potresti anche dover configurare il [comportamento di ridimensionamento][scaling behavior] (link in inglese):
|
||||
|
||||
> `scrcpy.exe` > Propietà > Compatibilità > Modifica impostazioni DPI elevati > Esegui l'override del comportamento di ridimensionamento DPI elevati > Ridimensionamento eseguito per: _Applicazione_.
|
||||
|
||||
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
||||
|
||||
|
||||
### Problema con Wayland
|
||||
|
||||
Per impostazione predefinita, SDL utilizza x11 su Linux. Il [video driver] può essere cambiato attraversio la variabile d'ambiente `SDL_VIDEODRIVER`:
|
||||
|
||||
[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver
|
||||
|
||||
```bash
|
||||
export SDL_VIDEODRIVER=wayland
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Su alcune distribuzioni (almeno Fedora), il pacchetto `libdecor` deve essere installato manualmente.
|
||||
|
||||
Vedi le issues [#2554] e [#2559].
|
||||
|
||||
[#2554]: https://github.com/Genymobile/scrcpy/issues/2554
|
||||
[#2559]: https://github.com/Genymobile/scrcpy/issues/2559
|
||||
|
||||
|
||||
### Crash del compositore KWin
|
||||
|
||||
In Plasma Desktop, il compositore è disabilitato mentre _scrcpy_ è in esecuzione.
|
||||
|
||||
Come soluzione alternativa, [disattiva la "composizione dei blocchi"][kwin] (link in inglese).
|
||||
<!--- Non sono sicuro di aver tradotto correttamente la stringa di testo del pulsante --->
|
||||
|
||||
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
||||
|
||||
|
||||
## Crash
|
||||
|
||||
### Eccezione
|
||||
|
||||
Ci potrebbero essere molte ragioni. Una causa comune è che il codificatore hardware del tuo dispositivo non riesce a codificare alla definizione selezionata:
|
||||
|
||||
> ```
|
||||
> ERROR: Exception on thread Thread[main,5,main]
|
||||
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
||||
> ...
|
||||
> Exit due to uncaughtException in main thread:
|
||||
> ERROR: Could not open video stream
|
||||
> INFO: Initial texture: 1080x2336
|
||||
> ```
|
||||
|
||||
o
|
||||
|
||||
> ```
|
||||
> ERROR: Exception on thread Thread[main,5,main]
|
||||
> java.lang.IllegalStateException
|
||||
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||
> ```
|
||||
|
||||
Prova con una definizione inferiore:
|
||||
|
||||
```
|
||||
scrcpy -m 1920
|
||||
scrcpy -m 1024
|
||||
scrcpy -m 800
|
||||
```
|
||||
|
||||
Potresti anche provare un altro [codificatore](README.it.md#codificatore).
|
||||
|
||||
|
||||
## Linea di comando in Windows
|
||||
|
||||
Alcuni utenti Windows non sono familiari con la riga di comando. Qui è descritto come aprire un terminale ed eseguire `scrcpy` con gli argomenti:
|
||||
|
||||
1. Premi <kbd>Windows</kbd>+<kbd>r</kbd>, questo apre una finestra di dialogo.
|
||||
2. Scrivi `cmd` e premi <kbd>Enter</kbd>, questo apre un terminale.
|
||||
3. Vai nella tua cartella di _scrcpy_ scrivendo (adatta il percorso):
|
||||
|
||||
```bat
|
||||
cd C:\Users\user\Downloads\scrcpy-win64-xxx
|
||||
```
|
||||
|
||||
e premi <kbd>Enter</kbd>
|
||||
4. Scrivi il tuo comando. Per esempio:
|
||||
|
||||
```bat
|
||||
scrcpy --record file.mkv
|
||||
```
|
||||
|
||||
Se pianifichi di utilizzare sempre gli stessi argomenti, crea un file `myscrcpy.bat` (abilita mostra [estensioni nomi file][show file extensions] per evitare di far confusione) contenente il tuo comando nella cartella di `scrcpy`. Per esempio:
|
||||
|
||||
```bat
|
||||
scrcpy --prefer-text --turn-screen-off --stay-awake
|
||||
```
|
||||
|
||||
Poi fai doppio click su quel file.
|
||||
|
||||
Potresti anche modificare (una copia di) `scrcpy-console.bat` o `scrcpy-noconsole.vbs` per aggiungere alcuni argomenti.
|
||||
|
||||
[show file extensions]: https://www.techpedia.it/14-windows/windows-10/171-visualizzare-le-estensioni-nomi-file-con-windows-10
|
||||
84
FAQ.ko.md
Normal file
84
FAQ.ko.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 자주하는 질문 (FAQ)
|
||||
|
||||
다음은 자주 제보되는 문제들과 그들의 현황입니다.
|
||||
|
||||
|
||||
### Windows 운영체제에서, 디바이스가 발견되지 않습니다.
|
||||
|
||||
가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다.
|
||||
다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요:
|
||||
|
||||
adb devices
|
||||
|
||||
Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다.
|
||||
|
||||
[드라이버]: https://developer.android.com/studio/run/oem-usb.html
|
||||
|
||||
|
||||
### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다.
|
||||
|
||||
일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다.
|
||||
개발자 옵션에서 (developer options) 다음을 활성화 하세요:
|
||||
|
||||
> **USB debugging (Security settings)**
|
||||
> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_
|
||||
|
||||
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
### 마우스 클릭이 다른 곳에 적용됩니다.
|
||||
|
||||
Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다.
|
||||
[issue 15]를 참고하세요.
|
||||
|
||||
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
|
||||
|
||||
차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다:
|
||||
|
||||
```bash
|
||||
meson x --buildtype release -Dhidpi_support=false
|
||||
```
|
||||
|
||||
하지만, 동영상은 낮은 해상도로 재생될 것 입니다.
|
||||
|
||||
|
||||
### HiDPI display의 화질이 낮습니다.
|
||||
|
||||
Windows에서는, [scaling behavior] 환경을 설정해야 할 수도 있습니다.
|
||||
|
||||
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
|
||||
> Override high DPI scaling behavior > Scaling performed by: _Application_.
|
||||
|
||||
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
||||
|
||||
|
||||
### KWin compositor가 실행되지 않습니다
|
||||
|
||||
Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다.
|
||||
|
||||
차석책으로는, ["Block compositing"를 비활성화하세요][kwin].
|
||||
|
||||
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
||||
|
||||
|
||||
###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream).
|
||||
|
||||
여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가
|
||||
주어진 해상도를 인코딩할 수 없는 경우입니다.
|
||||
|
||||
```
|
||||
ERROR: Exception on thread Thread[main,5,main]
|
||||
android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
||||
...
|
||||
Exit due to uncaughtException in main thread:
|
||||
ERROR: Could not open video stream
|
||||
INFO: Initial texture: 1080x2336
|
||||
```
|
||||
|
||||
더 낮은 해상도로 시도 해보세요:
|
||||
|
||||
```
|
||||
scrcpy -m 1920
|
||||
scrcpy -m 1024
|
||||
scrcpy -m 800
|
||||
```
|
||||
37
FAQ.md
37
FAQ.md
@@ -44,9 +44,9 @@ If your device is not detected, you may need some [drivers] (on Windows). There
|
||||
|
||||
### Device unauthorized
|
||||
|
||||
> ERROR: Device is unauthorized:
|
||||
> ERROR: --> (usb) 0123456789abcdef unauthorized
|
||||
> ERROR: A popup should open on the device to request authorization.
|
||||
> ERROR: Device is unauthorized:
|
||||
> ERROR: --> (usb) 0123456789abcdef unauthorized
|
||||
> ERROR: A popup should open on the device to request authorization.
|
||||
|
||||
When connecting, a popup should open on the device. You must authorize USB
|
||||
debugging.
|
||||
@@ -60,10 +60,10 @@ If it does not open, check [stackoverflow][device-unauthorized].
|
||||
|
||||
If several devices are connected, you will encounter this error:
|
||||
|
||||
> ERROR: Multiple (2) ADB devices:
|
||||
> ERROR: --> (usb) 0123456789abcdef device Nexus_5
|
||||
> ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913
|
||||
> ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip)
|
||||
ERROR: Multiple (2) ADB devices:
|
||||
ERROR: --> (usb) 0123456789abcdef device Nexus_5
|
||||
ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913
|
||||
ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip)
|
||||
|
||||
In that case, you can either provide the identifier of the device you want to
|
||||
mirror:
|
||||
@@ -103,20 +103,7 @@ You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to
|
||||
use a specific `adb` binary, by setting the `ADB` environment variable:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB=/path/to/your/adb
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB=C:\path\to\your\adb.exe
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB = 'C:\path\to\your\adb.exe'
|
||||
set ADB=/path/to/your/adb
|
||||
scrcpy
|
||||
```
|
||||
|
||||
@@ -324,8 +311,8 @@ to add some arguments.
|
||||
|
||||
## Translations
|
||||
|
||||
Translations of this FAQ in other languages are available in the [wiki].
|
||||
This FAQ is available in other languages:
|
||||
|
||||
[wiki]: https://github.com/Genymobile/scrcpy/wiki
|
||||
|
||||
Only this README file is guaranteed to be up-to-date.
|
||||
- [Italiano (Italiano, `it`) - v1.19](FAQ.it.md)
|
||||
- [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md)
|
||||
- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.22](FAQ.zh-Hans.md)
|
||||
|
||||
284
FAQ.zh-Hans.md
Normal file
284
FAQ.zh-Hans.md
Normal file
@@ -0,0 +1,284 @@
|
||||
_Only the original [FAQ.md](FAQ.md) is guaranteed to be up-to-date._
|
||||
|
||||
_只有原版的 [FAQ.md](FAQ.md)是保证最新的。_
|
||||
|
||||
Current version is based on [28054cd]
|
||||
|
||||
本文根据[28054cd]进行翻译。
|
||||
|
||||
[28054cd]: https://github.com/Genymobile/scrcpy/blob/28054cd471f848733e11372c9d745cd5d71e6ce7/FAQ.md
|
||||
|
||||
# 常见问题
|
||||
|
||||
这里是一些常见的问题以及他们的状态。
|
||||
|
||||
## `adb` 相关问题
|
||||
|
||||
`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果 `adb` 执行失败了, scrcpy 就无法工作。
|
||||
|
||||
在这种情况中,将会输出这个错误:
|
||||
|
||||
> ERROR: "adb get-serialno" returned with value 1
|
||||
|
||||
这通常不是 _scrcpy_ 的bug,而是你的环境的问题。
|
||||
|
||||
要找出原因,请执行以下操作:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
```
|
||||
|
||||
### 找不到`adb`
|
||||
|
||||
|
||||
你的`PATH`中需要能访问到`adb`。
|
||||
|
||||
在Windows上,当前目录会包含在`PATH`中,并且`adb.exe`也包含在发行版中,因此它应该是开箱即用(直接解压就可以)的。
|
||||
|
||||
|
||||
### 设备未授权
|
||||
|
||||
|
||||
> error: device unauthorized.
|
||||
> This adb server's $ADB_VENDOR_KEYS is not set
|
||||
> Try 'adb kill-server' if that seems wrong.
|
||||
> Otherwise check for a confirmation dialog on your device.
|
||||
|
||||
连接时,在设备上应该会打开一个弹出窗口。 您必须授权 USB 调试。
|
||||
|
||||
如果没有打开,参见[stackoverflow][device-unauthorized].
|
||||
|
||||
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
|
||||
|
||||
|
||||
### 未检测到设备
|
||||
|
||||
> error: no devices/emulators found
|
||||
|
||||
确认已经正确启用 [adb debugging][enable-adb].
|
||||
|
||||
如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上)。这里有一个单独的 [适用于Google设备的USB驱动][google-usb-driver].
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||
[google-usb-driver]: https://developer.android.com/studio/run/win-usb
|
||||
|
||||
|
||||
### 已连接多个设备
|
||||
|
||||
如果连接了多个设备,您将遇到以下错误:
|
||||
|
||||
> error: more than one device/emulator
|
||||
|
||||
必须提供要镜像的设备的标识符:
|
||||
|
||||
```bash
|
||||
scrcpy -s 01234567890abcdef
|
||||
```
|
||||
|
||||
注意,如果你的设备是通过 TCP/IP 连接的, 你将会收到以下消息:
|
||||
|
||||
> adb: error: more than one device/emulator
|
||||
> ERROR: "adb reverse" returned with value 1
|
||||
> WARN: 'adb reverse' failed, fallback to 'adb forward'
|
||||
|
||||
这是意料之中的 (由于旧版安卓的一个bug, 请参见 [#5]),但是在这种情况下,scrcpy会退回到另一种方法,这种方法应该可以起作用。
|
||||
|
||||
[#5]: https://github.com/Genymobile/scrcpy/issues/5
|
||||
|
||||
|
||||
### adb版本之间冲突
|
||||
|
||||
> adb server version (41) doesn't match this client (39); killing...
|
||||
|
||||
同时使用多个版本的`adb`时会发生此错误。你必须查找使用不同`adb`版本的程序,并在所有地方使用相同版本的`adb`。
|
||||
|
||||
你可以覆盖另一个程序中的`adb`二进制文件,或者通过设置`ADB`环境变量来让 _scrcpy_ 使用特定的`adb`二进制文件。
|
||||
|
||||
```bash
|
||||
set ADB=/path/to/your/adb
|
||||
scrcpy
|
||||
```
|
||||
|
||||
|
||||
### 设备断开连接
|
||||
|
||||
如果 _scrcpy_ 在警告“设备连接断开”的情况下自动中止,那就意味着`adb`连接已经断开了。
|
||||
|
||||
请尝试使用另一条USB线或者电脑上的另一个USB接口。请参看 [#281] 和 [#283]。
|
||||
|
||||
[#281]: https://github.com/Genymobile/scrcpy/issues/281
|
||||
[#283]: https://github.com/Genymobile/scrcpy/issues/283
|
||||
|
||||
|
||||
## 控制相关问题
|
||||
|
||||
### 鼠标和键盘不起作用
|
||||
|
||||
|
||||
在某些设备上,您可能需要启用一个选项以允许 [模拟输入][simulating input]。
|
||||
在开发者选项中,打开:
|
||||
|
||||
> **USB调试 (安全设置)**
|
||||
> _允许通过USB调试修改权限或模拟点击_
|
||||
|
||||
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
### 特殊字符不起作用
|
||||
|
||||
可输入的文本[被限制为ASCII字符][text-input]。也可以用一些小技巧输入一些[带重音符号的字符][accented-characters],但是仅此而已。参见[#37]。
|
||||
|
||||
自 Linux 上的 scrcpy v1.20 之后,可以模拟[物理键盘][hid] (HID)。
|
||||
|
||||
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
||||
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
||||
[#37]: https://github.com/Genymobile/scrcpy/issues/37
|
||||
[hid]: README.md#physical-keyboard-simulation-hid
|
||||
|
||||
|
||||
## 客户端相关问题
|
||||
|
||||
### 效果很差
|
||||
|
||||
如果你的客户端窗口分辨率比你的设备屏幕小,则可能出现效果差的问题,尤其是在文本上(参见 [#40])。
|
||||
|
||||
[#40]: https://github.com/Genymobile/scrcpy/issues/40
|
||||
|
||||
为了提升降尺度的质量,如果渲染器是OpenGL并且支持mip映射,就会自动开启三线性过滤。
|
||||
|
||||
在Windows上,你可能希望强制使用OpenGL:
|
||||
|
||||
```
|
||||
scrcpy --render-driver=opengl
|
||||
```
|
||||
|
||||
你可能还需要配置[缩放行为][scaling behavior]:
|
||||
|
||||
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
|
||||
> Override high DPI scaling behavior > Scaling performed by: _Application_.
|
||||
|
||||
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
||||
|
||||
|
||||
### Wayland相关的问题
|
||||
|
||||
在Linux上,SDL默认使用x11。可以通过`SDL_VIDEODRIVER`环境变量来更改[视频驱动][video driver]:
|
||||
|
||||
[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver
|
||||
|
||||
```bash
|
||||
export SDL_VIDEODRIVER=wayland
|
||||
scrcpy
|
||||
```
|
||||
|
||||
在一些发行版上 (至少包括 Fedora), `libdecor` 包必须手动安装。
|
||||
|
||||
参见 [#2554] 和 [#2559]。
|
||||
|
||||
[#2554]: https://github.com/Genymobile/scrcpy/issues/2554
|
||||
[#2559]: https://github.com/Genymobile/scrcpy/issues/2559
|
||||
|
||||
|
||||
### KWin compositor 崩溃
|
||||
|
||||
在Plasma桌面中,当 _scrcpy_ 运行时,会禁用compositor。
|
||||
|
||||
一种解决方法是, [禁用 "Block compositing"][kwin].
|
||||
|
||||
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
||||
|
||||
|
||||
## 崩溃
|
||||
|
||||
### 异常
|
||||
|
||||
可能有很多原因。一个常见的原因是您的设备无法按给定清晰度进行编码:
|
||||
|
||||
> ```
|
||||
> ERROR: Exception on thread Thread[main,5,main]
|
||||
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
||||
> ...
|
||||
> Exit due to uncaughtException in main thread:
|
||||
> ERROR: Could not open video stream
|
||||
> INFO: Initial texture: 1080x2336
|
||||
> ```
|
||||
|
||||
或者
|
||||
|
||||
> ```
|
||||
> ERROR: Exception on thread Thread[main,5,main]
|
||||
> java.lang.IllegalStateException
|
||||
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||
> ```
|
||||
|
||||
请尝试使用更低的清晰度:
|
||||
|
||||
```
|
||||
scrcpy -m 1920
|
||||
scrcpy -m 1024
|
||||
scrcpy -m 800
|
||||
```
|
||||
|
||||
自 scrcpy v1.22以来,scrcpy 会自动在失败前以更低的分辨率重试。这种行为可以用`--no-downsize-on-error`关闭。
|
||||
|
||||
你也可以尝试另一种 [编码器](README.md#encoder)。
|
||||
|
||||
|
||||
如果您在 Android 12 上遇到此异常,则只需升级到 scrcpy >= 1.18 (见 [#2129]):
|
||||
|
||||
```
|
||||
> ERROR: Exception on thread Thread[main,5,main]
|
||||
java.lang.AssertionError: java.lang.reflect.InvocationTargetException
|
||||
at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75)
|
||||
...
|
||||
Caused by: java.lang.reflect.InvocationTargetException
|
||||
at java.lang.reflect.Method.invoke(Native Method)
|
||||
at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73)
|
||||
... 7 more
|
||||
Caused by: java.lang.IllegalArgumentException: displayToken must not be null
|
||||
at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067)
|
||||
at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147)
|
||||
... 9 more
|
||||
```
|
||||
|
||||
[#2129]: https://github.com/Genymobile/scrcpy/issues/2129
|
||||
|
||||
|
||||
## Windows命令行
|
||||
|
||||
从 v1.22 开始,增加了一个“快捷方式”,可以直接在 scrcpy 目录打开一个终端。双击`open_a_terminal_here.bat`,然后输入你的命令。 例如:
|
||||
|
||||
```
|
||||
scrcpy --record file.mkv
|
||||
```
|
||||
|
||||
您也可以打开终端并手动转到 scrcpy 文件夹:
|
||||
|
||||
1. 按下 <kbd>Windows</kbd>+<kbd>r</kbd>,打开一个对话框。
|
||||
2. 输入 `cmd` 并按 <kbd>Enter</kbd>,这样就打开了一个终端。
|
||||
3. 通过输入以下命令,切换到你的 _scrcpy_ 所在的目录 (根据你的实际位置修改路径):
|
||||
|
||||
```bat
|
||||
cd C:\Users\user\Downloads\scrcpy-win64-xxx
|
||||
```
|
||||
|
||||
然后按 <kbd>Enter</kbd>
|
||||
4. 输入你的命令。比如:
|
||||
|
||||
```bat
|
||||
scrcpy --record file.mkv
|
||||
```
|
||||
|
||||
如果你打算总是使用相同的参数,在`scrcpy`目录创建一个文件 `myscrcpy.bat`
|
||||
(启用 [显示文件拓展名][show file extensions] 避免混淆),文件中包含你的命令。例如:
|
||||
|
||||
```bat
|
||||
scrcpy --prefer-text --turn-screen-off --stay-awake
|
||||
```
|
||||
|
||||
然后只需双击刚刚创建的文件。
|
||||
|
||||
你也可以编辑 `scrcpy-console.bat` 或者 `scrcpy-noconsole.vbs`(的副本)来添加参数。
|
||||
|
||||
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
|
||||
1016
README.de.md
Normal file
1016
README.de.md
Normal file
File diff suppressed because it is too large
Load Diff
696
README.id.md
Normal file
696
README.id.md
Normal file
@@ -0,0 +1,696 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
# scrcpy (v1.16)
|
||||
|
||||
Aplikasi ini menyediakan tampilan dan kontrol perangkat Android yang terhubung pada USB (atau [melalui TCP/IP][article-tcpip]). Ini tidak membutuhkan akses _root_ apa pun. Ini bekerja pada _GNU/Linux_, _Windows_ and _macOS_.
|
||||
|
||||

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

|
||||
|
||||
以下に焦点を当てています:
|
||||
|
||||
- **軽量** (ネイティブ、デバイス画面表示のみ)
|
||||
- **パフォーマンス** (30~60fps)
|
||||
- **クオリティ** (1920x1080以上)
|
||||
- **低遅延** ([35~70ms][lowlatency])
|
||||
- **短い起動時間** (初回画像を1秒以内に表示)
|
||||
- **非侵入型** (デバイスに何もインストールされていない状態になる)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## 必要要件
|
||||
|
||||
AndroidデバイスはAPI21(Android 5.0)以上。
|
||||
|
||||
Androidデバイスで[adbデバッグが有効][enable-adb]であること。
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
一部のAndroidデバイスでは、キーボードとマウスを使用して制御する[追加オプション][control]を有効にする必要がある。
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## アプリの取得
|
||||
|
||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
||||
|
||||
### Linux
|
||||
|
||||
Debian (_testing_ と _sid_) とUbuntu(20.04):
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
[Snap]パッケージが利用可能: [`scrcpy`][snap-link]
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
Fedora用[COPR]パッケージが利用可能: [`scrcpy`][copr-link]
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
Arch Linux用[AUR]パッケージが利用可能: [`scrcpy`][aur-link]
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
Gentoo用[Ebuild]が利用可能: [`scrcpy`][ebuild-link]
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
[自分でビルド][BUILD]も可能(心配しないでください、それほど難しくはありません。)
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
Windowsでは簡単に、(`adb`を含む)すべての依存関係を構築済みのアーカイブを利用可能です。
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
[Chocolatey]でも利用可能です:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # まだ入手していない場合
|
||||
```
|
||||
|
||||
[Scoop]でも利用可能です:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # まだ入手していない場合
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
また、[アプリケーションをビルド][BUILD]することも可能です。
|
||||
|
||||
### macOS
|
||||
|
||||
アプリケーションは[Homebrew]で利用可能です。ただインストールするだけです。
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
`PATH`からアクセス可能な`adb`が必要です。もし持っていない場合はインストールしてください。
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
`adb`は[MacPorts]からでもインストールできます。
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
また、[アプリケーションをビルド][BUILD]することも可能です。
|
||||
|
||||
## 実行
|
||||
|
||||
Androidデバイスを接続し、実行:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
次のコマンドでリストされるコマンドライン引数も受け付けます:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## 機能
|
||||
|
||||
### キャプチャ構成
|
||||
|
||||
#### サイズ削減
|
||||
|
||||
Androidデバイスを低解像度でミラーリングする場合、パフォーマンス向上に便利な場合があります。
|
||||
|
||||
幅と高さをある値(例:1024)に制限するには:
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # 短縮版
|
||||
```
|
||||
|
||||
一方のサイズはデバイスのアスペクト比が維持されるように計算されます。この方法では、1920x1080のデバイスでは1024x576にミラーリングされます。
|
||||
|
||||
|
||||
#### ビットレート変更
|
||||
|
||||
ビットレートの初期値は8Mbpsです。ビットレートを変更するには(例:2Mbpsに変更):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # 短縮版
|
||||
```
|
||||
|
||||
#### フレームレート制限
|
||||
|
||||
キャプチャするフレームレートを制限できます:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
この機能はAndroid 10からオフィシャルサポートとなっていますが、以前のバージョンでも動作する可能性があります。
|
||||
|
||||
#### トリミング
|
||||
|
||||
デバイスの画面は、画面の一部のみをミラーリングするようにトリミングできます。
|
||||
|
||||
これは、例えばOculus Goの片方の目をミラーリングする場合に便利です。:
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # オフセット位置(0,0)で1224x1440
|
||||
```
|
||||
|
||||
もし`--max-size`も指定されている場合、トリミング後にサイズ変更が適用されます。
|
||||
|
||||
#### ビデオの向きをロックする
|
||||
|
||||
ミラーリングの向きをロックするには:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # 現在の向き
|
||||
scrcpy --lock-video-orientation=0 # 自然な向き
|
||||
scrcpy --lock-video-orientation=1 # 90°反時計回り
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 90°時計回り
|
||||
```
|
||||
|
||||
この設定は録画の向きに影響します。
|
||||
|
||||
[ウィンドウは独立して回転することもできます](#回転)。
|
||||
|
||||
|
||||
#### エンコーダ
|
||||
|
||||
いくつかのデバイスでは一つ以上のエンコーダを持ちます。それらのいくつかは、問題やクラッシュを引き起こします。別のエンコーダを選択することが可能です:
|
||||
|
||||
|
||||
```bash
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
利用可能なエンコーダをリストするために、無効なエンコーダ名を渡すことができます。エラー表示で利用可能なエンコーダを提供します。
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### キャプチャ
|
||||
|
||||
#### 録画
|
||||
|
||||
ミラーリング中に画面の録画をすることが可能です:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
録画中にミラーリングを無効にするには:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# Ctrl+Cで録画を中断する
|
||||
```
|
||||
|
||||
"スキップされたフレーム"は(パフォーマンス上の理由で)リアルタイムで表示されなくても録画されます。
|
||||
|
||||
フレームはデバイス上で _タイムスタンプされる_ ため [パケット遅延のバリエーション] は録画されたファイルに影響を与えません。
|
||||
|
||||
[パケット遅延のバリエーション]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
Linuxでは、ビデオストリームをv4l2ループバックデバイスに送信することができます。
|
||||
v4l2loopbackのデバイスにビデオストリームを送信することで、Androidデバイスをウェブカメラのようにv4l2対応ツールで開くこともできます。
|
||||
|
||||
`v4l2loopback` モジュールのインストールが必要です。
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
v4l2デバイスを作成する。
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
これにより、新しいビデオデバイスが `/dev/videoN` に作成されます。(`N` は整数)
|
||||
(複数のデバイスや特定のIDのデバイスを作成するために、より多くの[オプション](https://github.com/umlaeute/v4l2loopback#options)が利用可能です。
|
||||
多くの[オプション]()が利用可能で複数のデバイスや特定のIDのデバイスを作成できます。
|
||||
|
||||
|
||||
有効なデバイスを一覧表示する:
|
||||
|
||||
```bash
|
||||
# v4l-utilsパッケージが必要
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# シンプルですが十分これで確認できます
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
v4l2シンクを使用してscrcpyを起動する。
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # ミラーリングウィンドウを無効化する
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # 短縮版
|
||||
```
|
||||
|
||||
(`N` をデバイス ID に置き換えて、`ls /dev/video*` で確認してください)
|
||||
有効にすると、v4l2対応のツールでビデオストリームを開けます。
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLCではバッファリングの遅延が発生する場合があります
|
||||
```
|
||||
|
||||
例えばですが [OBS]の中にこの映像を取り込めことができます。
|
||||
|
||||
[OBS]: https://obsproject.com/
|
||||
|
||||
|
||||
#### Buffering
|
||||
|
||||
バッファリングを追加することも可能です。これによりレイテンシーは増加しますが、ジッターは減少します。(参照
|
||||
[#2464])
|
||||
|
||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
||||
|
||||
このオプションでディスプレイバッファリングを設定できます。
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # ディスプレイに50msのバッファリングを追加する
|
||||
```
|
||||
|
||||
V4L2の場合はこちらのオプションで設定できます。
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink
|
||||
```
|
||||
|
||||
### 接続
|
||||
|
||||
#### ワイヤレス
|
||||
|
||||
_Scrcpy_ はデバイスとの通信に`adb`を使用します。そして`adb`はTCP/IPを介しデバイスに[接続]することができます:
|
||||
|
||||
1. あなたのコンピュータと同じWi-Fiに接続します。
|
||||
2. あなたのIPアドレスを取得します。設定 → 端末情報 → ステータス情報、もしくは、このコマンドを実行します:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. あなたのデバイスでTCP/IPを介したadbを有効にします: `adb tcpip 5555`
|
||||
4. あなたのデバイスの接続を外します。
|
||||
5. あなたのデバイスに接続します:
|
||||
`adb connect DEVICE_IP:5555` _(`DEVICE_IP`は置き換える)_
|
||||
6. 通常通り`scrcpy`を実行します。
|
||||
|
||||
この方法はビットレートと解像度を減らすのにおそらく有用です:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # 短縮版
|
||||
```
|
||||
|
||||
[接続]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### マルチデバイス
|
||||
|
||||
もし`adb devices`でいくつかのデバイスがリストされる場合、 _シリアルナンバー_ を指定する必要があります:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # 短縮版
|
||||
```
|
||||
|
||||
デバイスがTCP/IPを介して接続されている場合:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # 短縮版
|
||||
```
|
||||
|
||||
複数のデバイスに対して、複数の _scrcpy_ インスタンスを開始することができます。
|
||||
|
||||
#### デバイス接続での自動起動
|
||||
|
||||
[AutoAdb]を使用可能です:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### SSHトンネル
|
||||
|
||||
リモートデバイスに接続するため、ローカル`adb`クライアントからリモート`adb`サーバーへ接続することが可能です(同じバージョンの _adb_ プロトコルを使用している場合):
|
||||
|
||||
```bash
|
||||
adb kill-server # 5037ポートのローカルadbサーバーを終了する
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# オープンしたままにする
|
||||
```
|
||||
|
||||
他の端末から:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
リモートポート転送の有効化を回避するためには、代わりに転送接続を強制することができます(`-R`の代わりに`-L`を使用することに注意):
|
||||
|
||||
```bash
|
||||
adb kill-server # 5037ポートのローカルadbサーバーを終了する
|
||||
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# オープンしたままにする
|
||||
```
|
||||
|
||||
他の端末から:
|
||||
|
||||
```bash
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
ワイヤレス接続と同様に、クオリティを下げると便利な場合があります:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### ウィンドウ構成
|
||||
|
||||
#### タイトル
|
||||
|
||||
ウィンドウのタイトルはデバイスモデルが初期値です。これは変更できます:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'My device'
|
||||
```
|
||||
|
||||
#### 位置とサイズ
|
||||
|
||||
ウィンドウの位置とサイズの初期値を指定できます:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### ボーダーレス
|
||||
|
||||
ウィンドウの装飾を無効化するには:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### 常に画面のトップ
|
||||
|
||||
scrcpyの画面を常にトップにするには:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### フルスクリーン
|
||||
|
||||
アプリケーションを直接フルスクリーンで開始できます:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # 短縮版
|
||||
```
|
||||
|
||||
フルスクリーンは、次のコマンドで動的に切り替えることができます <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
|
||||
|
||||
#### 回転
|
||||
|
||||
ウィンドウは回転することができます:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
設定可能な値:
|
||||
- `0`: 回転なし
|
||||
- `1`: 90° 反時計回り
|
||||
- `2`: 180°
|
||||
- `3`: 90° 時計回り
|
||||
|
||||
回転は次のコマンドで動的に変更することができます。 <kbd>MOD</kbd>+<kbd>←</kbd>_(左)_ 、 <kbd>MOD</kbd>+<kbd>→</kbd>_(右)_
|
||||
|
||||
_scrcpy_ は3つの回転を管理することに注意:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd>はデバイスに縦向きと横向きの切り替えを要求する(現在実行中のアプリで要求している向きをサポートしていない場合、拒否することがある)
|
||||
- [`--lock-video-orientation`](#ビデオの向きをロックする)は、ミラーリングする向きを変更する(デバイスからPCへ送信される向き)。録画に影響します。
|
||||
- `--rotation` (もしくは<kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)は、ウィンドウのコンテンツのみを回転します。これは表示にのみに影響し、録画には影響しません。
|
||||
|
||||
### 他のミラーリングオプション
|
||||
|
||||
#### Read-only リードオンリー
|
||||
|
||||
制御を無効にするには(デバイスと対話する全てのもの:入力キー、マウスイベント、ファイルのドラッグ&ドロップ):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### ディスプレイ
|
||||
|
||||
いくつか利用可能なディスプレイがある場合、ミラーリングするディスプレイを選択できます:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
ディスプレイIDのリストは次の方法で取得できます:
|
||||
|
||||
```
|
||||
adb shell dumpsys display # search "mDisplayId=" in the output
|
||||
```
|
||||
|
||||
セカンダリディスプレイは、デバイスが少なくともAndroid 10の場合にコントロール可能です。(それ以外ではリードオンリーでミラーリングされます)
|
||||
|
||||
|
||||
#### 起動状態にする
|
||||
|
||||
デバイス接続時、少し遅れてからデバイスのスリープを防ぐには:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
scrcpyが閉じられた時、初期状態に復元されます。
|
||||
|
||||
#### 画面OFF
|
||||
|
||||
コマンドラインオプションを使用することで、ミラーリングの開始時にデバイスの画面をOFFにすることができます:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
もしくは、<kbd>MOD</kbd>+<kbd>o</kbd>を押すことでいつでもできます。
|
||||
|
||||
元に戻すには、<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>を押します。
|
||||
|
||||
Androidでは、`POWER`ボタンはいつでも画面を表示します。便宜上、`POWER`がscrcpyを介して(右クリックもしくは<kbd>MOD</kbd>+<kbd>p</kbd>を介して)送信される場合、(ベストエフォートベースで)少し遅れて、強制的に画面を非表示にします。ただし、物理的な`POWER`ボタンを押した場合は、画面は表示されます。
|
||||
|
||||
このオプションはデバイスがスリープしないようにすることにも役立ちます:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
|
||||
#### タッチを表示
|
||||
|
||||
プレゼンテーションの場合(物理デバイス上で)物理的なタッチを表示すると便利な場合があります。
|
||||
|
||||
Androidはこの機能を _開発者オプション_ で提供します。
|
||||
|
||||
_Scrcpy_ は開始時にこの機能を有効にし、終了時に初期値を復元するオプションを提供します:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
(デバイス上で指を使った) _物理的な_ タッチのみ表示されることに注意してください。
|
||||
|
||||
|
||||
#### スクリーンセーバー無効
|
||||
|
||||
初期状態では、scrcpyはコンピュータ上でスクリーンセーバーが実行される事を妨げません。
|
||||
|
||||
これを無効にするには:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### 入力制御
|
||||
|
||||
#### デバイス画面の回転
|
||||
|
||||
<kbd>MOD</kbd>+<kbd>r</kbd>を押すことで、縦向きと横向きを切り替えます。
|
||||
|
||||
フォアグラウンドのアプリケーションが要求された向きをサポートしている場合のみ回転することに注意してください。
|
||||
|
||||
#### コピー-ペースト
|
||||
|
||||
Androidのクリップボードが変更される度に、コンピュータのクリップボードに自動的に同期されます。
|
||||
|
||||
<kbd>Ctrl</kbd>のショートカットは全てデバイスに転送されます。特に:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> 通常はコピーします
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> 通常はカットします
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> 通常はペーストします(コンピュータとデバイスのクリップボードが同期された後)
|
||||
|
||||
通常は期待通りに動作します。
|
||||
|
||||
しかしながら、実際の動作はアクティブなアプリケーションに依存します。例えば、_Termux_ は代わりに<kbd>Ctrl</kbd>+<kbd>c</kbd>でSIGINTを送信します、そして、_K-9 Mail_ は新しいメッセージを作成します。
|
||||
|
||||
このようなケースでコピー、カットそしてペーストをするには(Android 7以上でのサポートのみですが):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> `COPY`を挿入
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> `CUT`を挿入
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> `PASTE`を挿入(コンピュータとデバイスのクリップボードが同期された後)
|
||||
|
||||
加えて、<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>はコンピュータのクリップボードテキストにキーイベントのシーケンスとして挿入することを許可します。これはコンポーネントがテキストのペーストを許可しない場合(例えば _Termux_)に有用ですが、非ASCIIコンテンツを壊す可能性があります。
|
||||
|
||||
**警告:** デバイスにコンピュータのクリップボードを(<kbd>Ctrl</kbd>+<kbd>v</kbd>または<kbd>MOD</kbd>+<kbd>v</kbd>を介して)ペーストすることは、デバイスのクリップボードにコンテンツをコピーします。結果としてどのAndoridアプリケーションもそのコンテンツを読み取ることができます。機密性の高いコンテンツ(例えばパスワードなど)をこの方法でペーストすることは避けてください。
|
||||
|
||||
プログラムでデバイスのクリップボードを設定した場合、一部のデバイスは期待どおりに動作しません。`--legacy-paste`オプションは、コンピュータのクリップボードテキストをキーイベントのシーケンスとして挿入するため(<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>と同じ方法)、<kbd>Ctrl</kbd>+<kbd>v</kbd>と<kbd>MOD</kbd>+<kbd>v</kbd>の動作の変更を提供します。
|
||||
|
||||
#### ピンチしてズームする
|
||||
|
||||
"ピンチしてズームする"をシミュレートするには: <kbd>Ctrl</kbd>+_クリック&移動_
|
||||
|
||||
より正確にするには、左クリックボタンを押している間、<kbd>Ctrl</kbd>を押したままにします。左クリックボタンを離すまで、全てのマウスの動きは、(アプリでサポートされている場合)画面の中心を基準として、コンテンツを拡大縮小および回転します。
|
||||
|
||||
具体的には、scrcpyは画面の中央を反転した位置にある"バーチャルフィンガー"から追加のタッチイベントを生成します。
|
||||
|
||||
|
||||
#### テキストインジェクション環境設定
|
||||
|
||||
テキストをタイプした時に生成される2種類の[イベント][textevents]があります:
|
||||
- _key events_ はキーを押したときと離したことを通知します。
|
||||
- _text events_ はテキストが入力されたことを通知します。
|
||||
|
||||
初期状態で、文字はキーイベントで挿入されるため、キーボードはゲームで期待通りに動作します(通常はWASDキー)。
|
||||
|
||||
しかし、これは[問題を引き起こす][prefertext]かもしれません。もしこのような問題が発生した場合は、この方法で回避できます:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(しかしこの方法はゲームのキーボードの動作を壊します)
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### キーの繰り返し
|
||||
|
||||
初期状態では、キーの押しっぱなしは繰り返しのキーイベントを生成します。これらのイベントが使われない場合でも、この方法は一部のゲームでパフォーマンスの問題を引き起す可能性があります。
|
||||
|
||||
繰り返しのキーイベントの転送を回避するためには:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
|
||||
#### 右クリックと真ん中クリック
|
||||
|
||||
初期状態では、右クリックはバックの動作(もしくはパワーオン)を起こし、真ん中クリックではホーム画面へ戻ります。このショートカットを無効にし、代わりにデバイスへクリックを転送するには:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### ファイルのドロップ
|
||||
|
||||
#### APKのインストール
|
||||
|
||||
APKをインストールするには、(`.apk`で終わる)APKファイルを _scrcpy_ の画面にドラッグ&ドロップします。
|
||||
|
||||
見た目のフィードバックはありません。コンソールにログが出力されます。
|
||||
|
||||
|
||||
#### デバイスにファイルを送る
|
||||
|
||||
デバイスの`/sdcard/Download`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。
|
||||
|
||||
見た目のフィードバックはありません。コンソールにログが出力されます。
|
||||
|
||||
転送先ディレクトリを起動時に変更することができます:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
|
||||
### 音声転送
|
||||
|
||||
音声は _scrcpy_ では転送されません。[sndcpy]を使用します。
|
||||
|
||||
[issue #14]も参照ください。
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## ショートカット
|
||||
|
||||
次のリストでは、<kbd>MOD</kbd>でショートカット変更します。初期状態では、(left)<kbd>Alt</kbd>または(left)<kbd>Super</kbd>です。
|
||||
|
||||
これは`--shortcut-mod`で変更することができます。可能なキーは`lctrl`、`rctrl`、`lalt`、 `ralt`、 `lsuper`そして`rsuper`です。例えば:
|
||||
|
||||
```bash
|
||||
# RCtrlをショートカットとして使用します
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# ショートカットにLCtrl+LAltまたはLSuperのいずれかを使用します
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd>は通常<kbd>Windows</kbd>もしくは<kbd>Cmd</kbd>キーです。_
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| アクション | ショートカット
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| フルスクリーンモードへの切り替え | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| ディスプレイを左に回転 | <kbd>MOD</kbd>+<kbd>←</kbd> _(左)_
|
||||
| ディスプレイを右に回転 | <kbd>MOD</kbd>+<kbd>→</kbd> _(右)_
|
||||
| ウィンドウサイズを変更して1:1に変更(ピクセルパーフェクト) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| ウィンドウサイズを変更して黒い境界線を削除 | <kbd>MOD</kbd>+<kbd>w</kbd> \| _ダブルクリック¹_
|
||||
| `HOME`をクリック | <kbd>MOD</kbd>+<kbd>h</kbd> \| _真ん中クリック_
|
||||
| `BACK`をクリック | <kbd>MOD</kbd>+<kbd>b</kbd> \| _右クリック²_
|
||||
| `APP_SWITCH`をクリック | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4クリック³_
|
||||
| `MENU` (画面のアンロック)をクリック | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| `VOLUME_UP`をクリック | <kbd>MOD</kbd>+<kbd>↑</kbd> _(上)_
|
||||
| `VOLUME_DOWN`をクリック | <kbd>MOD</kbd>+<kbd>↓</kbd> _(下)_
|
||||
| `POWER`をクリック | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| 電源オン | _右クリック²_
|
||||
| デバイス画面をオフにする(ミラーリングしたまま) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| デバイス画面をオンにする | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| デバイス画面を回転する | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| 通知パネルを展開する | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5ボタンクリック³_
|
||||
| 設定パネルを展開する | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _5ダブルクリック³_
|
||||
| 通知パネルを折りたたむ | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| クリップボードへのコピー³ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| クリップボードへのカット³ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| クリップボードの同期とペースト³ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| コンピュータのクリップボードテキストの挿入 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| FPSカウンタ有効/無効(標準入出力上) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| ピンチしてズームする | <kbd>Ctrl</kbd>+_クリック&移動_
|
||||
|
||||
_¹黒い境界線を削除するため、境界線上でダブルクリック_
|
||||
_²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._
|
||||
_³4と5はマウスのボタンです、もしあなたのマウスにボタンがあれば使えます._
|
||||
_⁴Android 7以上のみ._
|
||||
|
||||
キーを繰り返すショートカットはキーを離して2回目を押したら実行されます。例えば「設定パネルを展開する」を実行する場合は以下のように操作する。
|
||||
|
||||
1. <kbd>MOD</kbd> キーを押し、押したままにする.
|
||||
2. その後に <kbd>n</kbd>キーを2回押す.
|
||||
3. 最後に <kbd>MOD</kbd>キーを離す.
|
||||
|
||||
全ての<kbd>Ctrl</kbd>+_キー_ ショートカットはデバイスに転送されます、そのためアクティブなアプリケーションによって処理されます。
|
||||
|
||||
## カスタムパス
|
||||
|
||||
特定の _adb_ バイナリを使用する場合、そのパスを環境変数`ADB`で構成します:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
`scrcpy-server`ファイルのパスを上書きするには、`SCRCPY_SERVER_PATH`でそのパスを構成します。
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
|
||||
## なぜ _scrcpy_?
|
||||
|
||||
同僚が私に、[gnirehtet]のように発音できない名前を見つけるように要求しました。
|
||||
|
||||
[`strcpy`]は**str**ingをコピーします。`scrcpy`は**scr**eenをコピーします。
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## ビルド方法は?
|
||||
|
||||
[BUILD]を参照してください。
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
|
||||
## よくある質問
|
||||
|
||||
[FAQ](FAQ.md)を参照してください。
|
||||
|
||||
|
||||
## 開発者
|
||||
|
||||
[開発者のページ]を読んでください。
|
||||
|
||||
[開発者のページ]: DEVELOP.md
|
||||
|
||||
|
||||
## ライセンス
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## 記事
|
||||
|
||||
- [Introducing scrcpy][article-intro]
|
||||
- [Scrcpy now works wirelessly][article-tcpip]
|
||||
|
||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
498
README.ko.md
Normal file
498
README.ko.md
Normal file
@@ -0,0 +1,498 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
# scrcpy (v1.11)
|
||||
|
||||
This document will be updated frequently along with the original Readme file
|
||||
이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다
|
||||
|
||||
이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다.
|
||||
_GNU/Linux_, _Windows_ 와 _macOS_ 상에서 작동합니다.
|
||||
(아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.)
|
||||
|
||||
[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
|
||||

|
||||
|
||||
주요 기능은 다음과 같습니다.
|
||||
|
||||
- **가벼움** (기본적이며 디바이스의 화면만을 보여줌)
|
||||
- **뛰어난 성능** (30~60fps)
|
||||
- **높은 품질** (1920×1080 이상의 해상도)
|
||||
- **빠른 반응 속도** ([35~70ms][lowlatency])
|
||||
- **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨)
|
||||
- **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## 요구사항
|
||||
|
||||
안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다.
|
||||
|
||||
디바이스에 [adb debugging][enable-adb]이 가능한지 확인하십시오.
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
어떤 디바이스에서는, 키보드와 마우스를 사용하기 위해서 [추가 옵션][control] 이 필요하기도 합니다.
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## 앱 설치하기
|
||||
|
||||
|
||||
### Linux (리눅스)
|
||||
|
||||
리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다.
|
||||
|
||||
[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md
|
||||
|
||||
[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link].
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
Arch Linux에서, [AUR] 패키지가 가능합니다 : [`scrcpy`][aur-link].
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link].
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
|
||||
### Windows (윈도우)
|
||||
|
||||
윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) :
|
||||
해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다.
|
||||
- [README](README.md#windows)
|
||||
|
||||
|
||||
[어플을 직접 설치][BUILD] 할 수도 있습니다.
|
||||
|
||||
|
||||
### macOS (맥 OS)
|
||||
|
||||
이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 :
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 :
|
||||
|
||||
```bash
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
[어플을 직접 설치][BUILD] 할 수도 있습니다.
|
||||
|
||||
|
||||
## 실행
|
||||
|
||||
안드로이드 디바이스를 연결하고 실행하십시오:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
다음과 같이 명령창 옵션 기능도 제공합니다.
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## 기능
|
||||
|
||||
### 캡쳐 환경 설정
|
||||
|
||||
|
||||
### 사이즈 재정의
|
||||
|
||||
가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다.
|
||||
|
||||
너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) :
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # 축약 버전
|
||||
```
|
||||
|
||||
이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다.
|
||||
이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다.
|
||||
|
||||
|
||||
### bit-rate 변경
|
||||
|
||||
기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # 축약 버전
|
||||
```
|
||||
|
||||
### 프레임 비율 제한
|
||||
|
||||
안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
|
||||
### Crop (잘라내기)
|
||||
|
||||
디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다.
|
||||
|
||||
예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 :
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
|
||||
scrcpy -c 1224:1440:0:0 # 축약 버전
|
||||
```
|
||||
|
||||
만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다.
|
||||
|
||||
|
||||
### 화면 녹화
|
||||
|
||||
미러링하는 동안 화면 녹화를 할 수 있습니다 :
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
녹화하는 동안 미러링을 멈출 수 있습니다 :
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# Ctrl+C 로 녹화를 중단할 수 있습니다.
|
||||
# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오.
|
||||
```
|
||||
|
||||
"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay
|
||||
variation] 은 녹화된 파일에 영향을 끼치지 않습니다.
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
## 연결
|
||||
|
||||
### 무선연결
|
||||
|
||||
_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 :
|
||||
|
||||
1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다.
|
||||
2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ).
|
||||
3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`.
|
||||
4. 디바이스 연결을 해제합니다.
|
||||
5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` 대신)_.
|
||||
6. `scrcpy` 실행합니다.
|
||||
|
||||
다음은 bit-rate 와 해상도를 줄이는데 유용합니다 :
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # 축약 버전
|
||||
```
|
||||
|
||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
|
||||
### 여러 디바이스 사용 가능
|
||||
|
||||
만약에 여러 디바이스들이 `adb devices` 목록에 표시되었다면, _serial_ 을 명시해야합니다:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # 축약 버전
|
||||
```
|
||||
|
||||
_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다.
|
||||
|
||||
|
||||
#### SSH tunnel
|
||||
|
||||
떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.):
|
||||
|
||||
```bash
|
||||
adb kill-server # 5037의 로컬 local adb server를 중단
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# 실행 유지
|
||||
```
|
||||
|
||||
다른 터미널에서는 :
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
## Window에서의 배치
|
||||
|
||||
### 맞춤형 window 제목
|
||||
|
||||
기본적으로, window의 이름은 디바이스의 모델명 입니다.
|
||||
다음의 명령어를 통해 변경하세요.
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'My device'
|
||||
```
|
||||
|
||||
|
||||
### 배치와 크기
|
||||
|
||||
초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
|
||||
### 경계 없애기
|
||||
|
||||
윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
### 항상 모든 윈도우 위에 실행창 고정
|
||||
|
||||
이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
scrcpy -T # 축약 버전
|
||||
```
|
||||
|
||||
### 전체 화면
|
||||
|
||||
이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다.
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # short version
|
||||
```
|
||||
|
||||
전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다.
|
||||
|
||||
|
||||
## 다른 미러링 옵션
|
||||
|
||||
### 읽기 전용(Read-only)
|
||||
|
||||
권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
### 화면 끄기
|
||||
|
||||
미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는
|
||||
다음의 커맨드 라인 옵션을(command line option) 입력하세요:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다.
|
||||
|
||||
다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요.
|
||||
|
||||
|
||||
### 유효기간이 지난 프레임 제공 (Render expired frames)
|
||||
|
||||
디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다
|
||||
과거의 프레임은 하나씩 삭제합니다.
|
||||
|
||||
모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다)
|
||||
다음의 명령어를 사용하세요:
|
||||
|
||||
```bash
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
|
||||
### 화면에 터치 나타내기
|
||||
|
||||
발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다.
|
||||
|
||||
안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다.
|
||||
|
||||
_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다.
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위).
|
||||
|
||||
|
||||
### 입력 제어
|
||||
|
||||
#### 복사-붙여넣기
|
||||
|
||||
컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다:
|
||||
|
||||
- `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다;
|
||||
- `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다;
|
||||
- `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 )
|
||||
|
||||
#### 텍스트 삽입 우선 순위
|
||||
|
||||
텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다:
|
||||
- _key events_, 키가 눌려있는 지에 대한 신호;
|
||||
- _text events_, 텍스트가 입력되었는지에 대한 신호.
|
||||
|
||||
기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ).
|
||||
|
||||
그러나 이는 [issues 를 발생][prefertext]시킵니다. 이와 관련된 문제를 접할 경우, 아래와 같이 피할 수 있습니다:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
( 그러나 이는 게임에서의 처리를 중단할 수 있습니다 )
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
### 파일 드랍
|
||||
|
||||
### APK 실행하기
|
||||
|
||||
APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop)
|
||||
|
||||
시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다.
|
||||
|
||||
### 디바이스에 파일 push하기
|
||||
|
||||
디바이스의`/sdcard/`에 파일을 push하기 위해서는,
|
||||
APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop).
|
||||
|
||||
시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다.
|
||||
|
||||
해당 디렉토리는 시작할 때 변경이 가능합니다:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
### 오디오의 전달
|
||||
|
||||
_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요.
|
||||
|
||||
추가적으로 [issue #14]를 참고하세요.
|
||||
|
||||
[USBaudio]: https://github.com/rom1v/usbaudio
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
## 단축키
|
||||
|
||||
| 실행내용 | 단축키 | 단축키 (macOS)
|
||||
| -------------------------------------- |:----------------------------- |:-----------------------------
|
||||
| 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f`
|
||||
| window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g`
|
||||
| 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_
|
||||
|`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_
|
||||
| `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_
|
||||
| `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s`
|
||||
| `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m`
|
||||
| `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_
|
||||
| `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_
|
||||
| `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p`
|
||||
| 전원 켜기 | _Right-click²_ | _Right-click²_
|
||||
| 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o`
|
||||
| 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n`
|
||||
| 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n`
|
||||
| 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c`
|
||||
| 컴퓨터의 clipboard 디바이스에 붙여넣기 | `Ctrl`+`v` | `Cmd`+`v`
|
||||
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v`
|
||||
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i`
|
||||
|
||||
_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_
|
||||
_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다.
|
||||
|
||||
## 맞춤 경로 (custom path)
|
||||
|
||||
특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요.
|
||||
`ADB`:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요.
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
|
||||
## _scrcpy_ 인 이유?
|
||||
|
||||
한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다.
|
||||
|
||||
[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다.
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
|
||||
## 빌드하는 방법?
|
||||
|
||||
[BUILD]을 참고하세요.
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
## 흔한 issue
|
||||
|
||||
[FAQ](FAQ.md)을 참고하세요.
|
||||
|
||||
|
||||
## 개발자들
|
||||
|
||||
[developers page]를 참고하세요.
|
||||
|
||||
[developers page]: DEVELOP.md
|
||||
|
||||
|
||||
## 라이선스
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 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.
|
||||
|
||||
## 관련 글 (articles)
|
||||
|
||||
- [scrcpy 소개][article-intro]
|
||||
- [무선으로 연결하는 Scrcpy][article-tcpip]
|
||||
|
||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
111
README.md
111
README.md
@@ -1,4 +1,4 @@
|
||||
# scrcpy (v1.25)
|
||||
# scrcpy (v1.24)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -106,10 +106,10 @@ process][BUILD_simple]).
|
||||
For Windows, a prebuilt archive with all the dependencies (including `adb`) is
|
||||
available:
|
||||
|
||||
- [`scrcpy-win64-v1.25.zip`][direct-win64]
|
||||
<sub>SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028`</sub>
|
||||
- [`scrcpy-win64-v1.24.zip`][direct-win64]
|
||||
<sub>SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367`</sub>
|
||||
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip
|
||||
|
||||
It is also available in [Chocolatey]:
|
||||
|
||||
@@ -186,7 +186,7 @@ increase performance.
|
||||
To limit both the width and height to some value (e.g. 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size=1024
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # short version
|
||||
```
|
||||
|
||||
@@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
|
||||
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate=2M
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # short version
|
||||
```
|
||||
|
||||
@@ -208,7 +208,7 @@ scrcpy -b 2M # short version
|
||||
The capture frame rate can be limited:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps=15
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
This is officially supported since Android 10, but may work on earlier versions.
|
||||
@@ -229,7 +229,7 @@ The device screen may be cropped to mirror only part of the screen.
|
||||
This is useful, for example, to mirror only one eye of the Oculus Go:
|
||||
|
||||
```bash
|
||||
scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
|
||||
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
|
||||
```
|
||||
|
||||
If `--max-size` is also specified, resizing is applied after cropping.
|
||||
@@ -252,33 +252,20 @@ This affects recording orientation.
|
||||
The [window may also be rotated](#rotation) independently.
|
||||
|
||||
|
||||
#### Codec
|
||||
#### Encoder
|
||||
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
Some devices have more than one encoder, and some of them may cause issues or
|
||||
crash. It is possible to select a different encoder:
|
||||
|
||||
```bash
|
||||
scrcpy --codec=h264 # default
|
||||
scrcpy --codec=h265
|
||||
scrcpy --codec=av1
|
||||
```
|
||||
|
||||
|
||||
##### Encoder
|
||||
|
||||
Some devices have more than one encoder for a specific codec, and some of them
|
||||
may cause issues or crash. It is possible to select a different encoder:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=OMX.qcom.video.encoder.avc
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
To list the available encoders, you can pass an invalid encoder name; the
|
||||
error will give the available encoders:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=_ # for the default codec
|
||||
scrcpy --codec=h265 --encoder=_ # for a specific codec
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### Capture
|
||||
@@ -288,14 +275,14 @@ scrcpy --codec=h265 --encoder=_ # for a specific codec
|
||||
It is possible to record the screen while mirroring:
|
||||
|
||||
```bash
|
||||
scrcpy --record=file.mp4
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
To disable mirroring while recording:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record=file.mp4
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# interrupt recording with Ctrl+C
|
||||
```
|
||||
@@ -444,7 +431,7 @@ none found, try running `adb disconnect`, and then run those two commands again.
|
||||
It may be useful to decrease the bit-rate and the resolution:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate=2M --max-size=800
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # short version
|
||||
```
|
||||
|
||||
@@ -456,7 +443,7 @@ scrcpy -b2M -m800 # short version
|
||||
If several devices are listed in `adb devices`, you can specify the _serial_:
|
||||
|
||||
```bash
|
||||
scrcpy --serial=0123456789abcdef
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # short version
|
||||
```
|
||||
|
||||
@@ -466,7 +453,7 @@ The serial may also be provided via the environment variable `ANDROID_SERIAL`
|
||||
If the device is connected over TCP/IP:
|
||||
|
||||
```bash
|
||||
scrcpy --serial=192.168.0.1:5555
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # short version
|
||||
```
|
||||
|
||||
@@ -518,23 +505,10 @@ Suppose that this server is accessible at 192.168.1.2. Then, from another
|
||||
terminal, run `scrcpy`:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037'
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
By default, `scrcpy` uses the local port used for `adb forward` tunnel
|
||||
establishment (typically `27183`, see `--port`). It is also possible to force a
|
||||
different tunnel port (it may be useful in more complex situations, when more
|
||||
@@ -568,23 +542,10 @@ ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
From another terminal, run `scrcpy`:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
|
||||
scrcpy
|
||||
```
|
||||
|
||||
To avoid enabling remote port forwarding, you could force a forward connection
|
||||
instead (notice the `-L` instead of `-R`):
|
||||
|
||||
@@ -598,28 +559,15 @@ ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
From another terminal, run `scrcpy`:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
Like for wireless connections, it may be useful to reduce quality:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps=15
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### Window configuration
|
||||
@@ -629,7 +577,7 @@ scrcpy -b2M -m800 --max-fps=15
|
||||
By default, the window title is the device model. It can be changed:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title='My device'
|
||||
scrcpy --window-title 'My device'
|
||||
```
|
||||
|
||||
#### Position and size
|
||||
@@ -637,7 +585,7 @@ scrcpy --window-title='My device'
|
||||
The initial window position and size may be specified:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### Borderless
|
||||
@@ -672,7 +620,7 @@ Fullscreen can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>.
|
||||
The window may be rotated:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation=1
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
Possible values:
|
||||
@@ -714,7 +662,7 @@ If several displays are available, it is possible to select the display to
|
||||
mirror:
|
||||
|
||||
```bash
|
||||
scrcpy --display=1
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
The list of display ids can be retrieved by:
|
||||
@@ -1222,8 +1170,17 @@ For general questions or discussions, you can also use:
|
||||
|
||||
## Translations
|
||||
|
||||
Translations of this README in other languages are available in the [wiki].
|
||||
This README is available in other languages:
|
||||
|
||||
[wiki]: https://github.com/Genymobile/scrcpy/wiki
|
||||
- [Deutsch (German, `de`) - v1.22](README.de.md)
|
||||
- [Indonesian (Indonesia, `id`) - v1.16](README.id.md)
|
||||
- [Italiano (Italiano, `it`) - v1.23](README.it.md)
|
||||
- [日本語 (Japanese, `jp`) - v1.19](README.jp.md)
|
||||
- [한국어 (Korean, `ko`) - v1.11](README.ko.md)
|
||||
- [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md)
|
||||
- [Español (Spanish, `sp`) - v1.21](README.sp.md)
|
||||
- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.22](README.zh-Hans.md)
|
||||
- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md)
|
||||
- [Turkish (Turkish, `tr`) - v1.18](README.tr.md)
|
||||
|
||||
Only this README file is guaranteed to be up-to-date.
|
||||
|
||||
880
README.pt-br.md
Normal file
880
README.pt-br.md
Normal file
@@ -0,0 +1,880 @@
|
||||
_Apenas o [README](README.md) original é garantido estar atualizado._
|
||||
|
||||
# scrcpy (v1.19)
|
||||
|
||||
Esta aplicação fornece exibição e controle de dispositivos Android conectados via
|
||||
USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso _root_.
|
||||
Funciona em _GNU/Linux_, _Windows_ e _macOS_.
|
||||
|
||||

|
||||
|
||||
Foco em:
|
||||
|
||||
- **leveza** (nativo, mostra apenas a tela do dispositivo)
|
||||
- **performance** (30~60fps)
|
||||
- **qualidade** (1920×1080 ou acima)
|
||||
- **baixa latência** ([35~70ms][lowlatency])
|
||||
- **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem)
|
||||
- **não intrusivo** (nada é deixado instalado no dispositivo)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## Requisitos
|
||||
|
||||
O dispositivo Android requer pelo menos a API 21 (Android 5.0).
|
||||
|
||||
Tenha certeza de ter [ativado a depuração adb][enable-adb] no(s) seu(s) dispositivo(s).
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
Em alguns dispositivos, você também precisa ativar [uma opção adicional][control] para
|
||||
controlá-lo usando teclado e mouse.
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## Obter o app
|
||||
|
||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
||||
|
||||
### Sumário
|
||||
|
||||
- Linux: `apt install scrcpy`
|
||||
- Windows: [baixar][direct-win64]
|
||||
- macOS: `brew install scrcpy`
|
||||
|
||||
Compilar pelos arquivos fontes: [BUILD] ([processo simplificado][BUILD_simple])
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
[BUILD_simple]: BUILD.md#simple
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04):
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
Um pacote [Snap] está disponível: [`scrcpy`][snap-link].
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
Para Fedora, um pacote [COPR] está disponível: [`scrcpy`][copr-link].
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link].
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link].
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
Você também pode [compilar o app manualmente][BUILD] ([processo simplificado][BUILD_simple]).
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
Para Windows, por simplicidade, um arquivo pré-compilado com todas as dependências
|
||||
(incluindo `adb`) está disponível:
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
Também está disponível em [Chocolatey]:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # se você ainda não o tem
|
||||
```
|
||||
|
||||
E no [Scoop]:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # se você ainda não o tem
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
Você também pode [compilar o app manualmente][BUILD].
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
A aplicação está disponível em [Homebrew]. Apenas instale-a:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
Está também disponivel em [MacPorts], que prepara o adb para você:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
|
||||
Você também pode [compilar o app manualmente][BUILD].
|
||||
|
||||
|
||||
## Executar
|
||||
|
||||
Conecte um dispositivo Android e execute:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Também aceita argumentos de linha de comando, listados por:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
### Configuração de captura
|
||||
|
||||
#### Reduzir tamanho
|
||||
|
||||
Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para
|
||||
aumentar a performance.
|
||||
|
||||
Para limitar ambos (largura e altura) para algum valor (ex: 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # versão curta
|
||||
```
|
||||
|
||||
A outra dimensão é calculada para que a proporção do dispositivo seja preservada.
|
||||
Dessa forma, um dispositivo de 1920x1080 será espelhado em 1024x576.
|
||||
|
||||
|
||||
#### Mudar bit-rate
|
||||
|
||||
O bit-rate padrão é 8 Mbps. Para mudar o bit-rate do vídeo (ex: para 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # versão curta
|
||||
```
|
||||
|
||||
#### Limitar frame rate
|
||||
|
||||
O frame rate de captura pode ser limitado:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
Isso é oficialmente suportado desde o Android 10, mas pode funcionar em versões anteriores.
|
||||
|
||||
#### Cortar
|
||||
|
||||
A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela.
|
||||
|
||||
Isso é útil por exemplo, para espelhar apenas um olho do Oculus Go:
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0)
|
||||
```
|
||||
|
||||
Se `--max-size` também for especificado, o redimensionamento é aplicado após o corte.
|
||||
|
||||
|
||||
#### Travar orientação do vídeo
|
||||
|
||||
|
||||
Para travar a orientação do espelhamento:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # orientação inicial (Atual)
|
||||
scrcpy --lock-video-orientation=0 # orientação natural
|
||||
scrcpy --lock-video-orientation=1 # 90° sentido anti-horário
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 90° sentido horário
|
||||
```
|
||||
|
||||
Isso afeta a orientação de gravação.
|
||||
|
||||
A [janela também pode ser rotacionada](#rotação) independentemente.
|
||||
|
||||
|
||||
#### Encoder
|
||||
|
||||
Alguns dispositivos têm mais de um encoder, e alguns deles podem causar problemas ou
|
||||
travar. É possível selecionar um encoder diferente:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
Para listar os encoders disponíveis, você pode passar um nome de encoder inválido, o
|
||||
erro dará os encoders disponíveis:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### Captura
|
||||
|
||||
#### Gravando
|
||||
|
||||
É possível gravar a tela enquanto ocorre o espelhamento:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
Para desativar o espelhamento durante a gravação:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# interrompa a gravação com Ctrl+C
|
||||
```
|
||||
|
||||
"Frames pulados" são gravados, mesmo que não sejam exibidos em tempo real (por
|
||||
motivos de performance). Frames têm seu _horário carimbado_ no dispositivo, então [variação de atraso nos
|
||||
pacotes][packet delay variation] não impacta o arquivo gravado.
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
Em Linux, é possível enviar a transmissão do video para um disposiivo v4l2 loopback, assim
|
||||
o dispositivo Android pode ser aberto como uma webcam por qualquer ferramneta capaz de v4l2
|
||||
|
||||
The module `v4l2loopback` must be installed:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
Para criar um dispositivo v4l2:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
Isso criara um novo dispositivo de vídeo em `/dev/videoN`, onde `N` é uma integer
|
||||
(mais [opções](https://github.com/umlaeute/v4l2loopback#options) estão disponiveis
|
||||
para criar varios dispositivos ou dispositivos com IDs específicas).
|
||||
|
||||
Para listar os dispositivos disponíveis:
|
||||
|
||||
```bash
|
||||
# requer o pacote v4l-utils
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# simples, mas pode ser suficiente
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
Para iniciar o scrcpy usando o coletor v4l2 (sink):
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # desativa a janela espelhada
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # versão curta
|
||||
```
|
||||
|
||||
(troque `N` pelo ID do dipositivo, verifique com `ls /dev/video*`)
|
||||
|
||||
Uma vez ativado, você pode abrir suas trasmissões de videos com uma ferramenta capaz de v4l2:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC pode adicionar um pouco de atraso de buffering
|
||||
```
|
||||
|
||||
Por exemplo, você pode capturar o video dentro do [OBS].
|
||||
|
||||
[OBS]: https://obsproject.com/
|
||||
|
||||
|
||||
#### Buffering
|
||||
|
||||
É possivel adicionar buffering. Isso aumenta a latência, mas reduz a tenção (jitter) (veja
|
||||
[#2464]).
|
||||
|
||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
||||
|
||||
A opção éta disponivel para buffering de exibição:
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # adiciona 50 ms de buffering para a exibição
|
||||
```
|
||||
|
||||
e coletor V4L2:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-buffer=500 # adiciona 500 ms de buffering para coletor V4L2
|
||||
```
|
||||
|
||||
,
|
||||
### Conexão
|
||||
|
||||
#### Sem fio
|
||||
|
||||
_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se][connect] a um
|
||||
dispositivo via TCP/IP:
|
||||
|
||||
1. Conecte o dispositivo no mesmo Wi-Fi do seu computador.
|
||||
2. Pegue o endereço IP do seu dispositivo, em Configurações → Sobre o telefone → Status, ou
|
||||
executando este comando:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`.
|
||||
4. Desconecte seu dispositivo.
|
||||
5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua `DEVICE_IP`)_.
|
||||
6. Execute `scrcpy` como de costume.
|
||||
|
||||
Pode ser útil diminuir o bit-rate e a resolução:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # versão curta
|
||||
```
|
||||
|
||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### Múltiplos dispositivos
|
||||
|
||||
Se vários dispositivos são listados em `adb devices`, você deve especificar o _serial_:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # versão curta
|
||||
```
|
||||
|
||||
Se o dispositivo está conectado via TCP/IP:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # versão curta
|
||||
```
|
||||
|
||||
Você pode iniciar várias instâncias do _scrcpy_ para vários dispositivos.
|
||||
|
||||
#### Iniciar automaticamente quando dispositivo é conectado
|
||||
|
||||
Você pode usar [AutoAdb]:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### Túnel SSH
|
||||
|
||||
Para conectar-se a um dispositivo remoto, é possível conectar um cliente `adb` local a
|
||||
um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo
|
||||
_adb_):
|
||||
|
||||
```bash
|
||||
adb kill-server # encerra o servidor adb local em 5037
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# mantenha isso aberto
|
||||
```
|
||||
|
||||
De outro terminal:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Para evitar ativar o encaminhamento de porta remota, você pode forçar uma conexão
|
||||
de encaminhamento (note o `-L` em vez de `-R`):
|
||||
|
||||
```bash
|
||||
adb kill-server # encerra o servidor adb local em 5037
|
||||
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# mantenha isso aberto
|
||||
```
|
||||
|
||||
De outro terminal:
|
||||
|
||||
```bash
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
Igual a conexões sem fio, pode ser útil reduzir a qualidade:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### Configuração de janela
|
||||
|
||||
#### Título
|
||||
|
||||
Por padrão, o título da janela é o modelo do dispositivo. Isso pode ser mudado:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'Meu dispositivo'
|
||||
```
|
||||
|
||||
#### Posição e tamanho
|
||||
|
||||
A posição e tamanho iniciais da janela podem ser especificados:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### Sem bordas
|
||||
|
||||
Para desativar decorações de janela:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### Sempre no topo
|
||||
|
||||
Para manter a janela do scrcpy sempre no topo:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### Tela cheia
|
||||
|
||||
A aplicação pode ser iniciada diretamente em tela cheia:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # versão curta
|
||||
```
|
||||
|
||||
Tela cheia pode ser alternada dinamicamente com <kbd>MOD</kbd>+<kbd>f</kbd>.
|
||||
|
||||
#### Rotação
|
||||
|
||||
A janela pode ser rotacionada:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
Valores possíveis são:
|
||||
- `0`: sem rotação
|
||||
- `1`: 90 graus sentido anti-horário
|
||||
- `2`: 180 graus
|
||||
- `3`: 90 graus sentido horário
|
||||
|
||||
A rotação também pode ser mudada dinamicamente com <kbd>MOD</kbd>+<kbd>←</kbd>
|
||||
_(esquerda)_ e <kbd>MOD</kbd>+<kbd>→</kbd> _(direita)_.
|
||||
|
||||
Note que _scrcpy_ controla 3 rotações diferentes:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> requisita ao dispositivo para mudar entre retrato
|
||||
e paisagem (a aplicação em execução pode se recusar, se ela não suporta a
|
||||
orientação requisitada).
|
||||
- [`--lock-video-orientation`](#travar-orientação-do-vídeo) muda a orientação de
|
||||
espelhamento (a orientação do vídeo enviado pelo dispositivo para o
|
||||
computador). Isso afeta a gravação.
|
||||
- `--rotation` (ou <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)
|
||||
rotaciona apenas o conteúdo da janela. Isso afeta apenas a exibição, não a
|
||||
gravação.
|
||||
|
||||
|
||||
### Outras opções de espelhamento
|
||||
|
||||
#### Apenas leitura
|
||||
|
||||
Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada,
|
||||
eventos de mouse, arrastar e soltar arquivos):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### Display
|
||||
|
||||
Se vários displays estão disponíveis, é possível selecionar o display para
|
||||
espelhar:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
A lista de IDs dos displays pode ser obtida por:
|
||||
|
||||
```
|
||||
adb shell dumpsys display # busca "mDisplayId=" na saída
|
||||
```
|
||||
|
||||
O display secundário pode apenas ser controlado se o dispositivo roda pelo menos Android
|
||||
10 (caso contrário é espelhado como apenas leitura).
|
||||
|
||||
|
||||
#### Permanecer ativo
|
||||
|
||||
Para evitar que o dispositivo seja suspenso após um delay quando o dispositivo é conectado:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
O estado inicial é restaurado quando o scrcpy é fechado.
|
||||
|
||||
|
||||
#### Desligar tela
|
||||
|
||||
É possível desligar a tela do dispositivo durante o início do espelhamento com uma
|
||||
opção de linha de comando:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
Ou apertando <kbd>MOD</kbd>+<kbd>o</kbd> a qualquer momento.
|
||||
|
||||
Para ligar novamente, pressione <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
|
||||
No Android, o botão de `POWER` sempre liga a tela. Por conveniência, se
|
||||
`POWER` é enviado via scrcpy (via clique-direito ou <kbd>MOD</kbd>+<kbd>p</kbd>), ele
|
||||
forçará a desligar a tela após um delay pequeno (numa base de melhor esforço).
|
||||
O botão `POWER` físico ainda causará a tela ser ligada.
|
||||
|
||||
Também pode ser útil evitar que o dispositivo seja suspenso:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
|
||||
#### Mostrar toques
|
||||
|
||||
Para apresentações, pode ser útil mostrar toques físicos (no dispositivo
|
||||
físico).
|
||||
|
||||
Android fornece esta funcionalidade nas _Opções do desenvolvedor_.
|
||||
|
||||
_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e restaurar o
|
||||
valor inicial no encerramento:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo).
|
||||
|
||||
|
||||
#### Desativar descanso de tela
|
||||
|
||||
Por padrão, scrcpy não evita que o descanso de tela rode no computador.
|
||||
|
||||
Para desativá-lo:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### Controle de entrada
|
||||
|
||||
#### Rotacionar a tela do dispositivo
|
||||
|
||||
Pressione <kbd>MOD</kbd>+<kbd>r</kbd> para mudar entre os modos retrato e
|
||||
paisagem.
|
||||
|
||||
Note que só será rotacionado se a aplicação em primeiro plano suportar a
|
||||
orientação requisitada.
|
||||
|
||||
#### Copiar-colar
|
||||
|
||||
Sempre que a área de transferência do Android muda, é automaticamente sincronizada com a
|
||||
área de transferência do computador.
|
||||
|
||||
Qualquer atalho com <kbd>Ctrl</kbd> é encaminhado para o dispositivo. Em particular:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> tipicamente copia
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> tipicamente recorta
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> tipicamente cola (após a sincronização de área de transferência
|
||||
computador-para-dispositivo)
|
||||
|
||||
Isso tipicamente funciona como esperado.
|
||||
|
||||
O comportamento de fato depende da aplicação ativa, no entanto. Por exemplo,
|
||||
_Termux_ envia SIGINT com <kbd>Ctrl</kbd>+<kbd>c</kbd>, e _K-9 Mail_
|
||||
compõe uma nova mensagem.
|
||||
|
||||
Para copiar, recortar e colar em tais casos (mas apenas suportado no Android >= 7):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> injeta `COPY`
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> injeta `CUT`
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> injeta `PASTE` (após a sincronização de área de transferência
|
||||
computador-para-dispositivo)
|
||||
|
||||
Em adição, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> permite injetar o
|
||||
texto da área de transferência do computador como uma sequência de eventos de tecla. Isso é útil quando o
|
||||
componente não aceita colar texto (por exemplo no _Termux_), mas pode
|
||||
quebrar conteúdo não-ASCII.
|
||||
|
||||
**ADVERTÊNCIA:** Colar a área de transferência do computador para o dispositivo (tanto via
|
||||
<kbd>Ctrl</kbd>+<kbd>v</kbd> quanto <kbd>MOD</kbd>+<kbd>v</kbd>) copia o conteúdo
|
||||
para a área de transferência do dispositivo. Como consequência, qualquer aplicação Android pode ler
|
||||
o seu conteúdo. Você deve evitar colar conteúdo sensível (como senhas) dessa
|
||||
forma.
|
||||
|
||||
Alguns dispositivos não se comportam como esperado quando a área de transferência é definida
|
||||
programaticamente. Uma opção `--legacy-paste` é fornecida para mudar o comportamento
|
||||
de <kbd>Ctrl</kbd>+<kbd>v</kbd> e <kbd>MOD</kbd>+<kbd>v</kbd> para que eles
|
||||
também injetem o texto da área de transferência do computador como uma sequência de eventos de tecla (da mesma
|
||||
forma que <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
||||
|
||||
#### Pinçar para dar zoom
|
||||
|
||||
Para simular "pinçar para dar zoom": <kbd>Ctrl</kbd>+_clicar-e-mover_.
|
||||
|
||||
Mais precisamente, segure <kbd>Ctrl</kbd> enquanto pressiona o botão de clique-esquerdo. Até que
|
||||
o botão de clique-esquerdo seja liberado, todos os movimentos do mouse ampliar e rotacionam o
|
||||
conteúdo (se suportado pelo app) relativo ao centro da tela.
|
||||
|
||||
Concretamente, scrcpy gera eventos adicionais de toque de um "dedo virtual" em
|
||||
uma posição invertida em relação ao centro da tela.
|
||||
|
||||
|
||||
#### Preferência de injeção de texto
|
||||
|
||||
Existem dois tipos de [eventos][textevents] gerados ao digitar um texto:
|
||||
- _eventos de tecla_, sinalizando que a tecla foi pressionada ou solta;
|
||||
- _eventos de texto_, sinalizando que o texto foi inserido.
|
||||
|
||||
Por padrão, letras são injetadas usando eventos de tecla, assim o teclado comporta-se
|
||||
como esperado em jogos (normalmente para teclas WASD).
|
||||
|
||||
Mas isso pode [causar problemas][prefertext]. Se você encontrar tal problema, você
|
||||
pode evitá-lo com:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(mas isso vai quebrar o comportamento do teclado em jogos)
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### Repetir tecla
|
||||
|
||||
Por padrão, segurar uma tecla gera eventos de tecla repetidos. Isso pode causar
|
||||
problemas de performance em alguns jogos, onde esses eventos são inúteis de qualquer forma.
|
||||
|
||||
Para evitar o encaminhamento eventos de tecla repetidos:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
|
||||
#### Clique-direito e clique-do-meio
|
||||
|
||||
Por padrão, clique-direito dispara BACK (ou POWER) e clique-do-meio dispara
|
||||
HOME. Para desabilitar esses atalhos e encaminhar os cliques para o dispositivo:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### Soltar arquivo
|
||||
|
||||
#### Instalar APK
|
||||
|
||||
Para instalar um APK, arraste e solte o arquivo APK (com extensão `.apk`) na janela
|
||||
_scrcpy_.
|
||||
|
||||
Não existe feedback visual, um log é imprimido no console.
|
||||
|
||||
|
||||
#### Enviar arquivo para dispositivo
|
||||
|
||||
Para enviar um arquivo para `/sdcard/Download/` no dispositivo, arraste e solte um arquivo (não-APK) para a
|
||||
janela do _scrcpy_.
|
||||
|
||||
Não existe feedback visual, um log é imprimido no console.
|
||||
|
||||
O diretório alvo pode ser mudado ao iniciar:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
|
||||
### Encaminhamento de áudio
|
||||
|
||||
Áudio não é encaminhado pelo _scrcpy_. Use [sndcpy].
|
||||
|
||||
Também veja [issue #14].
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## Atalhos
|
||||
|
||||
Na lista a seguir, <kbd>MOD</kbd> é o modificador de atalho. Por padrão, é
|
||||
<kbd>Alt</kbd> (esquerdo) ou <kbd>Super</kbd> (esquerdo).
|
||||
|
||||
Ele pode ser mudado usando `--shortcut-mod`. Possíveis teclas são `lctrl`, `rctrl`,
|
||||
`lalt`, `ralt`, `lsuper` e `rsuper`. Por exemplo:
|
||||
|
||||
```bash
|
||||
# usar RCtrl para atalhos
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# usar tanto LCtrl+LAlt quanto LSuper para atalhos
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> é tipicamente a tecla <kbd>Windows</kbd> ou <kbd>Cmd</kbd>._
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Ação | Atalho
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| Mudar modo de tela cheia | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| Rotacionar display para esquerda | <kbd>MOD</kbd>+<kbd>←</kbd> _(esquerda)_
|
||||
| Rotacionar display para direita | <kbd>MOD</kbd>+<kbd>→</kbd> _(direita)_
|
||||
| Redimensionar janela para 1:1 (pixel-perfeito) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Redimensionar janela para remover bordas pretas | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Clique-duplo-esquerdo¹_
|
||||
| Clicar em `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Clique-do-meio_
|
||||
| Clicar em `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Clique-direito²_
|
||||
| Clicar em `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _Clique-do-4.°³_
|
||||
| Clicar em `MENU` (desbloquear tela) | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| Clicar em `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(cima)_
|
||||
| Clicar em `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(baixo)_
|
||||
| Clicar em `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| Ligar | _Clique-direito²_
|
||||
| Desligar tela do dispositivo (continuar espelhando) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| Ligar tela do dispositivo | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| Rotacionar tela do dispositivo | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| Expandir painel de notificação | <kbd>MOD</kbd>+<kbd>n</kbd> \| _Clique-do-5.°³_
|
||||
| Expandir painel de configurção | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Clique-duplo-do-5.°³_
|
||||
| Colapsar paineis | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Copiar para área de transferência⁴ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Recortar para área de transferência⁴ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Sincronizar áreas de transferência e colar⁴ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Injetar texto da área de transferência do computador | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Ativar/desativar contador de FPS (em stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pinçar para dar zoom | <kbd>Ctrl</kbd>+_Clicar-e-mover_
|
||||
|
||||
_¹Clique-duplo-esquerdo na borda preta para remove-la._
|
||||
_²Clique-direito liga a tela caso esteja desligada, pressione BACK caso contrário._
|
||||
_³4.° and 5.° botões do mouse, caso o mouse possua._
|
||||
_⁴Apenas em Android >= 7._
|
||||
|
||||
Atalhos com teclas reptidas são executados soltando e precionando a tecla
|
||||
uma segunda vez. Por exemplo, para executar "Expandir painel de Configurção":
|
||||
|
||||
1. Mantenha pressionado <kbd>MOD</kbd>.
|
||||
2. Depois click duas vezes <kbd>n</kbd>.
|
||||
3. Finalmente, solte <kbd>MOD</kbd>.
|
||||
|
||||
Todos os atalhos <kbd>Ctrl</kbd>+_tecla_ são encaminhados para o dispositivo, para que eles sejam
|
||||
tratados pela aplicação ativa.
|
||||
|
||||
|
||||
## Caminhos personalizados
|
||||
|
||||
Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente
|
||||
`ADB`:
|
||||
|
||||
```bash
|
||||
ADB=/caminho/para/adb scrcpy
|
||||
```
|
||||
|
||||
Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em
|
||||
`SCRCPY_SERVER_PATH`.
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
|
||||
## Por quê _scrcpy_?
|
||||
|
||||
Um colega me desafiou a encontrar um nome tão impronunciável quanto [gnirehtet].
|
||||
|
||||
[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een.
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## Como compilar?
|
||||
|
||||
Veja [BUILD].
|
||||
|
||||
|
||||
## Problemas comuns
|
||||
|
||||
Veja o [FAQ](FAQ.md).
|
||||
|
||||
|
||||
## Desenvolvedores
|
||||
|
||||
Leia a [página dos desenvolvedores][developers page].
|
||||
|
||||
[developers page]: DEVELOP.md
|
||||
|
||||
|
||||
## Licença
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 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/
|
||||
974
README.sp.md
Normal file
974
README.sp.md
Normal file
@@ -0,0 +1,974 @@
|
||||
Solo se garantiza que el archivo [README](README.md) original esté actualizado.
|
||||
|
||||
# scrcpy (v1.21)
|
||||
|
||||
<img src="data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
Esta aplicación proporciona control e imagen de un dispositivo Android conectado
|
||||
por USB (o [por TCP/IP](#conexión)). No requiere acceso _root_.
|
||||
Compatible con _GNU/Linux_, _Windows_ y _macOS_.
|
||||
|
||||

|
||||
|
||||
Se enfoca en:
|
||||
- **ser ligera**: aplicación nativa, solo muestra la imagen del dispositivo
|
||||
- **rendimiento**: 30~120fps, dependiendo del dispositivo
|
||||
- **calidad**: 1920×1080 o superior
|
||||
- **baja latencia**: [35~70ms][lowlatency]
|
||||
- **inicio rápido**: ~1 segundo para mostrar la primera imagen
|
||||
- **no intrusivo**: no deja nada instalado en el dispositivo
|
||||
- **beneficios**: sin cuentas, sin anuncios, no requiere acceso a internet
|
||||
- **libertad**: software gratis y de código abierto
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
Con la aplicación puede:
|
||||
- [grabar la pantalla](#capturas-y-grabaciones)
|
||||
- duplicar la imagen con [la pantalla apagada](#apagar-la-pantalla)
|
||||
- [copiar y pegar](#copiar-y-pegar) en ambos sentidos
|
||||
- [configurar la calidad](#configuración-de-captura)
|
||||
- usar la pantalla del dispositivo [como webcam (V4L2)](#v4l2loopback) (solo en Linux)
|
||||
- [emular un teclado físico (HID)](#emular-teclado-físico-hid)
|
||||
(solo en Linux)
|
||||
- y mucho más…
|
||||
|
||||
## Requisitos
|
||||
|
||||
El dispositivo Android requiere como mínimo API 21 (Android 5.0).
|
||||
|
||||
Asegurate de [habilitar el adb debugging][enable-adb] en tu(s) dispositivo(s).
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
En algunos dispositivos, también necesitas habilitar [una opción adicional][control] para controlarlo con el teclado y ratón.
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## Consigue la app
|
||||
|
||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
||||
|
||||
### Resumen
|
||||
|
||||
- Linux: `apt install scrcpy`
|
||||
- Windows: [download](README.md#windows)
|
||||
- macOS: `brew install scrcpy`
|
||||
|
||||
Construir desde la fuente: [BUILD] ([proceso simplificado][BUILD_simple])
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
[BUILD_simple]: BUILD.md#simple
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
En Debian y Ubuntu:
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
Hay un paquete [Snap]: [`scrcpy`][snap-link].
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
Para Fedora, hay un paquete [COPR]: [`scrcpy`][copr-link].
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
Para Arch Linux, hay un paquete [AUR]: [`scrcpy`][aur-link].
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
Para Gentoo, hay un paquete [Ebuild]: [`scrcpy/`][ebuild-link].
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
También puedes [construir la aplicación manualmente][BUILD] ([proceso simplificado][BUILD_simple]).
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
Para Windows, por simplicidad, hay un pre-compilado con todas las dependencias
|
||||
(incluyendo `adb`):
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
También está disponible en [Chocolatey]:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # si aún no está instalado
|
||||
```
|
||||
|
||||
Y en [Scoop]:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # si aún no está instalado
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
También puedes [construir la aplicación manualmente][BUILD].
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
La aplicación está disponible en [Homebrew]. Solo instalala:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
Necesitarás `adb`, accesible desde `PATH`. Si aún no lo tienes:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
También está disponible en [MacPorts], que configura el adb automáticamente:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
|
||||
También puedes [construir la aplicación manualmente][BUILD].
|
||||
|
||||
|
||||
## Ejecutar
|
||||
|
||||
Enchufa el dispositivo Android, y ejecuta:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Acepta argumentos desde la línea de comandos, listados en:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## Características
|
||||
|
||||
### Configuración de captura
|
||||
|
||||
#### Reducir la definición
|
||||
|
||||
A veces es útil reducir la definición de la imagen del dispositivo Android para aumentar el desempeño.
|
||||
|
||||
Para limitar el ancho y la altura a un valor específico (ej. 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # versión breve
|
||||
```
|
||||
|
||||
La otra dimensión es calculada para conservar el aspect ratio del dispositivo.
|
||||
De esta forma, un dispositivo en 1920×1080 será transmitido a 1024×576.
|
||||
|
||||
|
||||
#### Cambiar el bit-rate
|
||||
|
||||
El bit-rate por defecto es 8 Mbps. Para cambiar el bit-rate del video (ej. a 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # versión breve
|
||||
```
|
||||
|
||||
#### Limitar los fps
|
||||
|
||||
El fps puede ser limitado:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
Es oficialmente soportado desde Android 10, pero puede funcionar en versiones anteriores.
|
||||
|
||||
#### Recortar
|
||||
|
||||
La imagen del dispositivo puede ser recortada para transmitir solo una parte de la pantalla.
|
||||
|
||||
Por ejemplo, puede ser útil para transmitir la imagen de un solo ojo del Oculus Go:
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # 1224x1440 con coordenadas de origen en (0,0)
|
||||
```
|
||||
|
||||
Si `--max-size` también está especificado, el cambio de tamaño es aplicado después de cortar.
|
||||
|
||||
|
||||
#### Fijar la rotación del video
|
||||
|
||||
|
||||
Para fijar la rotación de la transmisión:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # orientación inicial
|
||||
scrcpy --lock-video-orientation=0 # orientación normal
|
||||
scrcpy --lock-video-orientation=1 # 90° contrarreloj
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 90° sentido de las agujas del reloj
|
||||
```
|
||||
|
||||
Esto afecta la rotación de la grabación.
|
||||
|
||||
La [ventana también puede ser rotada](#rotación) independientemente.
|
||||
|
||||
|
||||
#### Codificador
|
||||
|
||||
Algunos dispositivos pueden tener más de una rotación, y algunos pueden causar problemas o errores. Es posible seleccionar un codificador diferente:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
Para listar los codificadores disponibles, puedes pasar un nombre de codificador inválido, el error te dará los codificadores disponibles:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### Capturas y grabaciones
|
||||
|
||||
|
||||
#### Grabación
|
||||
|
||||
Es posible grabar la pantalla mientras se transmite:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
Para grabar sin transmitir la pantalla:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# interrumpe la grabación con Ctrl+C
|
||||
```
|
||||
|
||||
Los "skipped frames" son grabados, incluso si no se mostrados en tiempo real (por razones de desempeño). Los frames tienen _marcas de tiempo_ en el dispositivo, por lo que el "[packet delay
|
||||
variation]" no impacta el archivo grabado.
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
En Linux se puede mandar el stream del video a un dispositivo loopback v4l2, por
|
||||
lo que se puede abrir el dispositivo Android como una webcam con cualquier
|
||||
programa compatible con v4l2.
|
||||
|
||||
Se debe instalar el modulo `v4l2loopback`:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
Para crear un dispositivo v4l2:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
Esto va a crear un nuevo dispositivo de video en `/dev/videoN`, donde `N` es un número
|
||||
(hay más [opciones](https://github.com/umlaeute/v4l2loopback#options) disponibles
|
||||
para crear múltiples dispositivos o usar un ID en específico).
|
||||
|
||||
Para ver los dispositivos disponibles:
|
||||
|
||||
```bash
|
||||
# requiere el paquete v4l-utils
|
||||
v4l2-ctl --list-devices
|
||||
# simple pero generalmente suficiente
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
Para iniciar scrcpy usando una fuente v4l2:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # deshabilita la transmisión de imagen
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # más corto
|
||||
```
|
||||
|
||||
(reemplace `N` con el ID del dispositivo, compruebe con `ls /dev/video*`)
|
||||
|
||||
Una vez habilitado, podés abrir el stream del video con una herramienta compatible con v4l2:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC puede agregar un delay por buffering
|
||||
```
|
||||
|
||||
Por ejemplo, podrías capturar el video usando [OBS].
|
||||
|
||||
[OBS]: https://obsproject.com/
|
||||
|
||||
|
||||
#### Buffering
|
||||
|
||||
Es posible agregar buffering al video. Esto reduce el ruido en la imagen ("jitter")
|
||||
pero aumenta la latencia (vea [#2464]).
|
||||
|
||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
||||
|
||||
La opción de buffering está disponible para la transmisión de imagen:
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # agrega 50 ms de buffering a la imagen
|
||||
```
|
||||
|
||||
y las fuentes V4L2:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-buffer=500 # agrega 500 ms de buffering a la fuente v4l2
|
||||
```
|
||||
|
||||
|
||||
### Conexión
|
||||
|
||||
#### TCP/IP (Inalámbrica)
|
||||
|
||||
_Scrcpy_ usa `adb` para comunicarse con el dispositivo, y `adb` puede [conectarse] vía TCP/IP.
|
||||
El dispositivo debe estar conectado a la misma red que la computadora:
|
||||
|
||||
##### Automático
|
||||
|
||||
La opción `--tcpip` permite configurar la conexión automáticamente. Hay 2 variables.
|
||||
|
||||
Si el dispositivo (accesible en 192.168.1.1 para este ejemplo) ya está escuchando
|
||||
en un puerto (generalmente 5555) esperando una conexión adb entrante, entonces corré:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip=192.168.1.1 # el puerto default es 5555
|
||||
scrcpy --tcpip=192.168.1.1:5555
|
||||
```
|
||||
|
||||
Si el dispositivo no tiene habilitado el modo adb TCP/IP (o si no sabés la dirección IP),
|
||||
entonces conectá el dispositivo por USB y corré:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip # sin argumentos
|
||||
```
|
||||
|
||||
El programa buscará automáticamente la IP del dispositivo, habilitará el modo TCP/IP, y
|
||||
se conectará al dispositivo antes de comenzar a transmitir la imagen.
|
||||
|
||||
##### Manual
|
||||
|
||||
Como alternativa, se puede habilitar la conexión TCP/IP manualmente usando `adb`:
|
||||
|
||||
1. Conecta el dispositivo al mismo Wi-Fi que tu computadora.
|
||||
2. Obtén la dirección IP del dispositivo, en Ajustes → Acerca del dispositivo → Estado, o ejecutando este comando:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. Habilita adb vía TCP/IP en el dispositivo: `adb tcpip 5555`.
|
||||
4. Desenchufa el dispositivo.
|
||||
5. Conéctate a tu dispositivo: `adb connect IP_DEL_DISPOSITIVO:5555` _(reemplaza `IP_DEL_DISPOSITIVO`)_.
|
||||
6. Ejecuta `scrcpy` con normalidad.
|
||||
|
||||
Podría resultar útil reducir el bit-rate y la definición:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # versión breve
|
||||
```
|
||||
|
||||
[conectarse]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### Múltiples dispositivos
|
||||
|
||||
Si hay muchos dispositivos listados en `adb devices`, será necesario especificar el _número de serie_:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # versión breve
|
||||
```
|
||||
|
||||
Si el dispositivo está conectado por TCP/IP:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # versión breve
|
||||
```
|
||||
|
||||
Puedes iniciar múltiples instancias de _scrcpy_ para múltiples dispositivos.
|
||||
|
||||
#### Iniciar automáticamente al detectar dispositivo
|
||||
|
||||
Puedes utilizar [AutoAdb]:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### Túneles
|
||||
|
||||
Para conectarse a un dispositivo remoto, es posible conectar un cliente local `adb` a un servidor remoto `adb` (siempre y cuando utilicen la misma versión de protocolos _adb_).
|
||||
|
||||
##### Servidor ADB remoto
|
||||
|
||||
Para conectarse a un servidor ADB remoto, haz que el servidor escuche en todas las interfaces:
|
||||
|
||||
```bash
|
||||
adb kill-server
|
||||
adb -a nodaemon server start
|
||||
# conserva este servidor abierto
|
||||
```
|
||||
|
||||
**Advertencia: todas las comunicaciones entre los clientes y el servidor ADB están desencriptadas.**
|
||||
|
||||
Supondremos que este servidor se puede acceder desde 192.168.1.2. Entonces, desde otra
|
||||
terminal, corré scrcpy:
|
||||
|
||||
```bash
|
||||
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
Por default, scrcpy usa el puerto local que se usó para establecer el tunel
|
||||
`adb forward` (típicamente `27183`, vea `--port`). También es posible forzar un
|
||||
puerto diferente (puede resultar útil en situaciones más complejas, donde haya
|
||||
múltiples redirecciones):
|
||||
|
||||
```
|
||||
scrcpy --tunnel-port=1234
|
||||
```
|
||||
|
||||
|
||||
##### Túnel SSH
|
||||
|
||||
Para comunicarse con un servidor ADB remoto de forma segura, es preferible usar un túnel SSH.
|
||||
|
||||
Primero, asegurate que el servidor ADB está corriendo en la computadora remota:
|
||||
|
||||
```bash
|
||||
adb start-server
|
||||
```
|
||||
|
||||
Después, establecé el túnel SSH:
|
||||
|
||||
```bash
|
||||
# local 5038 --> remoto 5037
|
||||
# local 27183 <-- remoto 27183
|
||||
ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# conserva este servidor abierto
|
||||
```
|
||||
|
||||
Desde otra terminal, corré scrcpy:
|
||||
|
||||
```bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Para evitar habilitar "remote port forwarding", puedes forzar una "forward connection" (nótese el argumento `-L` en vez de `-R`):
|
||||
|
||||
```bash
|
||||
# local 5038 --> remoto 5037
|
||||
# local 27183 --> remoto 27183
|
||||
ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# conserva este servidor abierto
|
||||
```
|
||||
|
||||
Desde otra terminal, corré scrcpy:
|
||||
|
||||
```bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
Al igual que las conexiones inalámbricas, puede resultar útil reducir la calidad:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### Configuración de la ventana
|
||||
|
||||
#### Título
|
||||
|
||||
Por defecto, el título de la ventana es el modelo del dispositivo. Puede ser modificado:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'My device'
|
||||
```
|
||||
|
||||
#### Posición y tamaño
|
||||
|
||||
La posición y tamaño inicial de la ventana puede ser especificado:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### Sin bordes
|
||||
|
||||
Para deshabilitar el diseño de la ventana:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### Siempre adelante
|
||||
|
||||
Para mantener la ventana de scrcpy siempre adelante:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### Pantalla completa
|
||||
|
||||
La aplicación puede ser iniciada en pantalla completa:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # versión breve
|
||||
```
|
||||
|
||||
Puede entrar y salir de la pantalla completa con la combinación <kbd>MOD</kbd>+<kbd>f</kbd>.
|
||||
|
||||
#### Rotación
|
||||
|
||||
Se puede rotar la ventana:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
Los posibles valores son:
|
||||
- `0`: sin rotación
|
||||
- `1`: 90 grados contrarreloj
|
||||
- `2`: 180 grados
|
||||
- `3`: 90 grados en sentido de las agujas del reloj
|
||||
|
||||
La rotación también puede ser modificada con la combinación de teclas <kbd>MOD</kbd>+<kbd>←</kbd> _(izquierda)_ y <kbd>MOD</kbd>+<kbd>→</kbd> _(derecha)_.
|
||||
|
||||
Nótese que _scrcpy_ maneja 3 diferentes rotaciones:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> solicita al dispositivo cambiar entre vertical y horizontal (la aplicación en uso puede rechazarlo si no soporta la orientación solicitada).
|
||||
- [`--lock-video-orientation`](#fijar-la-rotación-del-video) cambia la rotación de la transmisión (la orientación del video enviado a la PC). Esto afecta a la grabación.
|
||||
- `--rotation` (o <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>) rota solo el contenido de la imagen. Esto solo afecta a la imagen mostrada, no a la grabación.
|
||||
|
||||
|
||||
### Otras opciones
|
||||
|
||||
#### Solo lectura ("Read-only")
|
||||
|
||||
Para deshabilitar los controles (todo lo que interactúe con el dispositivo: eventos del teclado, eventos del mouse, arrastrar y soltar archivos):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n # versión breve
|
||||
```
|
||||
|
||||
#### Pantalla
|
||||
|
||||
Si múltiples pantallas están disponibles, es posible elegir cual transmitir:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
Los ids de las pantallas se pueden obtener con el siguiente comando:
|
||||
|
||||
```bash
|
||||
adb shell dumpsys display # busque "mDisplayId=" en la respuesta
|
||||
```
|
||||
|
||||
La segunda pantalla solo puede ser manejada si el dispositivo cuenta con Android 10 (en caso contrario será transmitida en el modo solo lectura).
|
||||
|
||||
|
||||
#### Permanecer activo
|
||||
|
||||
Para evitar que el dispositivo descanse después de un tiempo mientras está conectado:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w # versión breve
|
||||
```
|
||||
|
||||
La configuración original se restaura al cerrar scrcpy.
|
||||
|
||||
|
||||
#### Apagar la pantalla
|
||||
|
||||
Es posible apagar la pantalla mientras se transmite al iniciar con el siguiente comando:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S # versión breve
|
||||
```
|
||||
|
||||
O presionando <kbd>MOD</kbd>+<kbd>o</kbd> en cualquier momento.
|
||||
|
||||
Para volver a prenderla, presione <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
|
||||
En Android, el botón de `POWER` siempre prende la pantalla. Por conveniencia, si `POWER` es enviado vía scrcpy (con click-derecho o <kbd>MOD</kbd>+<kbd>p</kbd>), esto forzará a apagar la pantalla con un poco de atraso (en la mejor de las situaciones). El botón físico `POWER` seguirá prendiendo la pantalla.
|
||||
|
||||
También puede resultar útil para evitar que el dispositivo entre en inactividad:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw # versión breve
|
||||
```
|
||||
|
||||
|
||||
#### Apagar al cerrar la aplicación
|
||||
|
||||
Para apagar la pantalla del dispositivo al cerrar scrcpy:
|
||||
|
||||
```bash
|
||||
scrcpy --power-off-on-close
|
||||
```
|
||||
|
||||
#### Mostrar clicks
|
||||
|
||||
Para presentaciones, puede resultar útil mostrar los clicks físicos (en el dispositivo físicamente).
|
||||
|
||||
Android provee esta opción en _Opciones para desarrolladores_.
|
||||
|
||||
_Scrcpy_ provee una opción para habilitar esta función al iniciar la aplicación y restaurar el valor original al salir:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t # versión breve
|
||||
```
|
||||
|
||||
Nótese que solo muestra los clicks _físicos_ (con el dedo en el dispositivo).
|
||||
|
||||
|
||||
#### Desactivar protector de pantalla
|
||||
|
||||
Por defecto, scrcpy no evita que el protector de pantalla se active en la computadora.
|
||||
|
||||
Para deshabilitarlo:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### Control
|
||||
|
||||
#### Rotar pantalla del dispositivo
|
||||
|
||||
Presione <kbd>MOD</kbd>+<kbd>r</kbd> para cambiar entre posición vertical y horizontal.
|
||||
|
||||
Nótese que solo rotará si la aplicación activa soporta la orientación solicitada.
|
||||
|
||||
#### Copiar y pegar
|
||||
|
||||
Cuando que el portapapeles de Android cambia, automáticamente se sincroniza al portapapeles de la computadora.
|
||||
|
||||
Cualquier shortcut con <kbd>Ctrl</kbd> es enviado al dispositivo. En particular:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> normalmente copia
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> normalmente corta
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> normalmente pega (después de la sincronización de portapapeles entre la computadora y el dispositivo)
|
||||
|
||||
Esto normalmente funciona como es esperado.
|
||||
|
||||
Sin embargo, este comportamiento depende de la aplicación en uso. Por ejemplo, _Termux_ envía SIGINT con <kbd>Ctrl</kbd>+<kbd>c</kbd>, y _K-9 Mail_ crea un nuevo mensaje.
|
||||
|
||||
Para copiar, cortar y pegar, en tales casos (solo soportado en Android >= 7):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> inyecta `COPY`
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> inyecta `CUT`
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> inyecta `PASTE` (después de la sincronización de portapapeles entre la computadora y el dispositivo)
|
||||
|
||||
Además, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> permite inyectar el texto en el portapapeles de la computadora como una secuencia de teclas. Esto es útil cuando el componente no acepta pegado de texto (por ejemplo en _Termux_), pero puede romper caracteres no pertenecientes a ASCII.
|
||||
|
||||
**AVISO:** Pegar de la computadora al dispositivo (tanto con <kbd>Ctrl</kbd>+<kbd>v</kbd> o <kbd>MOD</kbd>+<kbd>v</kbd>) copia el contenido al portapapeles del dispositivo. Como consecuencia, cualquier aplicación de Android puede leer su contenido. Debería evitar pegar contenido sensible (como contraseñas) de esta forma.
|
||||
|
||||
Algunos dispositivos no se comportan como es esperado al establecer el portapapeles programáticamente. La opción `--legacy-paste` está disponible para cambiar el comportamiento de <kbd>Ctrl</kbd>+<kbd>v</kbd> y <kbd>MOD</kbd>+<kbd>v</kbd> para que también inyecten el texto del portapapeles de la computadora como una secuencia de teclas (de la misma forma que <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
||||
|
||||
Para deshabilitar la auto-sincronización del portapapeles, use `--no-clipboard-autosync`.
|
||||
|
||||
#### Pellizcar para zoom
|
||||
|
||||
Para simular "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-y-mover_.
|
||||
|
||||
Más precisamente, mantén <kbd>Ctrl</kbd> mientras presionas botón izquierdo. Hasta que no se suelte el botón, todos los movimientos del mouse cambiarán el tamaño y rotación del contenido (si es soportado por la app en uso) respecto al centro de la pantalla.
|
||||
|
||||
Concretamente, scrcpy genera clicks adicionales con un "dedo virtual" en la posición invertida respecto al centro de la pantalla.
|
||||
|
||||
#### Emular teclado físico (HID)
|
||||
|
||||
Por default, scrcpy usa el sistema de Android para la injección de teclas o texto:
|
||||
funciona en todas partes, pero está limitado a ASCII.
|
||||
|
||||
En Linux, scrcpy puede emular un teclado USB físico en Android para proveer
|
||||
una mejor experiencia al enviar _inputs_ (usando [USB HID vía AOAv2][hid-aoav2]):
|
||||
deshabilita el teclado virtual y funciona para todos los caracteres y IME.
|
||||
|
||||
[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
|
||||
|
||||
Sin embargo, solo funciona si el dispositivo está conectado por USB, y por ahora
|
||||
solo funciona en Linux.
|
||||
|
||||
Para habilitar este modo:
|
||||
|
||||
```bash
|
||||
scrcpy --hid-keyboard
|
||||
scrcpy -K # más corto
|
||||
```
|
||||
|
||||
Si por alguna razón falla (por ejemplo si el dispositivo no está conectado vía
|
||||
USB), automáticamente vuelve al modo default (un mensaje se escribirá en la consola).
|
||||
Se puede usar los mismos argumentos en la línea de comandos tanto si se conecta con
|
||||
USB o vía TCP/IP.
|
||||
|
||||
En este modo, los _raw key events_ (_scancodes_) se envían al dispositivo, independientemente
|
||||
del mapeo del teclado en el host. Por eso, si el diseño de tu teclado no concuerda, debe ser
|
||||
configurado en el dispositivo Android, en Ajustes → Sistema → Idioma y Entrada de Texto
|
||||
→ [Teclado Físico].
|
||||
|
||||
Se puede iniciar automáticamente en esta página de ajustes:
|
||||
|
||||
```bash
|
||||
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
|
||||
```
|
||||
|
||||
Sin embargo, la opción solo está disponible cuando el teclado HID está activo
|
||||
(o cuando se conecta un teclado físico).
|
||||
|
||||
[Teclado Físico]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
|
||||
|
||||
|
||||
#### Preferencias de inyección de texto
|
||||
|
||||
Existen dos tipos de [eventos][textevents] generados al escribir texto:
|
||||
- _key events_, marcando si la tecla es presionada o soltada;
|
||||
- _text events_, marcando si un texto fue introducido.
|
||||
|
||||
Por defecto, las letras son inyectadas usando _key events_, para que el teclado funcione como es esperado en juegos (típicamente las teclas WASD).
|
||||
|
||||
Pero esto puede [causar problemas][prefertext]. Si encuentras tales problemas, los puedes evitar con:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(Pero esto romperá el comportamiento del teclado en los juegos)
|
||||
|
||||
Por el contrario, se puede forzar scrcpy para siempre injectar _raw key events_:
|
||||
|
||||
```bash
|
||||
scrcpy --raw-key-events
|
||||
```
|
||||
|
||||
Estas opciones no tienen efecto en los teclados HID (todos los _key events_ son enviados como
|
||||
_scancodes_ en este modo).
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### Repetir tecla
|
||||
|
||||
Por defecto, mantener una tecla presionada genera múltiples _key events_. Esto puede
|
||||
causar problemas de desempeño en algunos juegos, donde estos eventos no tienen sentido de todos modos.
|
||||
|
||||
Para evitar enviar _key events_ repetidos:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
Estas opciones no tienen efecto en los teclados HID (Android maneja directamente
|
||||
las repeticiones de teclas en este modo)
|
||||
|
||||
|
||||
#### Botón derecho y botón del medio
|
||||
|
||||
Por defecto, botón derecho ejecuta RETROCEDER (o ENCENDIDO) y botón del medio INICIO. Para inhabilitar estos atajos y enviar los clicks al dispositivo:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### Arrastrar y soltar archivos
|
||||
|
||||
#### Instalar APKs
|
||||
|
||||
Para instalar un APK, arrastre y suelte el archivo APK (terminado en `.apk`) a la ventana de _scrcpy_.
|
||||
|
||||
No hay respuesta visual, un mensaje se escribirá en la consola.
|
||||
|
||||
|
||||
#### Enviar archivos al dispositivo
|
||||
|
||||
Para enviar un archivo a `/sdcard/Download/` en el dispositivo, arrastre y suelte
|
||||
un archivo (no APK) a la ventana de _scrcpy_.
|
||||
|
||||
No hay ninguna respuesta visual, un mensaje se escribirá en la consola.
|
||||
|
||||
El directorio de destino puede ser modificado al iniciar:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
|
||||
### Envío de Audio
|
||||
|
||||
_Scrcpy_ no envía el audio. Use [sndcpy].
|
||||
|
||||
También lea [issue #14].
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## Atajos
|
||||
|
||||
En la siguiente lista, <kbd>MOD</kbd> es el atajo modificador. Por defecto es <kbd>Alt</kbd> (izquierdo) o <kbd>Super</kbd> (izquierdo).
|
||||
|
||||
Se puede modificar usando `--shortcut-mod`. Las posibles teclas son `lctrl` (izquierdo), `rctrl` (derecho), `lalt` (izquierdo), `ralt` (derecho), `lsuper` (izquierdo) y `rsuper` (derecho). Por ejemplo:
|
||||
|
||||
```bash
|
||||
# use RCtrl para los atajos
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# use tanto LCtrl+LAlt o LSuper para los atajos
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> es generalmente la tecla <kbd>Windows</kbd> o <kbd>Cmd</kbd>._
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Acción | Atajo
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| Alterne entre pantalla compelta | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| Rotar pantalla hacia la izquierda | <kbd>MOD</kbd>+<kbd>←</kbd> _(izquierda)_
|
||||
| Rotar pantalla hacia la derecha | <kbd>MOD</kbd>+<kbd>→</kbd> _(derecha)_
|
||||
| Ajustar ventana a 1:1 ("pixel-perfect") | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Ajustar ventana para quitar los bordes negros| <kbd>MOD</kbd>+<kbd>w</kbd> \| _Doble click izquierdo¹_
|
||||
| Click en `INICIO` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Click medio_
|
||||
| Click en `RETROCEDER` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Click derecho²_
|
||||
| Click en `CAMBIAR APLICACIÓN` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _Cuarto botón³_
|
||||
| Click en `MENÚ` (desbloquear pantalla)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| Click en `SUBIR VOLUMEN` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(arriba)_
|
||||
| Click en `BAJAR VOLUME` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(abajo)_
|
||||
| Click en `ENCENDIDO` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| Encendido | _Botón derecho²_
|
||||
| Apagar pantalla (manteniendo la transmisión) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| Encender pantalla | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| Rotar pantalla del dispositivo | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| Abrir panel de notificaciones | <kbd>MOD</kbd>+<kbd>n</kbd> \| _Quinto botón³_
|
||||
| Abrir panel de configuración | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Doble quinto botón³_
|
||||
| Cerrar paneles | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Copiar al portapapeles⁵ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Cortar al portapapeles⁵ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Synchronizar portapapeles y pegar⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Inyectar texto del portapapeles de la PC | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Habilitar/Deshabilitar contador de FPS (en stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pellizcar para zoom | <kbd>Ctrl</kbd>+_click-y-mover_
|
||||
| Arrastrar y soltar un archivo (APK) | Instalar APK desde la computadora
|
||||
| Arrastrar y soltar un archivo (no APK) | [Mover archivo al dispositivo](#enviar-archivos-al-dispositivo)
|
||||
|
||||
_¹Doble click en los bordes negros para eliminarlos._
|
||||
_²Botón derecho enciende la pantalla si estaba apagada, sino ejecuta RETROCEDER._
|
||||
_³Cuarto y quinto botón del mouse, si tu mouse los tiene._
|
||||
_⁴Para las apps react-native en desarrollo, `MENU` activa el menú de desarrollo._
|
||||
_⁵Solo en Android >= 7._
|
||||
|
||||
Los shortcuts con teclas repetidas se ejecutan soltando y volviendo a apretar la tecla
|
||||
por segunda vez. Por ejemplo, para ejecutar "Abrir panel de configuración":
|
||||
|
||||
1. Apretá y mantené apretado <kbd>MOD</kbd>.
|
||||
2. Después apretá dos veces la tecla <kbd>n</kbd>.
|
||||
3. Por último, soltá la tecla <kbd>MOD</kbd>.
|
||||
|
||||
Todos los atajos <kbd>Ctrl</kbd>+_tecla_ son enviados al dispositivo para que sean manejados por la aplicación activa.
|
||||
|
||||
|
||||
## Path personalizado
|
||||
|
||||
Para usar un binario de _adb_ en particular, configure el path `ADB` en las variables de entorno:
|
||||
|
||||
```bash
|
||||
ADB=/path/to/adb scrcpy
|
||||
```
|
||||
|
||||
Para sobreescribir el path del archivo `scrcpy-server`, configure el path en `SCRCPY_SERVER_PATH`.
|
||||
|
||||
Para sobreescribir el ícono, configure el path en `SCRCPY_ICON_PATH`.
|
||||
|
||||
|
||||
## ¿Por qué _scrcpy_?
|
||||
|
||||
Un colega me retó a encontrar un nombre tan impronunciable como [gnirehtet].
|
||||
|
||||
[`strcpy`] copia un **str**ing; `scrcpy` copia un **scr**een.
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## ¿Cómo construir (BUILD)?
|
||||
|
||||
Véase [BUILD] (en inglés).
|
||||
|
||||
|
||||
## Problemas generales
|
||||
|
||||
Vea las [preguntas frecuentes (en inglés)](FAQ.md).
|
||||
|
||||
|
||||
## Desarrolladores
|
||||
|
||||
Lea la [hoja de desarrolladores (en inglés)](DEVELOP.md).
|
||||
|
||||
|
||||
## Licencia
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 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.
|
||||
|
||||
## Artículos
|
||||
|
||||
- [Introducing scrcpy][article-intro] (en inglés)
|
||||
- [Scrcpy now works wirelessly][article-tcpip] (en inglés)
|
||||
|
||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
824
README.tr.md
Normal file
824
README.tr.md
Normal file
@@ -0,0 +1,824 @@
|
||||
# scrcpy (v1.18)
|
||||
|
||||
Bu uygulama Android cihazların USB (ya da [TCP/IP][article-tcpip]) üzerinden
|
||||
görüntülenmesini ve kontrol edilmesini sağlar. _root_ erişimine ihtiyaç duymaz.
|
||||
_GNU/Linux_, _Windows_ ve _macOS_ sistemlerinde çalışabilir.
|
||||
|
||||

|
||||
|
||||
Öne çıkan özellikler:
|
||||
|
||||
- **hafiflik** (doğal, sadece cihazın ekranını gösterir)
|
||||
- **performans** (30~60fps)
|
||||
- **kalite** (1920×1080 ya da üzeri)
|
||||
- **düşük gecikme süresi** ([35~70ms][lowlatency])
|
||||
- **düşük başlangıç süresi** (~1 saniye ilk kareyi gösterme süresi)
|
||||
- **müdaheleci olmama** (cihazda kurulu yazılım kalmaz)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
## Gereksinimler
|
||||
|
||||
Android cihaz en düşük API 21 (Android 5.0) olmalıdır.
|
||||
|
||||
[Adb hata ayıklamasının][enable-adb] cihazınızda aktif olduğundan emin olun.
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
Bazı cihazlarda klavye ve fare ile kontrol için [ilave bir seçenek][control] daha
|
||||
etkinleştirmeniz gerekebilir.
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
## Uygulamayı indirin
|
||||
|
||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
||||
|
||||
### Özet
|
||||
|
||||
- Linux: `apt install scrcpy`
|
||||
- Windows: [indir][direct-win64]
|
||||
- macOS: `brew install scrcpy`
|
||||
|
||||
Kaynak kodu derle: [BUILD] ([basitleştirilmiş süreç][build_simple])
|
||||
|
||||
[build]: BUILD.md
|
||||
[build_simple]: BUILD.md#simple
|
||||
|
||||
### Linux
|
||||
|
||||
Debian (şimdilik _testing_ ve _sid_) ve Ubuntu (20.04) için:
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
[Snap] paketi: [`scrcpy`][snap-link].
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
Fedora için, [COPR] paketi: [`scrcpy`][copr-link].
|
||||
|
||||
[copr]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
Arch Linux için, [AUR] paketi: [`scrcpy`][aur-link].
|
||||
|
||||
[aur]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
Gentoo için, [Ebuild] mevcut: [`scrcpy/`][ebuild-link].
|
||||
|
||||
[ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
Ayrıca [uygulamayı el ile de derleyebilirsiniz][build] ([basitleştirilmiş süreç][build_simple]).
|
||||
|
||||
### Windows
|
||||
|
||||
Windows için (`adb` dahil) tüm gereksinimleri ile derlenmiş bir arşiv mevcut:
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
[Chocolatey] ile kurulum:
|
||||
|
||||
[chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
[Scoop] ile kurulum:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
[scoop]: https://scoop.sh
|
||||
|
||||
Ayrıca [uygulamayı el ile de derleyebilirsiniz][build].
|
||||
|
||||
### macOS
|
||||
|
||||
Uygulama [Homebrew] içerisinde mevcut. Sadece kurun:
|
||||
|
||||
[homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
`adb`, `PATH` içerisinden erişilebilir olmalıdır. Eğer değilse:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
[MacPorts] kullanılarak adb ve uygulamanın birlikte kurulumu yapılabilir:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[macports]: https://www.macports.org/
|
||||
|
||||
Ayrıca [uygulamayı el ile de derleyebilirsiniz][build].
|
||||
|
||||
## Çalıştırma
|
||||
|
||||
Android cihazınızı bağlayın ve aşağıdaki komutu çalıştırın:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Komut satırı argümanları aşağıdaki komut ile listelenebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## Özellikler
|
||||
|
||||
### Ekran yakalama ayarları
|
||||
|
||||
#### Boyut azaltma
|
||||
|
||||
Bazen, Android cihaz ekranını daha düşük seviyede göstermek performansı artırabilir.
|
||||
|
||||
Hem genişliği hem de yüksekliği bir değere sabitlemek için (ör. 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # kısa versiyon
|
||||
```
|
||||
|
||||
Diğer boyut en-boy oranı korunacak şekilde hesaplanır.
|
||||
Bu şekilde ekran boyutu 1920x1080 olan bir cihaz 1024x576 olarak görünür.
|
||||
|
||||
#### Bit-oranı değiştirme
|
||||
|
||||
Varsayılan bit-oranı 8 Mbps'dir. Değiştirmek için (ör. 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # kısa versiyon
|
||||
```
|
||||
|
||||
#### Çerçeve oranı sınırlama
|
||||
|
||||
Ekran yakalama için maksimum çerçeve oranı için sınır koyulabilir:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
Bu özellik Android 10 ve sonrası sürümlerde resmi olarak desteklenmektedir,
|
||||
ancak daha önceki sürümlerde çalışmayabilir.
|
||||
|
||||
#### Kesme
|
||||
|
||||
Cihaz ekranının sadece bir kısmı görünecek şekilde kesilebilir.
|
||||
|
||||
Bu özellik Oculus Go'nun bir gözünü yakalamak gibi durumlarda kullanışlı olur:
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # (0,0) noktasından 1224x1440
|
||||
```
|
||||
|
||||
Eğer `--max-size` belirtilmişse yeniden boyutlandırma kesme işleminden sonra yapılır.
|
||||
|
||||
#### Video yönünü kilitleme
|
||||
|
||||
Videonun yönünü kilitlemek için:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # başlangıç yönü
|
||||
scrcpy --lock-video-orientation=0 # doğal yön
|
||||
scrcpy --lock-video-orientation=1 # 90° saatin tersi yönü
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 90° saat yönü
|
||||
```
|
||||
|
||||
Bu özellik kaydetme yönünü de etkiler.
|
||||
|
||||
[Pencere ayrı olarak döndürülmüş](#rotation) olabilir.
|
||||
|
||||
#### Kodlayıcı
|
||||
|
||||
Bazı cihazlar birden fazla kodlayıcıya sahiptir, ve bunların bazıları programın
|
||||
kapanmasına sebep olabilir. Bu durumda farklı bir kodlayıcı seçilebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
Mevcut kodlayıcıları listelemek için geçerli olmayan bir kodlayıcı ismi girebilirsiniz,
|
||||
hata mesajı mevcut kodlayıcıları listeleyecektir:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### Yakalama
|
||||
|
||||
#### Kaydetme
|
||||
|
||||
Ekran yakalama sırasında kaydedilebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
Yakalama olmadan kayıt için:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# Ctrl+C ile kayıt kesilebilir
|
||||
```
|
||||
|
||||
"Atlanan kareler" gerçek zamanlı olarak gösterilmese (performans sebeplerinden ötürü) dahi kaydedilir.
|
||||
Kareler cihazda _zamandamgası_ ile saklanır, bu sayede [paket gecikme varyasyonu]
|
||||
kayıt edilen dosyayı etkilemez.
|
||||
|
||||
[paket gecikme varyasyonu]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
Linux'ta video akışı bir v4l2 loopback cihazına gönderilebilir. Bu sayede Android
|
||||
cihaz bir web kamerası gibi davranabilir.
|
||||
|
||||
Bu işlem için `v4l2loopback` modülü kurulu olmalıdır:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
v4l2 cihazı oluşturmak için:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
Bu komut `/dev/videoN` adresinde `N` yerine bir tamsayı koyarak yeni bir video
|
||||
cihazı oluşturacaktır.
|
||||
(birden fazla cihaz oluşturmak veya spesifik ID'ye sahip cihazlar için
|
||||
diğer [seçenekleri](https://github.com/umlaeute/v4l2loopback#options) inceleyebilirsiniz.)
|
||||
|
||||
Aktif cihazları listelemek için:
|
||||
|
||||
```bash
|
||||
# v4l-utils paketi ile
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# daha basit ama yeterli olabilecek şekilde
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
v4l2 kullanarak scrpy kullanmaya başlamak için:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # ayna penceresini kapatarak
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # kısa versiyon
|
||||
```
|
||||
|
||||
(`N` harfini oluşturulan cihaz ID numarası ile değiştirin. `ls /dev/video*` cihaz ID'lerini görebilirsiniz.)
|
||||
|
||||
Aktifleştirildikten sonra video akışını herhangi bir v4l2 özellikli araçla açabilirsiniz:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC kullanırken yükleme gecikmesi olabilir
|
||||
```
|
||||
|
||||
Örneğin, [OBS] ile video akışını kullanabilirsiniz.
|
||||
|
||||
[obs]: https://obsproject.com/
|
||||
|
||||
### Bağlantı
|
||||
|
||||
#### Kablosuz
|
||||
|
||||
_Scrcpy_ cihazla iletişim kurmak için `adb`'yi kullanır, Ve `adb`
|
||||
bir cihaza TCP/IP kullanarak [bağlanabilir].
|
||||
|
||||
1. Cihazınızı bilgisayarınızla aynı Wi-Fi ağına bağlayın.
|
||||
2. Cihazınızın IP adresini bulun. Ayarlar → Telefon Hakkında → Durum sekmesinden veya
|
||||
aşağıdaki komutu çalıştırarak öğrenebilirsiniz:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. Cihazınızda TCP/IP üzerinden adb kullanımını etkinleştirin: `adb tcpip 5555`.
|
||||
4. Cihazınızı bilgisayarınızdan sökün.
|
||||
5. Cihazınıza bağlanın: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` değerini değiştirin)_.
|
||||
6. `scrcpy` komutunu normal olarak çalıştırın.
|
||||
|
||||
Bit-oranını ve büyüklüğü azaltmak yararlı olabilir:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # kısa version
|
||||
```
|
||||
|
||||
[bağlanabilir]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
#### Birden fazla cihaz
|
||||
|
||||
Eğer `adb devices` komutu birden fazla cihaz listeliyorsa _serial_ değerini belirtmeniz gerekir:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # kısa versiyon
|
||||
```
|
||||
|
||||
Eğer cihaz TCP/IP üzerinden bağlanmışsa:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # kısa version
|
||||
```
|
||||
|
||||
Birden fazla cihaz için birden fazla _scrcpy_ uygulaması çalıştırabilirsiniz.
|
||||
|
||||
#### Cihaz bağlantısı ile otomatik başlatma
|
||||
|
||||
[AutoAdb] ile yapılabilir:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[autoadb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### SSH Tünel
|
||||
|
||||
Uzaktaki bir cihaza erişmek için lokal `adb` istemcisi, uzaktaki bir `adb` sunucusuna
|
||||
(aynı _adb_ sürümünü kullanmak şartı ile) bağlanabilir :
|
||||
|
||||
```bash
|
||||
adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# bunu açık tutun
|
||||
```
|
||||
|
||||
Başka bir terminalde:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
Uzaktan port yönlendirme ileri yönlü bağlantı kullanabilirsiniz
|
||||
(`-R` yerine `-L` olduğuna dikkat edin):
|
||||
|
||||
```bash
|
||||
adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat
|
||||
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# bunu açık tutun
|
||||
```
|
||||
|
||||
Başka bir terminalde:
|
||||
|
||||
```bash
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
Kablosuz bağlantı gibi burada da kalite düşürmek faydalı olabilir:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### Pencere ayarları
|
||||
|
||||
#### İsim
|
||||
|
||||
Cihaz modeli varsayılan pencere ismidir. Değiştirmek için:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'Benim cihazım'
|
||||
```
|
||||
|
||||
#### Konum ve
|
||||
|
||||
Pencerenin başlangıç konumu ve boyutu belirtilebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### Kenarlıklar
|
||||
|
||||
Pencere dekorasyonunu kapatmak için:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### Her zaman üstte
|
||||
|
||||
Scrcpy penceresini her zaman üstte tutmak için:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### Tam ekran
|
||||
|
||||
Uygulamayı tam ekran başlatmak için:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # kısa versiyon
|
||||
```
|
||||
|
||||
Tam ekran <kbd>MOD</kbd>+<kbd>f</kbd> ile dinamik olarak değiştirilebilir.
|
||||
|
||||
#### Döndürme
|
||||
|
||||
Pencere döndürülebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
Seçilebilecek değerler:
|
||||
|
||||
- `0`: döndürme yok
|
||||
- `1`: 90 derece saat yönünün tersi
|
||||
- `2`: 180 derece
|
||||
- `3`: 90 derece saat yönü
|
||||
|
||||
Döndürme <kbd>MOD</kbd>+<kbd>←</kbd>_(sol)_ ve
|
||||
<kbd>MOD</kbd>+<kbd>→</kbd> _(sağ)_ ile dinamik olarak değiştirilebilir.
|
||||
|
||||
_scrcpy_'de 3 farklı döndürme olduğuna dikkat edin:
|
||||
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> cihazın yatay veya dikey modda çalışmasını sağlar.
|
||||
(çalışan uygulama istenilen oryantasyonda çalışmayı desteklemiyorsa döndürme
|
||||
işlemini reddedebilir.)
|
||||
- [`--lock-video-orientation`](#lock-video-orientation) görüntü yakalama oryantasyonunu
|
||||
(cihazdan bilgisayara gelen video akışının oryantasyonu) değiştirir. Bu kayıt işlemini
|
||||
etkiler.
|
||||
- `--rotation` (or <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)
|
||||
pencere içeriğini dönderir. Bu sadece canlı görüntüyü etkiler, kayıt işlemini etkilemez.
|
||||
|
||||
### Diğer ekran yakalama seçenekleri
|
||||
|
||||
#### Yazma korumalı
|
||||
|
||||
Kontrolleri devre dışı bırakmak için (cihazla etkileşime geçebilecek her şey: klavye ve
|
||||
fare girdileri, dosya sürükleyip bırakma):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### Ekran
|
||||
|
||||
Eğer cihazın birden fazla ekranı varsa hangi ekranın kullanılacağını seçebilirsiniz:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
Kullanılabilecek ekranları listelemek için:
|
||||
|
||||
```bash
|
||||
adb shell dumpsys display # çıktı içerisinde "mDisplayId=" terimini arayın
|
||||
```
|
||||
|
||||
İkinci ekran ancak cihaz Android sürümü 10 veya üzeri olmalıdır (değilse yazma korumalı
|
||||
olarak görüntülenir).
|
||||
|
||||
#### Uyanık kalma
|
||||
|
||||
Cihazın uyku moduna girmesini engellemek için:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
scrcpy kapandığında cihaz başlangıç durumuna geri döner.
|
||||
|
||||
#### Ekranı kapatma
|
||||
|
||||
Ekran yakalama sırasında cihazın ekranı kapatılabilir:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
Ya da <kbd>MOD</kbd>+<kbd>o</kbd> kısayolunu kullanabilirsiniz.
|
||||
|
||||
Tekrar açmak için ise <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd> tuşlarına basın.
|
||||
|
||||
Android'de, `GÜÇ` tuşu her zaman ekranı açar. Eğer `GÜÇ` sinyali scrcpy ile
|
||||
gönderilsiyse (sağ tık veya <kbd>MOD</kbd>+<kbd>p</kbd>), ekran kısa bir gecikme
|
||||
ile kapanacaktır. Fiziksel `GÜÇ` tuşuna basmak hala ekranın açılmasına sebep olacaktır.
|
||||
|
||||
Bu cihazın uykuya geçmesini engellemek için kullanılabilir:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
#### Dokunuşları gösterme
|
||||
|
||||
Sunumlar sırasında fiziksel dokunuşları (fiziksel cihazdaki) göstermek
|
||||
faydalı olabilir.
|
||||
|
||||
Android'de bu özellik _Geliştici seçenekleri_ içerisinde bulunur.
|
||||
|
||||
_Scrcpy_ bu özelliği çalışırken etkinleştirebilir ve kapanırken eski
|
||||
haline geri getirebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
Bu opsiyon sadece _fiziksel_ dokunuşları (cihaz ekranındaki) gösterir.
|
||||
|
||||
#### Ekran koruyucuyu devre dışı bırakma
|
||||
|
||||
Scrcpy varsayılan ayarlarında ekran koruyucuyu devre dışı bırakmaz.
|
||||
|
||||
Bırakmak için:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
### Girdi kontrolü
|
||||
|
||||
#### Cihaz ekranını dönderme
|
||||
|
||||
<kbd>MOD</kbd>+<kbd>r</kbd> tuşları ile yatay ve dikey modlar arasında
|
||||
geçiş yapabilirsiniz.
|
||||
|
||||
Bu kısayol ancak çalışan uygulama desteklediği takdirde ekranı döndürecektir.
|
||||
|
||||
#### Kopyala yapıştır
|
||||
|
||||
Ne zaman Android cihazdaki pano değişse bilgisayardaki pano otomatik olarak
|
||||
senkronize edilir.
|
||||
|
||||
Tüm <kbd>Ctrl</kbd> kısayolları cihaza iletilir:
|
||||
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> genelde kopyalar
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> genelde keser
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> genelde yapıştırır (bilgisayar ve cihaz arasındaki
|
||||
pano senkronizasyonundan sonra)
|
||||
|
||||
Bu kısayollar genelde beklediğiniz gibi çalışır.
|
||||
|
||||
Ancak kısayolun gerçekten yaptığı eylemi açık olan uygulama belirler.
|
||||
Örneğin, _Termux_ <kbd>Ctrl</kbd>+<kbd>c</kbd> ile kopyalama yerine
|
||||
SIGINT sinyali gönderir, _K-9 Mail_ ise yeni mesaj oluşturur.
|
||||
|
||||
Bu tip durumlarda kopyalama, kesme ve yapıştırma için (Android versiyon 7 ve
|
||||
üstü):
|
||||
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> `KOPYALA`
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> `KES`
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> `YAPIŞTIR` (bilgisayar ve cihaz arasındaki
|
||||
pano senkronizasyonundan sonra)
|
||||
|
||||
Bunlara ek olarak, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> tuşları
|
||||
bilgisayar pano içeriğini tuş basma eylemleri şeklinde gönderir. Bu metin
|
||||
yapıştırmayı desteklemeyen (_Termux_ gibi) uygulamar için kullanışlıdır,
|
||||
ancak ASCII olmayan içerikleri bozabilir.
|
||||
|
||||
**UYARI:** Bilgisayar pano içeriğini cihaza yapıştırmak
|
||||
(<kbd>Ctrl</kbd>+<kbd>v</kbd> ya da <kbd>MOD</kbd>+<kbd>v</kbd> tuşları ile)
|
||||
içeriği cihaz panosuna kopyalar. Sonuç olarak, herhangi bir Android uygulaması
|
||||
içeriğe erişebilir. Hassas içerikler (parolalar gibi) için bu özelliği kullanmaktan
|
||||
kaçının.
|
||||
|
||||
Bazı cihazlar pano değişikleri konusunda beklenilen şekilde çalışmayabilir.
|
||||
Bu durumlarda `--legacy-paste` argümanı kullanılabilir. Bu sayede
|
||||
<kbd>Ctrl</kbd>+<kbd>v</kbd> ve <kbd>MOD</kbd>+<kbd>v</kbd> tuşları da
|
||||
pano içeriğini tuş basma eylemleri şeklinde gönderir
|
||||
(<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> ile aynı şekilde).
|
||||
|
||||
#### İki parmak ile yakınlaştırma
|
||||
|
||||
"İki parmak ile yakınlaştırma" için: <kbd>Ctrl</kbd>+_tıkla-ve-sürükle_.
|
||||
|
||||
Daha açıklayıcı şekilde, <kbd>Ctrl</kbd> tuşuna sol-tık ile birlikte basılı
|
||||
tutun. Sol-tık serbest bırakılıncaya kadar yapılan tüm fare hareketleri
|
||||
ekran içeriğini ekranın merkezini baz alarak dönderir, büyütür veya küçültür
|
||||
(eğer uygulama destekliyorsa).
|
||||
|
||||
Scrcpy ekranın merkezinde bir "sanal parmak" varmış gibi davranır.
|
||||
|
||||
#### Metin gönderme tercihi
|
||||
|
||||
Metin girilirken ili çeşit [eylem][textevents] gerçekleştirilir:
|
||||
|
||||
- _tuş eylemleri_, bir tuşa basıldığı sinyalini verir;
|
||||
- _metin eylemleri_, bir metin girildiği sinyalini verir.
|
||||
|
||||
Varsayılan olarak, harfler tuş eylemleri kullanılarak gönderilir. Bu sayede
|
||||
klavye oyunlarda beklenilene uygun olarak çalışır (Genelde WASD tuşları).
|
||||
|
||||
Ancak bu [bazı problemlere][prefertext] yol açabilir. Eğer bu problemler ile
|
||||
karşılaşırsanız metin eylemlerini tercih edebilirsiniz:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(Ama bu oyunlardaki klavye davranışlarını bozacaktır)
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
#### Tuş tekrarı
|
||||
|
||||
Varsayılan olarak, bir tuşa basılı tutmak tuş eylemini tekrarlar. Bu durum
|
||||
bazı oyunlarda problemlere yol açabilir.
|
||||
|
||||
Tuş eylemlerinin tekrarını kapatmak için:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
#### Sağ-tık ve Orta-tık
|
||||
|
||||
Varsayılan olarak, sağ-tık GERİ (ya da GÜÇ açma) eylemlerini, orta-tık ise
|
||||
ANA EKRAN eylemini tetikler. Bu kısayolları devre dışı bırakmak için:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
### Dosya bırakma
|
||||
|
||||
#### APK kurulumu
|
||||
|
||||
APK kurmak için, bilgisayarınızdaki APK dosyasını (`.apk` ile biten) _scrcpy_
|
||||
penceresine sürükleyip bırakın.
|
||||
|
||||
Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır.
|
||||
|
||||
#### Dosyayı cihaza gönderme
|
||||
|
||||
Bir dosyayı cihazdaki `/sdcard/Download/` dizinine atmak için, (APK olmayan)
|
||||
bir dosyayı _scrcpy_ penceresine sürükleyip bırakın.
|
||||
|
||||
Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır.
|
||||
|
||||
Hedef dizin uygulama başlatılırken değiştirilebilir:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
### Ses iletimi
|
||||
|
||||
_Scrcpy_ ses iletimi yapmaz. Yerine [sndcpy] kullanabilirsiniz.
|
||||
|
||||
Ayrıca bakınız [issue #14].
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
## Kısayollar
|
||||
|
||||
Aşağıdaki listede, <kbd>MOD</kbd> kısayol tamamlayıcısıdır. Varsayılan olarak
|
||||
(sol) <kbd>Alt</kbd> veya (sol) <kbd>Super</kbd> tuşudur.
|
||||
|
||||
Bu tuş `--shortcut-mod` argümanı kullanılarak `lctrl`, `rctrl`,
|
||||
`lalt`, `ralt`, `lsuper` ve `rsuper` tuşlarından biri ile değiştirilebilir.
|
||||
Örneğin:
|
||||
|
||||
```bash
|
||||
# Sağ Ctrl kullanmak için
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# Sol Ctrl, Sol Alt veya Sol Super tuşlarından birini kullanmak için
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> tuşu genelde <kbd>Windows</kbd> veya <kbd>Cmd</kbd> tuşudur._
|
||||
|
||||
[super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Action | Shortcut |
|
||||
| ------------------------------------------------ | :-------------------------------------------------------- |
|
||||
| Tam ekran modunu değiştirme | <kbd>MOD</kbd>+<kbd>f</kbd> |
|
||||
| Ekranı sola çevirme | <kbd>MOD</kbd>+<kbd>←</kbd> _(sol)_ |
|
||||
| Ekranı sağa çevirme | <kbd>MOD</kbd>+<kbd>→</kbd> _(sağ)_ |
|
||||
| Pencereyi 1:1 oranına çevirme (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd> |
|
||||
| Penceredeki siyah kenarlıkları kaldırma | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Çift-sol-tık¹_ |
|
||||
| `ANA EKRAN` tuşu | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Orta-tık_ |
|
||||
| `GERİ` tuşu | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Sağ-tık²_ |
|
||||
| `UYGULAMA_DEĞİŞTİR` tuşu | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4.tık³_ |
|
||||
| `MENÜ` tuşu (ekran kilidini açma) | <kbd>MOD</kbd>+<kbd>m</kbd> |
|
||||
| `SES_AÇ` tuşu | <kbd>MOD</kbd>+<kbd>↑</kbd> _(yukarı)_ |
|
||||
| `SES_KIS` tuşu | <kbd>MOD</kbd>+<kbd>↓</kbd> _(aşağı)_ |
|
||||
| `GÜÇ` tuşu | <kbd>MOD</kbd>+<kbd>p</kbd> |
|
||||
| Gücü açma | _Sağ-tık²_ |
|
||||
| Cihaz ekranını kapatma (ekran yakalama durmadan) | <kbd>MOD</kbd>+<kbd>o</kbd> |
|
||||
| Cihaz ekranını açma | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd> |
|
||||
| Cihaz ekranını dönderme | <kbd>MOD</kbd>+<kbd>r</kbd> |
|
||||
| Bildirim panelini genişletme | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5.tık³_ |
|
||||
| Ayarlar panelini genişletme | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Çift-5.tık³_ |
|
||||
| Panelleri kapatma | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd> |
|
||||
| Panoya kopyalama⁴ | <kbd>MOD</kbd>+<kbd>c</kbd> |
|
||||
| Panoya kesme⁴ | <kbd>MOD</kbd>+<kbd>x</kbd> |
|
||||
| Panoları senkronize ederek yapıştırma⁴ | <kbd>MOD</kbd>+<kbd>v</kbd> |
|
||||
| Bilgisayar panosundaki metini girme | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> |
|
||||
| FPS sayacını açma/kapatma (terminalde) | <kbd>MOD</kbd>+<kbd>i</kbd> |
|
||||
| İki parmakla yakınlaştırma | <kbd>Ctrl</kbd>+_tıkla-ve-sürükle_ |
|
||||
|
||||
_¹Siyah kenarlıkları silmek için üzerine çift tıklayın._
|
||||
_²Sağ-tık ekran kapalıysa açar, değilse GERİ sinyali gönderir._
|
||||
_³4. ve 5. fare tuşları (eğer varsa)._
|
||||
_⁴Sadece Android 7 ve üzeri versiyonlarda._
|
||||
|
||||
Tekrarlı tuşu olan kısayollar tuş bırakılıp tekrar basılarak tekrar çalıştırılır.
|
||||
Örneğin, "Ayarlar panelini genişletmek" için:
|
||||
|
||||
1. <kbd>MOD</kbd> tuşuna basın ve basılı tutun.
|
||||
2. <kbd>n</kbd> tuşuna iki defa basın.
|
||||
3. <kbd>MOD</kbd> tuşuna basmayı bırakın.
|
||||
|
||||
Tüm <kbd>Ctrl</kbd>+_tuş_ kısayolları cihaza gönderilir. Bu sayede istenilen komut
|
||||
uygulama tarafından çalıştırılır.
|
||||
|
||||
## Özel dizinler
|
||||
|
||||
Varsayılandan farklı bir _adb_ programı çalıştırmak için `ADB` ortam değişkenini
|
||||
ayarlayın:
|
||||
|
||||
```bash
|
||||
ADB=/path/to/adb scrcpy
|
||||
```
|
||||
|
||||
`scrcpy-server` programının dizinini değiştirmek için `SCRCPY_SERVER_PATH`
|
||||
değişkenini ayarlayın.
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
## Neden _scrcpy_?
|
||||
|
||||
Bir meslektaşım [gnirehtet] gibi söylenmesi zor bir isim bulmam için bana meydan okudu.
|
||||
|
||||
[`strcpy`] **str**ing kopyalıyor; `scrcpy` **scr**een kopyalıyor.
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
## Nasıl derlenir?
|
||||
|
||||
Bakınız [BUILD].
|
||||
|
||||
## Yaygın problemler
|
||||
|
||||
Bakınız [FAQ](FAQ.md).
|
||||
|
||||
## Geliştiriciler
|
||||
|
||||
[Geliştiriciler sayfası]nı okuyun.
|
||||
|
||||
[geliştiriciler sayfası]: DEVELOP.md
|
||||
|
||||
## Lisans
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 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.
|
||||
|
||||
## Makaleler
|
||||
|
||||
- [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/
|
||||
993
README.zh-Hans.md
Normal file
993
README.zh-Hans.md
Normal file
@@ -0,0 +1,993 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
_只有原版的 [README](README.md)是保证最新的。_
|
||||
|
||||
Current version is based on [f4c7044]
|
||||
|
||||
本文根据[f4c7044]进行翻译。
|
||||
|
||||
[f4c7044]: https://github.com/Genymobile/scrcpy/blob/f4c7044b46ae28eb64cb5e1a15c9649a44023c70/README.md
|
||||
|
||||
# scrcpy (v1.22)
|
||||
|
||||
<img src="data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
_发音为 "**scr**een **c**o**py**"_
|
||||
|
||||
本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。
|
||||
|
||||

|
||||
|
||||
本应用专注于:
|
||||
|
||||
- **轻量**: 原生,仅显示设备屏幕
|
||||
- **性能**: 30~120fps,取决于设备
|
||||
- **质量**: 分辨率可达 1920×1080 或更高
|
||||
- **低延迟**: [35~70ms][lowlatency]
|
||||
- **快速启动**: 最快 1 秒内即可显示第一帧
|
||||
- **无侵入性**: 不会在设备上遗留任何程序
|
||||
- **用户利益**: 无需帐号,无广告,无需联网
|
||||
- **自由**: 自由和开源软件
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
功能:
|
||||
- [屏幕录制](#屏幕录制)
|
||||
- 镜像时[关闭设备屏幕](#关闭设备屏幕)
|
||||
- 双向[复制粘贴](#复制粘贴)
|
||||
- [可配置显示质量](#采集设置)
|
||||
- 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux)
|
||||
- [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux)
|
||||
- [物理鼠标模拟 (HID)](#物理鼠标模拟-hid) (仅限 Linux)
|
||||
- [OTG模式](#otg) (仅限 Linux)
|
||||
- 更多 ……
|
||||
|
||||
## 系统要求
|
||||
|
||||
安卓设备最低需要支持 API 21 (Android 5.0)。
|
||||
|
||||
确保设备已[开启 adb 调试][enable-adb]。
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
在某些设备上,还需要开启[额外的选项][control]以使用鼠标和键盘进行控制。
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## 获取本程序
|
||||
|
||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
||||
|
||||
### 概要
|
||||
|
||||
- Linux: `apt install scrcpy`
|
||||
- Windows: [下载][direct-win64]
|
||||
- macOS: `brew install scrcpy`
|
||||
|
||||
从源代码编译: [构建][BUILD] ([简化过程][BUILD_simple])
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
[BUILD_simple]: BUILD.md#simple
|
||||
|
||||
### Linux
|
||||
|
||||
在 Debian 和 Ubuntu 上:
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
在 Arch Linux 上:
|
||||
|
||||
```
|
||||
pacman -S scrcpy
|
||||
```
|
||||
|
||||
我们也提供 [Snap] 包: [`scrcpy`][snap-link]。
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
对 Fedora 我们提供 [COPR] 包: [`scrcpy`][copr-link]。
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
对 Gentoo 我们提供 [Ebuild] 包:[`scrcpy/`][ebuild-link]。
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
您也可以[自行构建][BUILD] ([简化过程][BUILD_simple])。
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
也可以使用 [Chocolatey]:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # 如果还没有 adb
|
||||
```
|
||||
|
||||
或者 [Scoop]:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # 如果还没有 adb
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
您也可以[自行构建][BUILD]。
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
本程序已发布到 [Homebrew]。直接安装即可:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
你还需要在 `PATH` 内有 `adb`。如果还没有:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
或者通过 [MacPorts],该方法同时设置好 adb:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
您也可以[自行构建][BUILD]。
|
||||
|
||||
|
||||
## 运行
|
||||
|
||||
连接安卓设备,然后执行:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
本程序支持命令行参数,查看参数列表:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
## 功能介绍
|
||||
|
||||
### 采集设置
|
||||
|
||||
#### 降低分辨率
|
||||
|
||||
有时候,可以通过降低镜像的分辨率来提高性能。
|
||||
|
||||
要同时限制宽度和高度到某个值 (例如 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # 简写
|
||||
```
|
||||
|
||||
另一边会被按比例缩小以保持设备的显示比例。这样,1920×1080 分辨率的设备会以 1024×576 的分辨率进行镜像。
|
||||
|
||||
|
||||
#### 修改码率
|
||||
|
||||
默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # 简写
|
||||
```
|
||||
|
||||
#### 限制帧率
|
||||
|
||||
要限制采集的帧率:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
本功能从 Android 10 开始才被官方支持,但在一些旧版本中也能生效。
|
||||
|
||||
#### 画面裁剪
|
||||
|
||||
可以对设备屏幕进行裁剪,只镜像屏幕的一部分。
|
||||
|
||||
例如可以只镜像 Oculus Go 的一只眼睛。
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素
|
||||
```
|
||||
|
||||
如果同时指定了 `--max-size`,会先进行裁剪,再进行缩放。
|
||||
|
||||
|
||||
#### 锁定屏幕方向
|
||||
|
||||
|
||||
要锁定镜像画面的方向:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # 初始(目前)方向
|
||||
scrcpy --lock-video-orientation=0 # 自然方向
|
||||
scrcpy --lock-video-orientation=1 # 逆时针旋转 90°
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 顺时针旋转 90°
|
||||
```
|
||||
|
||||
只影响录制的方向。
|
||||
|
||||
[窗口可以独立旋转](#旋转)。
|
||||
|
||||
|
||||
#### 编码器
|
||||
|
||||
一些设备内置了多种编码器,但是有的编码器会导致问题或崩溃。可以手动选择其它编码器:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
要列出可用的编码器,可以指定一个不存在的编码器名称,错误信息中会包含所有的编码器:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder _
|
||||
```
|
||||
|
||||
### 采集
|
||||
|
||||
#### 屏幕录制
|
||||
|
||||
可以在镜像的同时录制视频:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
仅录制,不显示镜像:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# 按 Ctrl+C 停止录制
|
||||
```
|
||||
|
||||
录制时会包含“被跳过的帧”,即使它们由于性能原因没有实时显示。设备会为每一帧打上 _时间戳_ ,所以 [包时延抖动][packet delay variation] 不会影响录制的文件。
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
#### v4l2loopback
|
||||
|
||||
在 Linux 上,可以将视频流发送至 v4l2 回环 (loopback) 设备,因此可以使用任何 v4l2 工具像摄像头一样打开安卓设备。
|
||||
|
||||
需安装 `v4l2loopback` 模块:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
创建一个 v4l2 设备:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
这样会在 `/dev/videoN` 创建一个新的视频设备,其中 `N` 是整数。 ([更多选项](https://github.com/umlaeute/v4l2loopback#options) 可以用来创建多个设备或者特定 ID 的设备)。
|
||||
|
||||
列出已启用的设备:
|
||||
|
||||
```bash
|
||||
# 需要 v4l-utils 包
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# 简单但或许足够
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
使用一个 v4l2 漏开启 scrcpy:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # 禁用窗口镜像
|
||||
scrcpy --v4l2-sink=/dev/videoN -N # 简写
|
||||
```
|
||||
|
||||
(将 `N` 替换为设备 ID,使用 `ls /dev/video*` 命令查看)
|
||||
|
||||
启用之后,可以使用 v4l2 工具打开视频流:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC 可能存在一些缓冲延迟
|
||||
```
|
||||
|
||||
例如,可以在 [OBS] 中采集视频。
|
||||
|
||||
[OBS]: https://obsproject.com/
|
||||
|
||||
|
||||
#### 缓冲
|
||||
|
||||
可以加入缓冲,会增加延迟,但可以减少抖动 (见 [#2464])。
|
||||
|
||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
||||
|
||||
对于显示缓冲:
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # 为显示增加 50 毫秒的缓冲
|
||||
```
|
||||
|
||||
对于 V4L2 漏:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲
|
||||
```
|
||||
|
||||
|
||||
### 连接
|
||||
|
||||
#### TCP/IP (无线)
|
||||
|
||||
_Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接]到设备(设备必须连接与电脑相同的网络)。
|
||||
|
||||
##### 自动配置
|
||||
|
||||
参数 `--tcpip` 允许自动配置连接。这里有两种方式。
|
||||
|
||||
对于传入的 adb 连接,如果设备(在这个例子中以192.168.1.1为可用地址)已经监听了一个端口(通常是5555),运行:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip=192.168.1.1 # 默认端口是5555
|
||||
scrcpy --tcpip=192.168.1.1:5555
|
||||
```
|
||||
|
||||
如果adb TCP/IP(无线) 模式在某些设备上不被启用(或者你不知道IP地址),用USB连接设备,然后运行:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip # 无需其他参数
|
||||
```
|
||||
|
||||
这将会自动寻找设备IP地址,启用TCP/IP模式,然后在启动之前连接到设备。
|
||||
|
||||
##### 手动配置
|
||||
|
||||
或者,可以通过 `adb` 使用手动启用 TCP/IP 连接:
|
||||
|
||||
1. 将设备和电脑连接至同一 Wi-Fi。
|
||||
2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
3. 启用设备的网络 adb 功能:`adb tcpip 5555`。
|
||||
4. 断开设备的 USB 连接。
|
||||
5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_。
|
||||
6. 正常运行 `scrcpy`。
|
||||
|
||||
降低比特率和分辨率可能很有用:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # 简写
|
||||
```
|
||||
|
||||
[连接]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### 多设备
|
||||
|
||||
如果 `adb devices` 列出了多个设备,您必须指定设备的 _序列号_ :
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # 简写
|
||||
```
|
||||
|
||||
如果设备通过 TCP/IP 连接:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # 简写
|
||||
```
|
||||
|
||||
您可以同时启动多个 _scrcpy_ 实例以同时显示多个设备的画面。
|
||||
|
||||
#### 在设备连接时自动启动
|
||||
|
||||
您可以使用 [AutoAdb]:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### 隧道
|
||||
|
||||
要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同)。
|
||||
|
||||
##### 远程ADB服务器
|
||||
|
||||
要连接到一个远程ADB服务器,让服务器在所有接口上监听:
|
||||
|
||||
```bash
|
||||
adb kill-server
|
||||
adb -a nodaemon server start
|
||||
# 保持该窗口开启
|
||||
```
|
||||
|
||||
**警告:所有客户端与ADB服务器的交流都是未加密的。**
|
||||
|
||||
假设此服务器可在 192.168.1.2 访问。 然后,从另一个终端,运行 scrcpy:
|
||||
|
||||
```bash
|
||||
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
默认情况下,scrcpy使用用于 `adb forward` 隧道建立的本地端口(通常是 `27183`,见 `--port` )。它也可以强制使用一个不同的隧道端口(当涉及更多的重定向时,这在更复杂的情况下可能很有用):
|
||||
|
||||
```
|
||||
scrcpy --tunnel-port=1234
|
||||
```
|
||||
|
||||
|
||||
##### SSH 隧道
|
||||
|
||||
为了安全地与远程ADB服务器通信,最好使用SSH隧道。
|
||||
|
||||
首先,确保ADB服务器正在远程计算机上运行:
|
||||
|
||||
```bash
|
||||
adb start-server
|
||||
```
|
||||
|
||||
然后,建立一个SSH隧道:
|
||||
|
||||
```bash
|
||||
# 本地 5038 --> 远程 5037
|
||||
# 本地 27183 <-- 远程 27183
|
||||
ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# 保持该窗口开启
|
||||
```
|
||||
|
||||
在另一个终端上,运行scrcpy:
|
||||
|
||||
```bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy
|
||||
```
|
||||
|
||||
若要不使用远程端口转发,可以强制使用正向连接(注意是 `-L` 而不是 `-R` ):
|
||||
|
||||
```bash
|
||||
# 本地 5038 --> 远程 5037
|
||||
# 本地 27183 <-- 远程 27183
|
||||
ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# 保持该窗口开启
|
||||
```
|
||||
|
||||
在另一个终端上,运行scrcpy:
|
||||
|
||||
```bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
类似地,对于无线连接,可能需要降低画面质量:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### 窗口设置
|
||||
|
||||
#### 标题
|
||||
|
||||
窗口的标题默认为设备型号。可以通过如下命令修改:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title "我的设备"
|
||||
```
|
||||
|
||||
#### 位置和大小
|
||||
|
||||
您可以指定初始的窗口位置和大小:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### 无边框
|
||||
|
||||
禁用窗口边框:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### 保持窗口在最前
|
||||
|
||||
您可以通过如下命令保持窗口在最前面:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### 全屏
|
||||
|
||||
您可以通过如下命令直接全屏启动 scrcpy:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # 简写
|
||||
```
|
||||
|
||||
全屏状态可以通过 <kbd>MOD</kbd>+<kbd>f</kbd> 随时切换。
|
||||
|
||||
#### 旋转
|
||||
|
||||
可以通过以下命令旋转窗口:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
可选的值有:
|
||||
- `0`: 无旋转
|
||||
- `1`: 逆时针旋转 90°
|
||||
- `2`: 旋转 180°
|
||||
- `3`: 顺时针旋转 90°
|
||||
|
||||
也可以使用 <kbd>MOD</kbd>+<kbd>←</kbd> _(左箭头)_ 和 <kbd>MOD</kbd>+<kbd>→</kbd> _(右箭头)_ 随时更改。
|
||||
|
||||
需要注意的是, _scrcpy_ 中有三类旋转方向:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。
|
||||
- [`--lock-video-orientation`](#锁定屏幕方向) 改变镜像的朝向 (设备传输到电脑的画面的朝向)。这会影响录制。
|
||||
- `--rotation` (或 <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>) 只旋转窗口的内容。这只影响显示,不影响录制。
|
||||
|
||||
|
||||
### 其他镜像设置
|
||||
|
||||
#### 只读
|
||||
|
||||
禁用电脑对设备的控制 (任何可与设备交互的方式:如键盘输入、鼠标事件和文件拖放):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### 显示屏
|
||||
|
||||
如果设备有多个显示屏,可以选择要镜像的显示屏:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
可以通过如下命令列出所有显示屏的 id:
|
||||
|
||||
```
|
||||
adb shell dumpsys display # 在输出中搜索 “mDisplayId=”
|
||||
```
|
||||
|
||||
控制第二显示屏需要设备运行 Android 10 或更高版本 (否则将在只读状态下镜像)。
|
||||
|
||||
|
||||
#### 保持常亮
|
||||
|
||||
阻止设备在连接时一段时间后休眠:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
scrcpy 关闭时会恢复设备原来的设置。
|
||||
|
||||
|
||||
#### 关闭设备屏幕
|
||||
|
||||
可以通过以下的命令行参数在关闭设备屏幕的状态下进行镜像:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
或者在任何时候按 <kbd>MOD</kbd>+<kbd>o</kbd>。
|
||||
|
||||
要重新打开屏幕,按下 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>。
|
||||
|
||||
在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 <kbd>MOD</kbd>+<kbd>p</kbd>),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。
|
||||
|
||||
还可以同时阻止设备休眠:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
#### 退出时息屏
|
||||
|
||||
scrcpy 退出时关闭设备屏幕:
|
||||
|
||||
```bash
|
||||
scrcpy --power-off-on-close
|
||||
```
|
||||
|
||||
#### 显示触摸
|
||||
|
||||
在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。
|
||||
|
||||
Android 在 _开发者选项_ 中提供了这项功能。
|
||||
|
||||
_Scrcpy_ 提供一个选项可以在启动时开启这项功能并在退出时恢复初始设置:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
请注意这项功能只能显示 _物理_ 触摸 (用手指在屏幕上的触摸)。
|
||||
|
||||
|
||||
#### 关闭屏保
|
||||
|
||||
_Scrcpy_ 默认不会阻止电脑上开启的屏幕保护。
|
||||
|
||||
关闭屏幕保护:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
||||
|
||||
|
||||
### 输入控制
|
||||
|
||||
#### 旋转设备屏幕
|
||||
|
||||
使用 <kbd>MOD</kbd>+<kbd>r</kbd> 在竖屏和横屏模式之间切换。
|
||||
|
||||
需要注意的是,只有在前台应用程序支持所要求的模式时,才会进行切换。
|
||||
|
||||
#### 复制粘贴
|
||||
|
||||
每次安卓的剪贴板变化时,其内容都会被自动同步到电脑的剪贴板上。
|
||||
|
||||
所有的 <kbd>Ctrl</kbd> 快捷键都会被转发至设备。其中:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> 通常执行复制
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> 通常执行剪切
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> 通常执行粘贴 (在电脑到设备的剪贴板同步完成之后)
|
||||
|
||||
大多数时候这些按键都会执行以上的功能。
|
||||
|
||||
但实际的行为取决于设备上的前台程序。例如,_Termux_ 会在按下 <kbd>Ctrl</kbd>+<kbd>c</kbd> 时发送 SIGINT,又如 _K-9 Mail_ 会新建一封邮件。
|
||||
|
||||
要在这种情况下进行剪切,复制和粘贴 (仅支持 Android >= 7):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> 注入 `COPY` (复制)
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> 注入 `CUT` (剪切)
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> 注入 `PASTE` (粘贴) (在电脑到设备的剪贴板同步完成之后)
|
||||
|
||||
另外,<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> 会将电脑的剪贴板内容转换为一串按键事件输入到设备。在应用程序不接受粘贴时 (比如 _Termux_),这项功能可以派上一定的用场。不过这项功能可能会导致非 ASCII 编码的内容出现错误。
|
||||
|
||||
**警告:** 将电脑剪贴板的内容粘贴至设备 (无论是通过 <kbd>Ctrl</kbd>+<kbd>v</kbd> 还是 <kbd>MOD</kbd>+<kbd>v</kbd>) 都会将内容复制到设备的剪贴板。如此,任何安卓应用程序都能读取到。您应避免将敏感内容 (如密码) 通过这种方式粘贴。
|
||||
|
||||
一些设备不支持通过程序设置剪贴板。通过 `--legacy-paste` 选项可以修改 <kbd>Ctrl</kbd>+<kbd>v</kbd> 和 <kbd>MOD</kbd>+<kbd>v</kbd> 的工作方式,使它们通过按键事件 (同 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>) 来注入电脑剪贴板内容。
|
||||
|
||||
要禁用自动剪贴板同步功能,使用`--no-clipboard-autosync`。
|
||||
|
||||
#### 双指缩放
|
||||
|
||||
模拟“双指缩放”:<kbd>Ctrl</kbd>+_按下并拖动鼠标_。
|
||||
|
||||
在按住 <kbd>Ctrl</kbd> 时按下鼠标左键,直到松开鼠标左键前,移动鼠标会使屏幕内容相对于屏幕中心进行缩放或旋转 (如果应用支持)。
|
||||
|
||||
具体来说,_scrcpy_ 会在鼠标位置,以及鼠标以屏幕中心镜像的位置分别生成触摸事件。
|
||||
|
||||
#### 物理键盘模拟 (HID)
|
||||
|
||||
默认情况下,scrcpy 使用安卓按键或文本注入,这在任何情况都可以使用,但仅限于ASCII字符。
|
||||
|
||||
在 Linux 上,scrcpy 可以模拟为 Android 上的物理 USB 键盘,以提供更好地输入体验 (使用 [USB HID over AOAv2][hid-aoav2]):禁用虚拟键盘,并适用于任何字符和输入法。
|
||||
|
||||
[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
|
||||
|
||||
不过,这种方法仅支持 USB 连接以及 Linux平台。
|
||||
|
||||
启用 HID 模式:
|
||||
|
||||
```bash
|
||||
scrcpy --hid-keyboard
|
||||
scrcpy -K # 简写
|
||||
```
|
||||
|
||||
如果失败了 (如设备未通过 USB 连接),则自动回退至默认模式 (终端中会输出日志)。这即允许通过 USB 和 TCP/IP 连接时使用相同的命令行参数。
|
||||
|
||||
在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。
|
||||
|
||||
[实体键盘]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
|
||||
|
||||
#### 物理鼠标模拟 (HID)
|
||||
|
||||
与物理键盘模拟类似,可以模拟一个物理鼠标。 同样,它仅在设备通过 USB 连接时才有效,并且目前仅在 Linux 上受支持。
|
||||
|
||||
默认情况下,scrcpy 使用 Android 鼠标事件注入,使用绝对坐标。 通过模拟物理鼠标,在Android设备上出现鼠标指针,并注入鼠标相对运动、点击和滚动。
|
||||
|
||||
启用此模式:
|
||||
|
||||
```bash
|
||||
scrcpy --hid-mouse
|
||||
scrcpy -M # 简写
|
||||
```
|
||||
|
||||
您还可以将 `--forward-all-clicks` 添加到 [转发所有点击][forward_all_clicks].
|
||||
|
||||
[forward_all_clicks]: #右键和中键
|
||||
|
||||
启用此模式后,计算机鼠标将被“捕获”(鼠标指针从计算机上消失并出现在 Android 设备上)。
|
||||
|
||||
特殊的捕获键,<kbd>Alt</kbd> 或 <kbd>Super</kbd>,切换(禁用或启用)鼠标捕获。 使用其中之一将鼠标的控制权交还给计算机。
|
||||
|
||||
|
||||
#### OTG
|
||||
|
||||
可以仅使用物理键盘和鼠标模拟 (HID) 运行 _scrcpy_,就好像计算机键盘和鼠标通过 OTG 线直接插入设备一样。
|
||||
|
||||
在这个模式下,_adb_ (USB 调试)是不必要的,且镜像被禁用。
|
||||
|
||||
启用 OTG 模式:
|
||||
|
||||
```bash
|
||||
scrcpy --otg
|
||||
# 如果有多个 USB 设备可用,则通过序列号选择
|
||||
scrcpy --otg -s 0123456789abcdef
|
||||
```
|
||||
|
||||
只开启 HID 键盘 或 HID 鼠标 是可行的:
|
||||
|
||||
```bash
|
||||
scrcpy --otg --hid-keyboard # 只开启 HID 键盘
|
||||
scrcpy --otg --hid-mouse # 只开启 HID 鼠标
|
||||
scrcpy --otg --hid-keyboard --hid-mouse # 开启 HID 键盘 和 HID 鼠标
|
||||
# 为了方便,默认两者都开启
|
||||
scrcpy --otg # 开启 HID 键盘 和 HID 鼠标
|
||||
```
|
||||
|
||||
像 `--hid-keyboard` 和 `--hid-mouse` 一样,它只在设备通过 USB 连接时才有效,且目前仅在 Linux 上支持。
|
||||
|
||||
|
||||
#### 文本注入偏好
|
||||
|
||||
输入文字的时候,系统会产生两种[事件][textevents]:
|
||||
- _按键事件_ ,代表一个按键被按下或松开。
|
||||
- _文本事件_ ,代表一个字符被输入。
|
||||
|
||||
程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作 (例如 WASD 键)。
|
||||
|
||||
但这也有可能[造成一些问题][prefertext]。如果您遇到了问题,可以通过以下方式避免:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(但这会导致键盘在游戏中工作不正常)
|
||||
|
||||
相反,您可以强制始终注入原始按键事件:
|
||||
|
||||
```bash
|
||||
scrcpy --raw-key-events
|
||||
```
|
||||
|
||||
该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### 按键重复
|
||||
|
||||
默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这通常没有实际用途,且可能会导致性能问题。
|
||||
|
||||
避免转发重复按键事件:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。
|
||||
|
||||
#### 右键和中键
|
||||
|
||||
默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
|
||||
### 文件拖放
|
||||
|
||||
#### 安装APK
|
||||
|
||||
将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。
|
||||
|
||||
不会有视觉反馈,终端会输出一条日志。
|
||||
|
||||
|
||||
#### 将文件推送至设备
|
||||
|
||||
要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
|
||||
|
||||
不会有视觉反馈,终端会输出一条日志。
|
||||
|
||||
在启动时可以修改目标目录:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
|
||||
### 音频转发
|
||||
|
||||
_Scrcpy_ 不支持音频。请使用 [sndcpy]。
|
||||
|
||||
另见 [issue #14]。
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## 快捷键
|
||||
|
||||
在以下列表中, <kbd>MOD</kbd> 是快捷键的修饰键。
|
||||
默认是 (左) <kbd>Alt</kbd> 或 (左) <kbd>Super</kbd>。
|
||||
|
||||
您可以使用 `--shortcut-mod` 来修改。可选的按键有 `lctrl`、`rctrl`、`lalt`、`ralt`、`lsuper` 和 `rsuper`。例如:
|
||||
|
||||
```bash
|
||||
# 使用右 Ctrl 键
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# 使用左 Ctrl 键 + 左 Alt 键,或 Super 键
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> 键通常是指 <kbd>Windows</kbd> 或 <kbd>Cmd</kbd> 键。_
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| 操作 | 快捷键
|
||||
| --------------------------------- | :-------------------------------------------
|
||||
| 全屏 | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| 向左旋转屏幕 | <kbd>MOD</kbd>+<kbd>←</kbd> _(左箭头)_
|
||||
| 向右旋转屏幕 | <kbd>MOD</kbd>+<kbd>→</kbd> _(右箭头)_
|
||||
| 将窗口大小重置为1:1 (匹配像素) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| 将窗口大小重置为消除黑边 | <kbd>MOD</kbd>+<kbd>w</kbd> \| _双击左键¹_
|
||||
| 点按 `主屏幕` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _中键_
|
||||
| 点按 `返回` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _右键²_
|
||||
| 点按 `切换应用` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _第4键³_
|
||||
| 点按 `菜单` (解锁屏幕)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| 点按 `音量+` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(上箭头)_
|
||||
| 点按 `音量-` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(下箭头)_
|
||||
| 点按 `电源` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| 打开屏幕 | _鼠标右键²_
|
||||
| 关闭设备屏幕 (但继续在电脑上显示) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| 打开设备屏幕 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| 旋转设备屏幕 | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| 展开通知面板 | <kbd>MOD</kbd>+<kbd>n</kbd> \| _第5键³_
|
||||
| 展开设置面板 | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _双击第5键³_
|
||||
| 收起通知面板 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| 复制到剪贴板⁵ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| 剪切到剪贴板⁵ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| 同步剪贴板并粘贴⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| 注入电脑剪贴板文本 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| 打开/关闭FPS显示 (至标准输出) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| 捏拉缩放 | <kbd>Ctrl</kbd>+_按住并移动鼠标_
|
||||
| 拖放 APK 文件 | 从电脑安装 APK 文件
|
||||
| 拖放非 APK 文件 | [将文件推送至设备](#push-file-to-device)
|
||||
|
||||
_¹双击黑边可以去除黑边。_
|
||||
_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
|
||||
_³鼠标的第4键和第5键。_
|
||||
_⁴对于开发中的 react-native 应用程序,`MENU` 触发开发菜单。_
|
||||
_⁵需要安卓版本 Android >= 7。_
|
||||
|
||||
有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”:
|
||||
|
||||
1. 按下 <kbd>MOD</kbd> 不放。
|
||||
2. 双击 <kbd>n</kbd>。
|
||||
3. 松开 <kbd>MOD</kbd>。
|
||||
|
||||
所有的 <kbd>Ctrl</kbd>+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。
|
||||
|
||||
|
||||
## 自定义路径
|
||||
|
||||
要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`:
|
||||
|
||||
```bash
|
||||
ADB=/path/to/adb scrcpy
|
||||
```
|
||||
|
||||
要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。
|
||||
|
||||
要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。
|
||||
|
||||
|
||||
## 为什么叫 _scrcpy_ ?
|
||||
|
||||
一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。
|
||||
|
||||
[`strcpy`] 源于 **str**ing (字符串); `scrcpy` 源于 **scr**een (屏幕)。
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## 如何构建?
|
||||
|
||||
请查看 [BUILD]。
|
||||
|
||||
|
||||
## 常见问题
|
||||
|
||||
请查看 [FAQ](FAQ.md)。
|
||||
|
||||
|
||||
## 开发者
|
||||
|
||||
请查看[开发者页面]。
|
||||
|
||||
[开发者页面]: DEVELOP.md
|
||||
|
||||
|
||||
## 许可协议
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## 相关文章
|
||||
|
||||
- [Introducing scrcpy][article-intro]
|
||||
- [Scrcpy now works wirelessly][article-tcpip]
|
||||
|
||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
702
README.zh-Hant.md
Normal file
702
README.zh-Hant.md
Normal file
@@ -0,0 +1,702 @@
|
||||
_Only the original [README](README.md) is guaranteed to be up-to-date._
|
||||
|
||||
_只有原版的 [README](README.md)是保證最新的。_
|
||||
|
||||
|
||||
本文件翻譯時點: [521f2fe](https://github.com/Genymobile/scrcpy/commit/521f2fe994019065e938aa1a54b56b4f10a4ac4a#diff-04c6e90faac2675aa89e2176d2eec7d8)
|
||||
|
||||
|
||||
# scrcpy (v1.15)
|
||||
|
||||
Scrcpy 可以透過 USB、或是 [TCP/IP][article-tcpip] 來顯示或控制 Android 裝置。且 scrcpy 不需要 _root_ 權限。
|
||||
|
||||
Scrcpy 目前支援 _GNU/Linux_、_Windows_ 和 _macOS_。
|
||||
|
||||

|
||||
|
||||
特色:
|
||||
|
||||
- **輕量** (只顯示裝置螢幕)
|
||||
- **效能** (30~60fps)
|
||||
- **品質** (1920×1080 或更高)
|
||||
- **低延遲** ([35~70ms][lowlatency])
|
||||
- **快速啟動** (~1 秒就可以顯示第一個畫面)
|
||||
- **非侵入性** (不安裝、留下任何東西在裝置上)
|
||||
|
||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
||||
|
||||
|
||||
## 需求
|
||||
|
||||
Android 裝置必須是 API 21+ (Android 5.0+)。
|
||||
|
||||
請確認裝置上的 [adb 偵錯 (通常位於開發者模式內)][enable-adb] 已啟用。
|
||||
|
||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||
|
||||
|
||||
在部分的裝置上,你也必須啟用[特定的額外選項][control]才能使用鍵盤和滑鼠控制。
|
||||
|
||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||
|
||||
|
||||
## 下載/獲取軟體
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
Debian (目前支援 _testing_ 和 _sid_) 和 Ubuntu (20.04):
|
||||
|
||||
```
|
||||
apt install scrcpy
|
||||
```
|
||||
|
||||
[Snap] 上也可以下載: [`scrcpy`][snap-link].
|
||||
|
||||
[snap-link]: https://snapstats.org/snaps/scrcpy
|
||||
|
||||
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
|
||||
|
||||
在 Fedora 上也可以使用 [COPR] 下載: [`scrcpy`][copr-link].
|
||||
|
||||
[COPR]: https://fedoraproject.org/wiki/Category:Copr
|
||||
[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/
|
||||
|
||||
在 Arch Linux 上也可以使用 [AUR] 下載: [`scrcpy`][aur-link].
|
||||
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[aur-link]: https://aur.archlinux.org/packages/scrcpy/
|
||||
|
||||
在 Gentoo 上也可以使用 [Ebuild] 下載: [`scrcpy/`][ebuild-link].
|
||||
|
||||
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
你也可以自己[編譯 _Scrcpy_][BUILD]。別擔心,並沒有想像中的難。
|
||||
|
||||
|
||||
|
||||
### Windows
|
||||
|
||||
為了保持簡單,Windows 用戶可以下載一個包含所有必需軟體 (包含 `adb`) 的壓縮包:
|
||||
|
||||
- [README](README.md#windows)
|
||||
|
||||
[Chocolatey] 上也可以下載:
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # 如果你還沒有安裝的話
|
||||
```
|
||||
|
||||
[Scoop] 上也可以下載:
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # 如果你還沒有安裝的話
|
||||
```
|
||||
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
你也可以自己[編譯 _Scrcpy_][BUILD]。
|
||||
|
||||
|
||||
### macOS
|
||||
|
||||
_Scrcpy_ 可以在 [Homebrew] 上直接安裝:
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
由於執行期間需要可以藉由 `PATH` 存取 `adb` 。如果還沒有安裝 `adb` 可以使用下列方式安裝:
|
||||
|
||||
```bash
|
||||
brew cask install android-platform-tools
|
||||
```
|
||||
|
||||
你也可以自己[編譯 _Scrcpy_][BUILD]。
|
||||
|
||||
|
||||
## 執行
|
||||
|
||||
將電腦和你的 Android 裝置連線,然後執行:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
_Scrcpy_ 可以接受命令列參數。輸入下列指令就可以瀏覽可以使用的命令列參數:
|
||||
|
||||
```bash
|
||||
scrcpy --help
|
||||
```
|
||||
|
||||
|
||||
## 功能
|
||||
|
||||
> 以下說明中,有關快捷鍵的說明可能會出現 <kbd>MOD</kbd> 按鈕。相關說明請參見[快捷鍵]內的說明。
|
||||
|
||||
[快捷鍵]: #快捷鍵
|
||||
|
||||
### 畫面擷取
|
||||
|
||||
#### 縮小尺寸
|
||||
|
||||
使用比較低的解析度來投放 Android 裝置在某些情況可以提升效能。
|
||||
|
||||
限制寬和高的最大值(例如: 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size 1024
|
||||
scrcpy -m 1024 # 縮短版本
|
||||
```
|
||||
|
||||
比較小的參數會根據螢幕比例重新計算。
|
||||
根據上面的範例,1920x1080 會被縮小成 1024x576。
|
||||
|
||||
|
||||
#### 更改 bit-rate
|
||||
|
||||
預設的 bit-rate 是 8 Mbps。如果要更改 bit-rate (例如: 2 Mbps):
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M
|
||||
scrcpy -b 2M # 縮短版本
|
||||
```
|
||||
|
||||
#### 限制 FPS
|
||||
|
||||
限制畫面最高的 FPS:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps 15
|
||||
```
|
||||
|
||||
僅在 Android 10 後正式支援,不過也有可能可以在 Android 10 以前的版本使用。
|
||||
|
||||
#### 裁切
|
||||
|
||||
裝置的螢幕可以裁切。如此一來,鏡像出來的螢幕就只會是原本的一部份。
|
||||
|
||||
假如只要鏡像 Oculus Go 的其中一隻眼睛:
|
||||
|
||||
```bash
|
||||
scrcpy --crop 1224:1440:0:0 # 位於 (0,0),大小1224x1440
|
||||
```
|
||||
|
||||
如果 `--max-size` 也有指定的話,裁切後才會縮放。
|
||||
|
||||
|
||||
#### 鎖定影像方向
|
||||
|
||||
|
||||
如果要鎖定鏡像影像方向:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation 0 # 原本的方向
|
||||
scrcpy --lock-video-orientation 1 # 逆轉 90°
|
||||
scrcpy --lock-video-orientation 2 # 180°
|
||||
scrcpy --lock-video-orientation 3 # 順轉 90°
|
||||
```
|
||||
|
||||
這會影響錄影結果的影像方向。
|
||||
|
||||
|
||||
### 錄影
|
||||
|
||||
鏡像投放螢幕的同時也可以錄影:
|
||||
|
||||
```bash
|
||||
scrcpy --record file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
如果只要錄影,不要投放螢幕鏡像的話:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# 用 Ctrl+C 停止錄影
|
||||
```
|
||||
|
||||
就算有些幀為了效能而被跳過,它們還是一樣會被錄製。
|
||||
|
||||
裝置上的每一幀都有時間戳記,所以 [封包延遲 (Packet Delay Variation, PDV)][packet delay variation] 並不會影響錄影的檔案。
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
|
||||
### 連線
|
||||
|
||||
#### 無線
|
||||
|
||||
_Scrcpy_ 利用 `adb` 和裝置通訊,而 `adb` 可以[透過 TCP/IP 連結][connect]:
|
||||
|
||||
1. 讓電腦和裝置連到同一個 Wi-Fi。
|
||||
2. 獲取手機的 IP 位址(設定 → 關於手機 → 狀態).
|
||||
3. 啟用裝置上的 `adb over TCP/IP`: `adb tcpip 5555`.
|
||||
4. 拔掉裝置上的線。
|
||||
5. 透過 TCP/IP 連接裝置: `adb connect DEVICE_IP:5555` _(把 `DEVICE_IP` 換成裝置的IP位址)_.
|
||||
6. 和平常一樣執行 `scrcpy`。
|
||||
|
||||
如果效能太差,可以降低 bit-rate 和解析度:
|
||||
|
||||
```bash
|
||||
scrcpy --bit-rate 2M --max-size 800
|
||||
scrcpy -b2M -m800 # 縮短版本
|
||||
```
|
||||
|
||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
#### 多裝置
|
||||
|
||||
如果 `adb devices` 內有多個裝置,則必須附上 _serial_:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # 縮短版本
|
||||
```
|
||||
|
||||
如果裝置是透過 TCP/IP 連線:
|
||||
|
||||
```bash
|
||||
scrcpy --serial 192.168.0.1:5555
|
||||
scrcpy -s 192.168.0.1:5555 # 縮短版本
|
||||
```
|
||||
|
||||
你可以啟用復數個對應不同裝置的 _scrcpy_。
|
||||
|
||||
#### 裝置連結後自動啟動
|
||||
|
||||
你可以使用 [AutoAdb]:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
#### SSH tunnel
|
||||
|
||||
本地的 `adb` 可以連接到遠端的 `adb` 伺服器(假設兩者使用相同版本的 _adb_ 通訊協定),以連接到遠端裝置:
|
||||
|
||||
```bash
|
||||
adb kill-server # 停止在 Port 5037 的本地 adb 伺服
|
||||
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# 保持開啟
|
||||
```
|
||||
|
||||
從另外一個終端機:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
如果要避免啟用 remote port forwarding,你可以強制它使用 forward connection (注意 `-L` 和 `-R` 的差別):
|
||||
|
||||
```bash
|
||||
adb kill-server # 停止在 Port 5037 的本地 adb 伺服
|
||||
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# 保持開啟
|
||||
```
|
||||
|
||||
從另外一個終端機:
|
||||
|
||||
```bash
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
|
||||
和無線連接一樣,有時候降低品質會比較好:
|
||||
|
||||
```
|
||||
scrcpy -b2M -m800 --max-fps 15
|
||||
```
|
||||
|
||||
### 視窗調整
|
||||
|
||||
#### 標題
|
||||
|
||||
預設標題是裝置的型號,不過可以透過以下方式修改:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title 'My device'
|
||||
```
|
||||
|
||||
#### 位置 & 大小
|
||||
|
||||
初始的視窗位置和大小也可以指定:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
|
||||
```
|
||||
|
||||
#### 無邊框
|
||||
|
||||
如果要停用視窗裝飾:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
#### 保持最上層
|
||||
|
||||
如果要保持 `scrcpy` 的視窗在最上層:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
#### 全螢幕
|
||||
|
||||
這個軟體可以直接在全螢幕模式下起動:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # 縮短版本
|
||||
```
|
||||
|
||||
全螢幕可以使用 <kbd>MOD</kbd>+<kbd>f</kbd> 開關。
|
||||
|
||||
#### 旋轉
|
||||
|
||||
視窗可以旋轉:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation 1
|
||||
```
|
||||
|
||||
可用的數值:
|
||||
- `0`: 不旋轉
|
||||
- `1`: 90 度**逆**轉
|
||||
- `2`: 180 度
|
||||
- `3`: 90 度**順**轉
|
||||
|
||||
旋轉方向也可以使用 <kbd>MOD</kbd>+<kbd>←</kbd> _(左方向鍵)_ 和 <kbd>MOD</kbd>+<kbd>→</kbd> _(右方向鍵)_ 調整。
|
||||
|
||||
_scrcpy_ 有 3 種不同的旋轉:
|
||||
- <kbd>MOD</kbd>+<kbd>r</kbd> 要求裝置在垂直、水平之間旋轉 (目前運行中的 App 有可能會因為不支援而拒絕)。
|
||||
- `--lock-video-orientation` 修改鏡像的方向 (裝置傳給電腦的影像)。這會影響錄影結果的影像方向。
|
||||
- `--rotation` (或是 <kbd>MOD</kbd>+<kbd>←</kbd> / <kbd>MOD</kbd>+<kbd>→</kbd>) 只旋轉視窗的內容。這只會影響鏡像結果,不會影響錄影結果。
|
||||
|
||||
|
||||
### 其他鏡像選項
|
||||
|
||||
#### 唯讀
|
||||
|
||||
停用所有控制,包含鍵盤輸入、滑鼠事件、拖放檔案:
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n
|
||||
```
|
||||
|
||||
#### 顯示螢幕
|
||||
|
||||
如果裝置有複數個螢幕,可以指定要鏡像哪個螢幕:
|
||||
|
||||
```bash
|
||||
scrcpy --display 1
|
||||
```
|
||||
|
||||
可以透過下列指令獲取螢幕 ID:
|
||||
|
||||
```
|
||||
adb shell dumpsys display # 找輸出結果中的 "mDisplayId="
|
||||
```
|
||||
|
||||
第二螢幕只有在 Android 10+ 時可以控制。如果不是 Android 10+,螢幕就會在唯讀狀態下投放。
|
||||
|
||||
|
||||
#### 保持清醒
|
||||
|
||||
如果要避免裝置在連接狀態下進入睡眠:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
_scrcpy_ 關閉後就會回復成原本的設定。
|
||||
|
||||
|
||||
#### 關閉螢幕
|
||||
|
||||
鏡像開始時,可以要求裝置關閉螢幕:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S
|
||||
```
|
||||
|
||||
或是在任何時候輸入 <kbd>MOD</kbd>+<kbd>o</kbd>。
|
||||
|
||||
如果要開啟螢幕,輸入 <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>。
|
||||
|
||||
在 Android 上,`POWER` 按鈕總是開啟螢幕。
|
||||
|
||||
為了方便,如果 `POWER` 是透過 scrcpy 轉送 (右鍵 或 <kbd>MOD</kbd>+<kbd>p</kbd>)的話,螢幕將會在短暫的延遲後關閉。
|
||||
|
||||
實際在手機上的 `POWER` 還是會開啟螢幕。
|
||||
|
||||
防止裝置進入睡眠狀態:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw
|
||||
```
|
||||
|
||||
|
||||
#### 顯示過期的幀
|
||||
|
||||
為了降低延遲, _scrcpy_ 預設只會顯示最後解碼的幀,並且拋棄所有在這之前的幀。
|
||||
|
||||
如果要強制顯示所有的幀 (有可能會拉高延遲),輸入:
|
||||
|
||||
```bash
|
||||
scrcpy --render-expired-frames
|
||||
```
|
||||
|
||||
#### 顯示觸控點
|
||||
|
||||
對於要報告的人來說,顯示裝置上的實際觸控點有時候是有幫助的。
|
||||
|
||||
Android 在_開發者選項_中有提供這個功能。
|
||||
|
||||
_Scrcpy_ 可以在啟動時啟用這個功能,並且在停止後恢復成原本的設定:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t
|
||||
```
|
||||
|
||||
這個選項只會顯示**實際觸碰在裝置上的觸碰點**。
|
||||
|
||||
|
||||
### 輸入控制
|
||||
|
||||
|
||||
#### 旋轉裝置螢幕
|
||||
|
||||
輸入 <kbd>MOD</kbd>+<kbd>r</kbd> 以在垂直、水平之間切換。
|
||||
|
||||
如果使用中的程式不支援,則不會切換。
|
||||
|
||||
|
||||
#### 複製/貼上
|
||||
|
||||
如果 Android 剪貼簿上的內容有任何更動,電腦的剪貼簿也會一起更動。
|
||||
|
||||
任何與 <kbd>Ctrl</kbd> 相關的快捷鍵事件都會轉送到裝置上。特別來說:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> 通常是複製
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> 通常是剪下
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> 通常是貼上 (在電腦的剪貼簿與裝置上的剪貼簿同步之後)
|
||||
|
||||
這些跟你通常預期的行為一樣。
|
||||
|
||||
但是,實際上的行為是根據目前運行中的應用程式而定。
|
||||
|
||||
舉例來說, _Termux_ 在收到 <kbd>Ctrl</kbd>+<kbd>c</kbd> 後,會傳送 SIGINT;而 _K-9 Mail_ 則是建立新訊息。
|
||||
|
||||
如果在這情況下,要剪下、複製或貼上 (只有在Android 7+時才支援):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> 注入 `複製`
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> 注入 `剪下`
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> 注入 `貼上` (在電腦的剪貼簿與裝置上的剪貼簿同步之後)
|
||||
|
||||
另外,<kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> 則是以一連串的按鍵事件貼上電腦剪貼簿中的內容。當元件不允許文字貼上 (例如 _Termux_) 時,這就很有用。不過,這在非 ASCII 內容上就無法使用。
|
||||
|
||||
**警告:** 貼上電腦的剪貼簿內容 (無論是從 <kbd>Ctrl</kbd>+<kbd>v</kbd> 或 <kbd>MOD</kbd>+<kbd>v</kbd>) 時,會複製剪貼簿中的內容至裝置的剪貼簿上。這會讓所有 Android 程式讀取剪貼簿的內容。請避免貼上任何敏感內容 (像是密碼)。
|
||||
|
||||
|
||||
#### 文字輸入偏好
|
||||
|
||||
輸入文字時,有兩種[事件][textevents]會被觸發:
|
||||
- _鍵盤事件 (key events)_,代表有一個按鍵被按下或放開
|
||||
- _文字事件 (text events)_,代表有一個文字被輸入
|
||||
|
||||
預設上,文字是被以鍵盤事件 (key events) 輸入的,所以鍵盤和遊戲內所預期的一樣 (通常是指 WASD)。
|
||||
|
||||
但是這可能造成[一些問題][prefertext]。如果在這輸入這方面遇到了問題,你可以試試:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(不過遊戲內鍵盤就會不可用)
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
#### 重複輸入
|
||||
|
||||
通常來說,長時間按住一個按鍵會重複觸發按鍵事件。這會在一些遊戲中造成效能問題,而且這個重複的按鍵事件是沒有意義的。
|
||||
|
||||
如果不要轉送這些重複的按鍵事件:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
|
||||
### 檔案
|
||||
|
||||
#### 安裝 APK
|
||||
|
||||
如果要安裝 APK ,拖放一個 APK 檔案 (以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。
|
||||
|
||||
視窗上不會有任何反饋;結果會顯示在命令列中。
|
||||
|
||||
|
||||
#### 推送檔案至裝置
|
||||
|
||||
如果要推送檔案到裝置上的 `/sdcard/` ,拖放一個非 APK 檔案 (**不**以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。
|
||||
|
||||
視窗上不會有任何反饋;結果會顯示在命令列中。
|
||||
|
||||
推送檔案的目標路徑可以在啟動時指定:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target /sdcard/foo/bar/
|
||||
```
|
||||
|
||||
|
||||
### 音訊轉送
|
||||
|
||||
_scrcpy_ **不**轉送音訊。請使用 [sndcpy]。另外,參見 [issue #14]。
|
||||
|
||||
[sndcpy]: https://github.com/rom1v/sndcpy
|
||||
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
|
||||
|
||||
## 快捷鍵
|
||||
|
||||
在以下的清單中,<kbd>MOD</kbd> 是快捷鍵的特殊按鍵。通常來說,這個按鍵是 (左) <kbd>Alt</kbd> 或是 (左) <kbd>Super</kbd>。
|
||||
|
||||
這個是可以使用 `--shortcut-mod` 更改的。可以用的選項有:
|
||||
- `lctrl`: 左邊的 <kbd>Ctrl</kbd>
|
||||
- `rctrl`: 右邊的 <kbd>Ctrl</kbd>
|
||||
- `lalt`: 左邊的 <kbd>Alt</kbd>
|
||||
- `ralt`: 右邊的 <kbd>Alt</kbd>
|
||||
- `lsuper`: 左邊的 <kbd>Super</kbd>
|
||||
- `rsuper`: 右邊的 <kbd>Super</kbd>
|
||||
|
||||
```bash
|
||||
# 以 右邊的 Ctrl 為快捷鍵特殊按鍵
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# 以 左邊的 Ctrl 和左邊的 Alt 或是 左邊的 Super 為快捷鍵特殊按鍵
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> 通常是 <kbd>Windows</kbd> 或 <kbd>Cmd</kbd> 鍵。_
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Action | Shortcut
|
||||
| ------------------------------------------------- |:-----------------------------
|
||||
| 切換至全螢幕 | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| 左旋顯示螢幕 | <kbd>MOD</kbd>+<kbd>←</kbd> _(左)_
|
||||
| 右旋顯示螢幕 | <kbd>MOD</kbd>+<kbd>→</kbd> _(右)_
|
||||
| 縮放視窗成 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| 縮放視窗到沒有黑邊框為止 | <kbd>MOD</kbd>+<kbd>w</kbd> \| _雙擊¹_
|
||||
| 按下 `首頁` 鍵 | <kbd>MOD</kbd>+<kbd>h</kbd> \| _中鍵_
|
||||
| 按下 `返回` 鍵 | <kbd>MOD</kbd>+<kbd>b</kbd> \| _右鍵²_
|
||||
| 按下 `切換 APP` 鍵 | <kbd>MOD</kbd>+<kbd>s</kbd>
|
||||
| 按下 `選單` 鍵 (或解鎖螢幕) | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| 按下 `音量+` 鍵 | <kbd>MOD</kbd>+<kbd>↑</kbd> _(上)_
|
||||
| 按下 `音量-` 鍵 | <kbd>MOD</kbd>+<kbd>↓</kbd> _(下)_
|
||||
| 按下 `電源` 鍵 | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| 開啟 | _右鍵²_
|
||||
| 關閉裝置螢幕(持續鏡像) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| 開啟裝置螢幕 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| 旋轉裝置螢幕 | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| 開啟通知列 | <kbd>MOD</kbd>+<kbd>n</kbd>
|
||||
| 關閉通知列 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| 複製至剪貼簿³ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| 剪下至剪貼簿³ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| 同步剪貼簿並貼上³ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| 複製電腦剪貼簿中的文字至裝置並貼上 | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| 啟用/停用 FPS 計數器(顯示於 stdout - 通常是命令列) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
|
||||
_¹在黑邊框上雙擊以移除它們。_
|
||||
_²右鍵會返回。如果螢幕是關閉狀態,則會打開螢幕。_
|
||||
_³只支援 Android 7+。_
|
||||
|
||||
所有 <kbd>Ctrl</kbd>+_按鍵_ 快捷鍵都會傳送到裝置上,所以它們是由目前運作的應用程式處理的。
|
||||
|
||||
|
||||
## 自訂路徑
|
||||
|
||||
如果要使用特定的 _adb_ ,將它設定到環境變數中的 `ADB`:
|
||||
|
||||
ADB=/path/to/adb scrcpy
|
||||
|
||||
如果要覆寫 `scrcpy-server` 檔案的路徑,則將路徑設定到環境變數中的 `SCRCPY_SERVER_PATH`。
|
||||
|
||||
[相關連結][useful]
|
||||
|
||||
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
|
||||
|
||||
|
||||
## 為何叫 _scrcpy_ ?
|
||||
|
||||
有一個同事要我找一個跟 [gnirehtet] 一樣難念的名字。
|
||||
|
||||
[`strcpy`] 複製一個字串 (**str**ing);`scrcpy` 複製一個螢幕 (**scr**een)。
|
||||
|
||||
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||
|
||||
|
||||
## 如何編譯?
|
||||
|
||||
請看[這份文件 (英文)][BUILD]。
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
|
||||
## 常見問題
|
||||
|
||||
請看[這份文件 (英文)][FAQ]。
|
||||
|
||||
[FAQ]: FAQ.md
|
||||
|
||||
|
||||
## 開發者文件
|
||||
|
||||
請看[這個頁面 (英文)][developers page].
|
||||
|
||||
[developers page]: DEVELOP.md
|
||||
|
||||
|
||||
## Licence
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
## 相關文章
|
||||
|
||||
- [Scrcpy 簡介 (英文)][article-intro]
|
||||
- [Scrcpy 可以無線連線了 (英文)][article-tcpip]
|
||||
|
||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
||||
@@ -21,9 +21,9 @@ src = [
|
||||
'src/mouse_inject.c',
|
||||
'src/opengl.c',
|
||||
'src/options.c',
|
||||
'src/packet_merger.c',
|
||||
'src/receiver.c',
|
||||
'src/recorder.c',
|
||||
'src/rtp.c',
|
||||
'src/scrcpy.c',
|
||||
'src/screen.c',
|
||||
'src/server.c',
|
||||
@@ -38,7 +38,6 @@ src = [
|
||||
'src/util/net_intr.c',
|
||||
'src/util/process.c',
|
||||
'src/util/process_intr.c',
|
||||
'src/util/rand.c',
|
||||
'src/util/strbuf.c',
|
||||
'src/util/str.c',
|
||||
'src/util/term.c',
|
||||
@@ -172,8 +171,6 @@ check_functions = [
|
||||
'strdup',
|
||||
'asprintf',
|
||||
'vasprintf',
|
||||
'nrand48',
|
||||
'jrand48',
|
||||
]
|
||||
|
||||
foreach f : check_functions
|
||||
@@ -201,6 +198,10 @@ conf.set('PORTABLE', get_option('portable'))
|
||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
||||
|
||||
# the default video bitrate, in bits/second
|
||||
# overridden by option --bit-rate
|
||||
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||
|
||||
# run a server debugger and wait for a client to be attached
|
||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
DEP_DIR=platform-tools-33.0.3
|
||||
DEP_DIR=platform-tools-33.0.1
|
||||
|
||||
FILENAME=platform-tools_r33.0.3-windows.zip
|
||||
SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
|
||||
FILENAME=platform-tools_r33.0.1-windows.zip
|
||||
SHA256SUM=c1f02d42ea24ef4ff2a405ae7370e764ef4546f9b3e4520f5571a00ed5012c42
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
||||
@@ -6,11 +6,11 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
VERSION=5.1.2
|
||||
VERSION=5.0.1
|
||||
DEP_DIR=ffmpeg-win64-$VERSION
|
||||
|
||||
FILENAME=ffmpeg-$VERSION-full_build-shared.7z
|
||||
SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9
|
||||
SHA256SUM=ded28435b6f04b74f5ef5a6a13761233bce9e8e9f8ecb0eabe936fd36a778b0c
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
||||
@@ -6,10 +6,10 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
DEP_DIR=SDL2-2.26.1
|
||||
DEP_DIR=SDL2-2.0.22
|
||||
|
||||
FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
|
||||
SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
|
||||
FILENAME=SDL2-devel-2.0.22-mingw.tar.gz
|
||||
SHA256SUM=0e91e35973366aa1e6f81ee368924d9b4f93f9da4d2f2a89ec80b06eadcf23d1
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
||||
@@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "1.25"
|
||||
VALUE "ProductVersion", "1.24"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
36
app/scrcpy.1
36
app/scrcpy.1
@@ -19,36 +19,14 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
||||
.B \-\-always\-on\-top
|
||||
Make scrcpy window always on top (above other windows).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-bit\-rate " value
|
||||
Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 196K (196000).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-codec " name
|
||||
Select an audio codec (opus or aac).
|
||||
|
||||
Default is opus.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-encoder " name
|
||||
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
||||
|
||||
.TP
|
||||
.BI "\-b, \-\-bit\-rate " value
|
||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 8M (8000000).
|
||||
Default is 8000000.
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec " name
|
||||
Select a video codec (h264, h265 or av1).
|
||||
|
||||
Default is h264.
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
||||
.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'.
|
||||
@@ -98,7 +76,7 @@ Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
|
||||
|
||||
.TP
|
||||
.BI "\-\-encoder " name
|
||||
Use a specific MediaCodec encoder (depending on the codec provided by \fB\-\-codec\fR).
|
||||
Use a specific MediaCodec encoder (must be a H.264 encoder).
|
||||
|
||||
.TP
|
||||
.B \-\-force\-adb\-forward
|
||||
@@ -139,7 +117,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
|
||||
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
||||
|
||||
.TP
|
||||
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
||||
.BI "\-\-lock\-video\-orientation[=value]
|
||||
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
|
||||
|
||||
Default is "unlocked".
|
||||
@@ -221,7 +199,7 @@ It may only work over USB.
|
||||
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
|
||||
.BI "\-p, \-\-port " port[:port]
|
||||
Set the TCP port (range) used by the client to listen.
|
||||
|
||||
Default is 27183:27199.
|
||||
@@ -282,7 +260,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
|
||||
The device serial number. Mandatory only if several devices are connected to adb.
|
||||
|
||||
.TP
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
.BI "\-\-shortcut\-mod " key[+...]][,...]
|
||||
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
|
||||
|
||||
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
|
||||
@@ -292,7 +270,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
|
||||
Default is "lalt,lsuper" (left-Alt or left-Super).
|
||||
|
||||
.TP
|
||||
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
|
||||
.BI "\-\-tcpip[=ip[:port]]
|
||||
Configure and reconnect the device over TCP/IP.
|
||||
|
||||
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "util/net_intr.h"
|
||||
#include "util/process_intr.h"
|
||||
|
||||
#define SC_SOCKET_NAME "scrcpy"
|
||||
|
||||
static bool
|
||||
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
||||
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
|
||||
@@ -15,11 +17,10 @@ listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
||||
static bool
|
||||
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
||||
struct sc_intr *intr, const char *serial,
|
||||
const char *device_socket_name,
|
||||
struct sc_port_range port_range) {
|
||||
uint16_t port = port_range.first;
|
||||
for (;;) {
|
||||
if (!sc_adb_reverse(intr, serial, device_socket_name, port,
|
||||
if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port,
|
||||
SC_ADB_NO_STDOUT)) {
|
||||
// the command itself failed, it will fail on any port
|
||||
return false;
|
||||
@@ -51,7 +52,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
||||
}
|
||||
|
||||
// failure, disable tunnel and try another port
|
||||
if (!sc_adb_reverse_remove(intr, serial, device_socket_name,
|
||||
if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
|
||||
SC_ADB_NO_STDOUT)) {
|
||||
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
|
||||
}
|
||||
@@ -77,13 +78,12 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
||||
static bool
|
||||
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
|
||||
struct sc_intr *intr, const char *serial,
|
||||
const char *device_socket_name,
|
||||
struct sc_port_range port_range) {
|
||||
tunnel->forward = true;
|
||||
|
||||
uint16_t port = port_range.first;
|
||||
for (;;) {
|
||||
if (sc_adb_forward(intr, serial, port, device_socket_name,
|
||||
if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME,
|
||||
SC_ADB_NO_STDOUT)) {
|
||||
// success
|
||||
tunnel->local_port = port;
|
||||
@@ -123,14 +123,13 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
|
||||
|
||||
bool
|
||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
const char *serial, const char *device_socket_name,
|
||||
struct sc_port_range port_range, bool force_adb_forward) {
|
||||
const char *serial, struct sc_port_range port_range,
|
||||
bool force_adb_forward) {
|
||||
assert(!tunnel->enabled);
|
||||
|
||||
if (!force_adb_forward) {
|
||||
// Attempt to use "adb reverse"
|
||||
if (enable_tunnel_reverse_any_port(tunnel, intr, serial,
|
||||
device_socket_name, port_range)) {
|
||||
if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,13 +139,12 @@ sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||
}
|
||||
|
||||
return enable_tunnel_forward_any_port(tunnel, intr, serial,
|
||||
device_socket_name, port_range);
|
||||
return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
const char *serial, const char *device_socket_name) {
|
||||
const char *serial) {
|
||||
assert(tunnel->enabled);
|
||||
|
||||
bool ret;
|
||||
@@ -154,7 +152,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
|
||||
SC_ADB_NO_STDOUT);
|
||||
} else {
|
||||
ret = sc_adb_reverse_remove(intr, serial, device_socket_name,
|
||||
ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
|
||||
SC_ADB_NO_STDOUT);
|
||||
|
||||
assert(tunnel->server_socket != SC_SOCKET_NONE);
|
||||
|
||||
@@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
|
||||
*/
|
||||
bool
|
||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
const char *serial, const char *device_socket_name,
|
||||
struct sc_port_range port_range, bool force_adb_forward);
|
||||
const char *serial, struct sc_port_range port_range,
|
||||
bool force_adb_forward);
|
||||
|
||||
/**
|
||||
* Close the tunnel
|
||||
*/
|
||||
bool
|
||||
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
||||
const char *serial, const char *device_socket_name);
|
||||
const char *serial);
|
||||
|
||||
#endif
|
||||
|
||||
100
app/src/cli.c
100
app/src/cli.c
@@ -57,11 +57,6 @@
|
||||
#define OPT_NO_CLEANUP 1037
|
||||
#define OPT_PRINT_FPS 1038
|
||||
#define OPT_NO_POWER_ON 1039
|
||||
#define OPT_VIDEO_CODEC 1040
|
||||
#define OPT_NO_AUDIO 1041
|
||||
#define OPT_AUDIO_BIT_RATE 1042
|
||||
#define OPT_AUDIO_CODEC 1043
|
||||
#define OPT_AUDIO_ENCODER_NAME 1044
|
||||
|
||||
struct sc_option {
|
||||
char shortopt;
|
||||
@@ -102,43 +97,13 @@ static const struct sc_option options[] = {
|
||||
.longopt = "always-on-top",
|
||||
.text = "Make scrcpy window always on top (above other windows).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||
.longopt = "audio-bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is 196K (196000).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_CODEC,
|
||||
.longopt = "audio-codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select an audio codec (opus or aac).\n"
|
||||
"Default is opus.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_ENCODER_NAME,
|
||||
.longopt = "audio-encoder",
|
||||
.argdesc = "name",
|
||||
.text = "Use a specific MediaCodec audio encoder (depending on the "
|
||||
"codec provided by --audio-codec).",
|
||||
},
|
||||
{
|
||||
.shortopt = 'b',
|
||||
.longopt = "bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
|
||||
.text = "Encode the video at the gitven bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is 8M (8000000).",
|
||||
},
|
||||
// TODO keep OPT_CODEC to avoid partial matching with codec_options
|
||||
{
|
||||
.longopt_id = OPT_VIDEO_CODEC,
|
||||
.longopt = "video-codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select a video codec (h264, h265 or av1).\n"
|
||||
"Default is h264.",
|
||||
"Default is " STR(DEFAULT_BIT_RATE) ".",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CODEC_OPTIONS,
|
||||
@@ -200,8 +165,7 @@ static const struct sc_option options[] = {
|
||||
.longopt_id = OPT_ENCODER_NAME,
|
||||
.longopt = "encoder",
|
||||
.argdesc = "name",
|
||||
.text = "Use a specific MediaCodec encoder (depending on the codec "
|
||||
"provided by --codec).",
|
||||
.text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_FORCE_ADB_FORWARD,
|
||||
@@ -327,11 +291,6 @@ static const struct sc_option options[] = {
|
||||
.text = "Do not display device (only when screen recording or V4L2 "
|
||||
"sink is enabled).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_AUDIO,
|
||||
.longopt = "no-audio",
|
||||
.text = "Disable audio forwarding.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_KEY_REPEAT,
|
||||
.longopt = "no-key-repeat",
|
||||
@@ -1418,38 +1377,6 @@ guess_record_format(const char *filename) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_video_codec(const char *optarg, enum sc_codec *codec) {
|
||||
if (!strcmp(optarg, "h264")) {
|
||||
*codec = SC_CODEC_H264;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "h265")) {
|
||||
*codec = SC_CODEC_H265;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "av1")) {
|
||||
*codec = SC_CODEC_AV1;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
||||
if (!strcmp(optarg, "opus")) {
|
||||
*codec = SC_CODEC_OPUS;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "aac")) {
|
||||
*codec = SC_CODEC_AAC;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported audio codec: %s (expected opus)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
const char *optstring, const struct option *longopts) {
|
||||
@@ -1465,11 +1392,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_BIT_RATE:
|
||||
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_CROP:
|
||||
opts->crop = optarg;
|
||||
break;
|
||||
@@ -1644,9 +1566,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_ENCODER_NAME:
|
||||
opts->encoder_name = optarg;
|
||||
break;
|
||||
case OPT_AUDIO_ENCODER_NAME:
|
||||
opts->audio_encoder_name = optarg;
|
||||
break;
|
||||
case OPT_FORCE_ADB_FORWARD:
|
||||
opts->force_adb_forward = true;
|
||||
break;
|
||||
@@ -1682,9 +1601,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_NO_DOWNSIZE_ON_ERROR:
|
||||
opts->downsize_on_error = false;
|
||||
break;
|
||||
case OPT_NO_AUDIO:
|
||||
opts->audio = false;
|
||||
break;
|
||||
case OPT_NO_CLEANUP:
|
||||
opts->cleanup = false;
|
||||
break;
|
||||
@@ -1694,16 +1610,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_PRINT_FPS:
|
||||
opts->start_fps_counter = true;
|
||||
break;
|
||||
case OPT_VIDEO_CODEC:
|
||||
if (!parse_video_codec(optarg, &opts->video_codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_CODEC:
|
||||
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_OTG:
|
||||
#ifdef HAVE_USB
|
||||
opts->otg = true;
|
||||
|
||||
@@ -51,47 +51,3 @@ int vasprintf(char **strp, const char *fmt, va_list ap) {
|
||||
return len;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48)
|
||||
#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits
|
||||
#define SC_RAND48_A UINT64_C(0x5DEECE66D)
|
||||
#define SC_RAND48_C 0xB
|
||||
static inline uint64_t rand_iter48(uint64_t x) {
|
||||
assert((x & ~SC_RAND48_MASK) == 0);
|
||||
return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK;
|
||||
}
|
||||
|
||||
static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) {
|
||||
uint64_t x = ((uint64_t) xsubi[0] << 32)
|
||||
| ((uint64_t) xsubi[1] << 16)
|
||||
| xsubi[2];
|
||||
|
||||
x = rand_iter48(x);
|
||||
|
||||
xsubi[0] = (x >> 32) & 0XFFFF;
|
||||
xsubi[1] = (x >> 16) & 0XFFFF;
|
||||
xsubi[2] = x & 0XFFFF;
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
#ifndef HAVE_NRAND48
|
||||
long nrand48(unsigned short xsubi[3]) {
|
||||
// range [0, 2^31)
|
||||
return rand_iter48_xsubi(xsubi) >> 17;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_JRAND48
|
||||
long jrand48(unsigned short xsubi[3]) {
|
||||
// range [-2^31, 2^31)
|
||||
union {
|
||||
uint32_t u;
|
||||
int32_t i;
|
||||
} v;
|
||||
v.u = rand_iter48_xsubi(xsubi) >> 16;
|
||||
return v.i;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -59,12 +59,4 @@ int asprintf(char **strp, const char *fmt, ...);
|
||||
int vasprintf(char **strp, const char *fmt, va_list ap);
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_NRAND48
|
||||
long nrand48(unsigned short xsubi[3]);
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_JRAND48
|
||||
long jrand48(unsigned short xsubi[3]);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -61,22 +61,6 @@ static const char *const copy_key_labels[] = {
|
||||
"cut",
|
||||
};
|
||||
|
||||
static inline const char *
|
||||
get_well_known_pointer_id_name(uint64_t pointer_id) {
|
||||
switch (pointer_id) {
|
||||
case POINTER_ID_MOUSE:
|
||||
return "mouse";
|
||||
case POINTER_ID_GENERIC_FINGER:
|
||||
return "finger";
|
||||
case POINTER_ID_VIRTUAL_MOUSE:
|
||||
return "vmouse";
|
||||
case POINTER_ID_VIRTUAL_FINGER:
|
||||
return "vfinger";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
write_position(uint8_t *buf, const struct sc_position *position) {
|
||||
sc_write32be(&buf[0], position->point.x);
|
||||
@@ -117,9 +101,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
|
||||
uint16_t pressure =
|
||||
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
||||
sc_write16be(&buf[22], pressure);
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
||||
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
||||
return 32;
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
|
||||
return 28;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||
int16_t hscroll =
|
||||
@@ -176,29 +159,25 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
int action = msg->inject_touch_event.action
|
||||
& AMOTION_EVENT_ACTION_MASK;
|
||||
uint64_t id = msg->inject_touch_event.pointer_id;
|
||||
const char *pointer_name = get_well_known_pointer_id_name(id);
|
||||
if (pointer_name) {
|
||||
if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) {
|
||||
// string pointer id
|
||||
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
||||
" pressure=%f action_button=%06lx buttons=%06lx",
|
||||
pointer_name,
|
||||
" pressure=%f buttons=%06lx",
|
||||
id == POINTER_ID_MOUSE ? "mouse" : "vfinger",
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
} else {
|
||||
// numeric pointer id
|
||||
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
||||
PRIi32 " pressure=%f action_button=%06lx"
|
||||
" buttons=%06lx",
|
||||
PRIi32 " pressure=%f buttons=%06lx",
|
||||
id,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -18,11 +18,7 @@
|
||||
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
|
||||
|
||||
#define POINTER_ID_MOUSE UINT64_C(-1)
|
||||
#define POINTER_ID_GENERIC_FINGER UINT64_C(-2)
|
||||
|
||||
// Used for injecting an additional virtual pointer for pinch-to-zoom
|
||||
#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3)
|
||||
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4)
|
||||
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2)
|
||||
|
||||
enum sc_control_msg_type {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
||||
@@ -65,7 +61,6 @@ struct sc_control_msg {
|
||||
} inject_text;
|
||||
struct {
|
||||
enum android_motionevent_action action;
|
||||
enum android_motionevent_buttons action_button;
|
||||
enum android_motionevent_buttons buttons;
|
||||
uint64_t pointer_id;
|
||||
struct sc_position position;
|
||||
|
||||
@@ -9,20 +9,20 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||
struct sc_acksync *acksync) {
|
||||
cbuf_init(&controller->queue);
|
||||
|
||||
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
||||
bool ok = receiver_init(&controller->receiver, control_socket, acksync);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_mutex_init(&controller->mutex);
|
||||
if (!ok) {
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
receiver_destroy(&controller->receiver);
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&controller->msg_cond);
|
||||
if (!ok) {
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_mutex_destroy(&controller->mutex);
|
||||
return false;
|
||||
}
|
||||
@@ -43,7 +43,7 @@ sc_controller_destroy(struct sc_controller *controller) {
|
||||
sc_control_msg_destroy(&msg);
|
||||
}
|
||||
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
receiver_destroy(&controller->receiver);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -117,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_receiver_start(&controller->receiver)) {
|
||||
if (!receiver_start(&controller->receiver)) {
|
||||
sc_controller_stop(controller);
|
||||
sc_thread_join(&controller->thread, NULL);
|
||||
return false;
|
||||
@@ -137,5 +137,5 @@ sc_controller_stop(struct sc_controller *controller) {
|
||||
void
|
||||
sc_controller_join(struct sc_controller *controller) {
|
||||
sc_thread_join(&controller->thread, NULL);
|
||||
sc_receiver_join(&controller->receiver);
|
||||
receiver_join(&controller->receiver);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ struct sc_controller {
|
||||
sc_cond msg_cond;
|
||||
bool stopped;
|
||||
struct sc_control_msg_queue queue;
|
||||
struct sc_receiver receiver;
|
||||
struct receiver receiver;
|
||||
};
|
||||
|
||||
bool
|
||||
|
||||
@@ -29,6 +29,7 @@ sc_decoder_open_sinks(struct sc_decoder *decoder) {
|
||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||
if (!sink->ops->open(sink)) {
|
||||
LOGE("Could not open frame sink %d", i);
|
||||
sc_decoder_close_first_sinks(decoder, i);
|
||||
return false;
|
||||
}
|
||||
@@ -62,6 +63,7 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
||||
}
|
||||
|
||||
if (!sc_decoder_open_sinks(decoder)) {
|
||||
LOGE("Could not open decoder sinks");
|
||||
av_frame_free(&decoder->frame);
|
||||
avcodec_close(decoder->codec_ctx);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
@@ -84,6 +86,7 @@ push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
|
||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||
if (!sink->ops->push(sink, frame)) {
|
||||
LOGE("Could not send frame to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "decoder.h"
|
||||
#include "events.h"
|
||||
#include "packet_merger.h"
|
||||
#include "recorder.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/log.h"
|
||||
@@ -18,42 +17,6 @@
|
||||
|
||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
||||
|
||||
static enum AVCodecID
|
||||
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
||||
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
||||
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||
switch (codec_id) {
|
||||
case SC_CODEC_ID_H264:
|
||||
return AV_CODEC_ID_H264;
|
||||
case SC_CODEC_ID_H265:
|
||||
return AV_CODEC_ID_HEVC;
|
||||
case SC_CODEC_ID_AV1:
|
||||
return AV_CODEC_ID_AV1;
|
||||
case SC_CODEC_ID_OPUS:
|
||||
return AV_CODEC_ID_OPUS;
|
||||
case SC_CODEC_ID_AAC:
|
||||
return AV_CODEC_ID_AAC;
|
||||
default:
|
||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||
return AV_CODEC_ID_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
||||
uint8_t data[4];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 4);
|
||||
if (r < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*codec_id = sc_read32be(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
// The video stream contains raw packets, without time information. When we
|
||||
@@ -117,6 +80,7 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
|
||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||
if (!sink->ops->push(sink, packet)) {
|
||||
LOGE("Could not send config packet to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -126,9 +90,52 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
|
||||
|
||||
static bool
|
||||
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
// A config packet must not be decoded immediately (it contains no
|
||||
// frame); instead, it must be concatenated with the future data packet.
|
||||
if (demuxer->pending || is_config) {
|
||||
size_t offset;
|
||||
if (demuxer->pending) {
|
||||
offset = demuxer->pending->size;
|
||||
if (av_grow_packet(demuxer->pending, packet->size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
offset = 0;
|
||||
demuxer->pending = av_packet_alloc();
|
||||
if (!demuxer->pending) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
if (av_new_packet(demuxer->pending, packet->size)) {
|
||||
LOG_OOM();
|
||||
av_packet_free(&demuxer->pending);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
|
||||
|
||||
if (!is_config) {
|
||||
// prepare the concat packet to send to the decoder
|
||||
demuxer->pending->pts = packet->pts;
|
||||
demuxer->pending->dts = packet->dts;
|
||||
demuxer->pending->flags = packet->flags;
|
||||
packet = demuxer->pending;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = push_packet_to_sinks(demuxer, packet);
|
||||
|
||||
if (!is_config && demuxer->pending) {
|
||||
// the pending packet must be discarded (consumed or error)
|
||||
av_packet_free(&demuxer->pending);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Demuxer '%s': could not process packet", demuxer->name);
|
||||
LOGE("Could not process packet");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -153,6 +160,7 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
|
||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||
if (!sink->ops->open(sink, codec)) {
|
||||
LOGE("Could not open packet sink %d", i);
|
||||
sc_demuxer_close_first_sinks(demuxer, i);
|
||||
return false;
|
||||
}
|
||||
@@ -161,93 +169,50 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) {
|
||||
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||
if (sink->ops->disable) {
|
||||
sink->ops->disable(sink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
run_demuxer(void *data) {
|
||||
struct sc_demuxer *demuxer = data;
|
||||
|
||||
// Flag to report end-of-stream (i.e. device disconnected)
|
||||
bool eos = false;
|
||||
|
||||
uint32_t raw_codec_id;
|
||||
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
||||
if (!ok) {
|
||||
LOGE("Demuxer '%s': stream disabled due to connection error",
|
||||
demuxer->name);
|
||||
eos = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (raw_codec_id == 0) {
|
||||
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
eos = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
|
||||
if (codec_id == AV_CODEC_ID_NONE) {
|
||||
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
goto end;
|
||||
}
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
if (!codec) {
|
||||
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
LOGE("H.264 decoder not found");
|
||||
goto end;
|
||||
}
|
||||
|
||||
demuxer->codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!demuxer->codec_ctx) {
|
||||
LOG_OOM();
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||
goto end;
|
||||
LOGE("Could not open demuxer sinks");
|
||||
goto finally_free_codec_ctx;
|
||||
}
|
||||
|
||||
// Config packets must be merged with the next non-config packet only for
|
||||
// video streams
|
||||
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||
|
||||
struct sc_packet_merger merger;
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_init(&merger);
|
||||
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
if (!demuxer->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
|
||||
// We must only pass complete frames to av_parser_parse2()!
|
||||
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
||||
demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
||||
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (!packet) {
|
||||
LOG_OOM();
|
||||
goto finally_close_sinks;
|
||||
goto finally_close_parser;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
||||
if (!ok) {
|
||||
// end of stream
|
||||
eos = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
// Prepend any config packet to the next media packet
|
||||
ok = sc_packet_merger_merge(&merger, packet);
|
||||
if (!ok) {
|
||||
av_packet_unref(packet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ok = sc_demuxer_push_packet(demuxer, packet);
|
||||
av_packet_unref(packet);
|
||||
if (!ok) {
|
||||
@@ -256,31 +221,33 @@ run_demuxer(void *data) {
|
||||
}
|
||||
}
|
||||
|
||||
LOGD("Demuxer '%s': end of frames", demuxer->name);
|
||||
LOGD("End of frames");
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_destroy(&merger);
|
||||
if (demuxer->pending) {
|
||||
av_packet_free(&demuxer->pending);
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
finally_close_parser:
|
||||
av_parser_close(demuxer->parser);
|
||||
finally_close_sinks:
|
||||
sc_demuxer_close_sinks(demuxer);
|
||||
finally_free_codec_ctx:
|
||||
avcodec_free_context(&demuxer->codec_ctx);
|
||||
end:
|
||||
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
|
||||
demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||
assert(socket != SC_SOCKET_NONE);
|
||||
|
||||
demuxer->name = name; // statically allocated
|
||||
demuxer->socket = socket;
|
||||
demuxer->pending = NULL;
|
||||
demuxer->sink_count = 0;
|
||||
|
||||
assert(cbs && cbs->on_ended);
|
||||
assert(cbs && cbs->on_eos);
|
||||
|
||||
demuxer->cbs = cbs;
|
||||
demuxer->cbs_userdata = cbs_userdata;
|
||||
@@ -296,12 +263,12 @@ sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
|
||||
|
||||
bool
|
||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
||||
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
||||
LOGD("Starting demuxer thread");
|
||||
|
||||
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
||||
demuxer);
|
||||
if (!ok) {
|
||||
LOGE("Demuxer '%s': could not start thread", demuxer->name);
|
||||
LOGE("Could not start demuxer thread");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -12,28 +12,31 @@
|
||||
#include "util/net.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
#define SC_DEMUXER_MAX_SINKS 2
|
||||
#define SC_DEMUXER_MAX_SINKS 3
|
||||
|
||||
struct sc_demuxer {
|
||||
const char *name; // must be statically allocated (e.g. a string literal)
|
||||
|
||||
sc_socket socket;
|
||||
sc_thread thread;
|
||||
|
||||
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
|
||||
unsigned sink_count;
|
||||
|
||||
AVCodecContext *codec_ctx;
|
||||
AVCodecParserContext *parser;
|
||||
// successive packets may need to be concatenated, until a non-config
|
||||
// packet is available
|
||||
AVPacket *pending;
|
||||
|
||||
const struct sc_demuxer_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_demuxer_callbacks {
|
||||
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
|
||||
void (*on_eos)(struct sc_demuxer *demuxer, void *userdata);
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#define SC_EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1)
|
||||
#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
||||
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
||||
#define EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
|
||||
#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
|
||||
@@ -353,7 +353,6 @@ struct sc_mouse_click_event {
|
||||
struct sc_position position;
|
||||
enum sc_action action;
|
||||
enum sc_mouse_button button;
|
||||
uint64_t pointer_id;
|
||||
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
|
||||
};
|
||||
|
||||
@@ -366,7 +365,6 @@ struct sc_mouse_scroll_event {
|
||||
|
||||
struct sc_mouse_motion_event {
|
||||
struct sc_position position;
|
||||
uint64_t pointer_id;
|
||||
int32_t xrel;
|
||||
int32_t yrel;
|
||||
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
|
||||
|
||||
@@ -335,11 +335,8 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
||||
msg.inject_touch_event.action = action;
|
||||
msg.inject_touch_event.position.screen_size = im->screen->frame_size;
|
||||
msg.inject_touch_event.position.point = point;
|
||||
msg.inject_touch_event.pointer_id =
|
||||
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
|
||||
: POINTER_ID_VIRTUAL_FINGER;
|
||||
msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER;
|
||||
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
|
||||
msg.inject_touch_event.action_button = 0;
|
||||
msg.inject_touch_event.buttons = 0;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
@@ -567,8 +564,6 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
event->x,
|
||||
event->y),
|
||||
},
|
||||
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
|
||||
: POINTER_ID_GENERIC_FINGER,
|
||||
.xrel = event->xrel,
|
||||
.yrel = event->yrel,
|
||||
.buttons_state =
|
||||
@@ -692,8 +687,6 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
},
|
||||
.action = sc_action_from_sdl_mousebutton_type(event->type),
|
||||
.button = sc_mouse_button_from_sdl(event->button),
|
||||
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
|
||||
: POINTER_ID_GENERIC_FINGER,
|
||||
.buttons_state =
|
||||
sc_mouse_buttons_state_from_sdl(sdl_buttons_state,
|
||||
im->forward_all_clicks),
|
||||
|
||||
@@ -69,7 +69,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
|
||||
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
.inject_touch_event = {
|
||||
.action = AMOTION_EVENT_ACTION_MOVE,
|
||||
.pointer_id = event->pointer_id,
|
||||
.pointer_id = POINTER_ID_MOUSE,
|
||||
.position = event->position,
|
||||
.pressure = 1.f,
|
||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||
@@ -90,10 +90,9 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
|
||||
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
.inject_touch_event = {
|
||||
.action = convert_mouse_action(event->action),
|
||||
.pointer_id = event->pointer_id,
|
||||
.pointer_id = POINTER_ID_MOUSE,
|
||||
.position = event->position,
|
||||
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
||||
.action_button = convert_mouse_buttons(event->button),
|
||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,8 +13,6 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.v4l2_device = NULL,
|
||||
#endif
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.video_codec = SC_CODEC_H264,
|
||||
.audio_codec = SC_CODEC_OPUS,
|
||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
||||
.port_range = {
|
||||
@@ -28,8 +26,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.count = 2,
|
||||
},
|
||||
.max_size = 0,
|
||||
.bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.bit_rate = DEFAULT_BIT_RATE,
|
||||
.max_fps = 0,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.rotation = 0,
|
||||
@@ -68,5 +65,4 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.cleanup = true,
|
||||
.start_fps_counter = false,
|
||||
.power_on = true,
|
||||
.audio = true,
|
||||
};
|
||||
|
||||
@@ -23,14 +23,6 @@ enum sc_record_format {
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
};
|
||||
|
||||
enum sc_codec {
|
||||
SC_CODEC_H264,
|
||||
SC_CODEC_H265,
|
||||
SC_CODEC_AV1,
|
||||
SC_CODEC_OPUS,
|
||||
SC_CODEC_AAC,
|
||||
};
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||
// lock the current orientation when scrcpy starts
|
||||
@@ -97,13 +89,10 @@ struct scrcpy_options {
|
||||
const char *render_driver;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
const char *audio_encoder_name;
|
||||
#ifdef HAVE_V4L2
|
||||
const char *v4l2_device;
|
||||
#endif
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec video_codec;
|
||||
enum sc_codec audio_codec;
|
||||
enum sc_record_format record_format;
|
||||
enum sc_keyboard_input_mode keyboard_input_mode;
|
||||
enum sc_mouse_input_mode mouse_input_mode;
|
||||
@@ -113,7 +102,6 @@ struct scrcpy_options {
|
||||
struct sc_shortcut_mods shortcut_mods;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
@@ -152,7 +140,6 @@ struct scrcpy_options {
|
||||
bool cleanup;
|
||||
bool start_fps_counter;
|
||||
bool power_on;
|
||||
bool audio;
|
||||
};
|
||||
|
||||
extern const struct scrcpy_options scrcpy_options_default;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#include "packet_merger.h"
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger) {
|
||||
merger->config = NULL;
|
||||
}
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
|
||||
free(merger->config);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
if (is_config) {
|
||||
free(merger->config);
|
||||
|
||||
merger->config = malloc(packet->size);
|
||||
if (!merger->config) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(merger->config, packet->data, packet->size);
|
||||
merger->config_size = packet->size;
|
||||
} else if (merger->config) {
|
||||
size_t config_size = merger->config_size;
|
||||
size_t media_size = packet->size;
|
||||
|
||||
if (av_grow_packet(packet, config_size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memmove(packet->data + config_size, packet->data, media_size);
|
||||
memcpy(packet->data, merger->config, config_size);
|
||||
|
||||
free(merger->config);
|
||||
merger->config = NULL;
|
||||
// merger->size is meaningless when merger->config is NULL
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
#ifndef SC_PACKET_MERGER_H
|
||||
#define SC_PACKET_MERGER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
/**
|
||||
* Config packets (containing the SPS/PPS) are sent in-band. A new config
|
||||
* packet is sent whenever a new encoding session is started (on start and on
|
||||
* device orientation change).
|
||||
*
|
||||
* Every time a config packet is received, it must be sent alone (for recorder
|
||||
* extradata), then concatenated to the next media packet (for correct decoding
|
||||
* and recording).
|
||||
*
|
||||
* This helper reads every input packet and modifies each media packet which
|
||||
* immediately follows a config packet to prepend the config packet payload.
|
||||
*/
|
||||
|
||||
struct sc_packet_merger {
|
||||
uint8_t *config;
|
||||
size_t config_size;
|
||||
};
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger);
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger);
|
||||
|
||||
/**
|
||||
* If the packet is a config packet, then keep its data for later.
|
||||
* Otherwise (if the packet is a media packet), then if a config packet is
|
||||
* pending, prepend the config packet to this packet (so the packet is
|
||||
* modified!).
|
||||
*/
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
|
||||
|
||||
#endif
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync) {
|
||||
bool ok = sc_mutex_init(&receiver->mutex);
|
||||
if (!ok) {
|
||||
@@ -21,12 +21,12 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
}
|
||||
|
||||
void
|
||||
sc_receiver_destroy(struct sc_receiver *receiver) {
|
||||
receiver_destroy(struct receiver *receiver) {
|
||||
sc_mutex_destroy(&receiver->mutex);
|
||||
}
|
||||
|
||||
static void
|
||||
process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
|
||||
process_msg(struct receiver *receiver, struct device_msg *msg) {
|
||||
switch (msg->type) {
|
||||
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
||||
char *current = SDL_GetClipboardText();
|
||||
@@ -51,7 +51,7 @@ process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
size_t head = 0;
|
||||
for (;;) {
|
||||
struct device_msg msg;
|
||||
@@ -76,7 +76,7 @@ process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len)
|
||||
|
||||
static int
|
||||
run_receiver(void *data) {
|
||||
struct sc_receiver *receiver = data;
|
||||
struct receiver *receiver = data;
|
||||
|
||||
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
|
||||
size_t head = 0;
|
||||
@@ -108,7 +108,7 @@ run_receiver(void *data) {
|
||||
}
|
||||
|
||||
bool
|
||||
sc_receiver_start(struct sc_receiver *receiver) {
|
||||
receiver_start(struct receiver *receiver) {
|
||||
LOGD("Starting receiver thread");
|
||||
|
||||
bool ok = sc_thread_create(&receiver->thread, run_receiver,
|
||||
@@ -122,6 +122,6 @@ sc_receiver_start(struct sc_receiver *receiver) {
|
||||
}
|
||||
|
||||
void
|
||||
sc_receiver_join(struct sc_receiver *receiver) {
|
||||
receiver_join(struct receiver *receiver) {
|
||||
sc_thread_join(&receiver->thread, NULL);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
// receive events from the device
|
||||
// managed by the controller
|
||||
struct sc_receiver {
|
||||
struct receiver {
|
||||
sc_socket control_socket;
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@@ -20,18 +20,18 @@ struct sc_receiver {
|
||||
};
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
|
||||
void
|
||||
sc_receiver_destroy(struct sc_receiver *receiver);
|
||||
receiver_destroy(struct receiver *receiver);
|
||||
|
||||
bool
|
||||
sc_receiver_start(struct sc_receiver *receiver);
|
||||
receiver_start(struct receiver *receiver);
|
||||
|
||||
// no sc_receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
// no receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
|
||||
void
|
||||
sc_receiver_join(struct sc_receiver *receiver);
|
||||
receiver_join(struct receiver *receiver);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,11 +8,8 @@
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
|
||||
/** Downcast packet sinks to recorder */
|
||||
#define DOWNCAST_VIDEO(SINK) \
|
||||
container_of(SINK, struct sc_recorder, video_packet_sink)
|
||||
#define DOWNCAST_AUDIO(SINK) \
|
||||
container_of(SINK, struct sc_recorder, audio_packet_sink)
|
||||
/** Downcast packet_sink to recorder */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
@@ -81,7 +78,9 @@ sc_recorder_get_format_name(enum sc_record_format format) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
|
||||
sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOG_OOM();
|
||||
@@ -93,56 +92,170 @@ sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
|
||||
|
||||
ostream->codecpar->extradata = extradata;
|
||||
ostream->codecpar->extradata_size = packet->size;
|
||||
|
||||
int ret = avformat_write_header(recorder->ctx, NULL);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base);
|
||||
static void
|
||||
sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index,
|
||||
AVPacket *packet) {
|
||||
AVStream *stream = recorder->ctx->streams[stream_index];
|
||||
sc_recorder_rescale_packet(stream, packet);
|
||||
return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
|
||||
sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
if (!recorder->header_written) {
|
||||
if (packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first packet is not a config packet");
|
||||
return false;
|
||||
}
|
||||
bool ok = sc_recorder_write_header(recorder, packet);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
recorder->header_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet->pts == AV_NOPTS_VALUE) {
|
||||
// ignore config packets
|
||||
return true;
|
||||
}
|
||||
|
||||
sc_recorder_rescale_packet(recorder, packet);
|
||||
return av_write_frame(recorder->ctx, packet) >= 0;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
|
||||
packet);
|
||||
}
|
||||
static int
|
||||
run_recorder(void *data) {
|
||||
struct sc_recorder *recorder = data;
|
||||
|
||||
static inline bool
|
||||
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
|
||||
packet);
|
||||
for (;;) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
// if stopped is set, continue to process the remaining events (to
|
||||
// finish the recording) before actually stopping
|
||||
|
||||
if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
struct sc_record_packet *last = recorder->previous;
|
||||
if (last) {
|
||||
// assign an arbitrary duration to the last packet
|
||||
last->packet->duration = 100000;
|
||||
bool ok = sc_recorder_write(recorder, last->packet);
|
||||
if (!ok) {
|
||||
// failing to write the last frame is not very serious, no
|
||||
// future frame may depend on it, so the resulting file
|
||||
// will still be valid
|
||||
LOGW("Could not record last packet");
|
||||
}
|
||||
sc_record_packet_delete(last);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
struct sc_record_packet *rec;
|
||||
sc_queue_take(&recorder->queue, next, &rec);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
// recorder->previous is only written from this thread, no need to lock
|
||||
struct sc_record_packet *previous = recorder->previous;
|
||||
recorder->previous = rec;
|
||||
|
||||
if (!previous) {
|
||||
// we just received the first packet
|
||||
continue;
|
||||
}
|
||||
|
||||
// config packets have no PTS, we must ignore them
|
||||
if (rec->packet->pts != AV_NOPTS_VALUE
|
||||
&& previous->packet->pts != AV_NOPTS_VALUE) {
|
||||
// we now know the duration of the previous packet
|
||||
previous->packet->duration =
|
||||
rec->packet->pts - previous->packet->pts;
|
||||
}
|
||||
|
||||
bool ok = sc_recorder_write(recorder, previous->packet);
|
||||
sc_record_packet_delete(previous);
|
||||
if (!ok) {
|
||||
LOGE("Could not record packet");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->failed = true;
|
||||
// discard pending packets
|
||||
sc_recorder_queue_clear(&recorder->queue);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recorder->failed) {
|
||||
if (recorder->header_written) {
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
recorder->failed = true;
|
||||
}
|
||||
} else {
|
||||
// the recorded file is empty
|
||||
recorder->failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (recorder->failed) {
|
||||
LOGE("Recording failed to %s", recorder->filename);
|
||||
} else {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name,
|
||||
recorder->filename);
|
||||
}
|
||||
|
||||
LOGD("Recorder thread ended");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
bool ok = sc_mutex_init(&recorder->mutex);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
sc_queue_init(&recorder->queue);
|
||||
recorder->stopped = false;
|
||||
recorder->failed = false;
|
||||
recorder->header_written = false;
|
||||
recorder->previous = NULL;
|
||||
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
assert(format_name);
|
||||
const AVOutputFormat *format = find_muxer(format_name);
|
||||
if (!format) {
|
||||
LOGE("Could not find muxer");
|
||||
return false;
|
||||
goto error_cond_destroy;
|
||||
}
|
||||
|
||||
recorder->ctx = avformat_alloc_context();
|
||||
if (!recorder->ctx) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
avformat_free_context(recorder->ctx);
|
||||
return false;
|
||||
goto error_cond_destroy;
|
||||
}
|
||||
|
||||
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
||||
@@ -154,436 +267,71 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
av_dict_set(&recorder->ctx->metadata, "comment",
|
||||
"Recorded by scrcpy " SCRCPY_VERSION, 0);
|
||||
|
||||
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
|
||||
if (!ostream) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
ostream->codecpar->codec_id = input_codec->id;
|
||||
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||
ostream->codecpar->width = recorder->declared_frame_size.width;
|
||||
ostream->codecpar->height = recorder->declared_frame_size.height;
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
// ostream will be cleaned up during context cleaning
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
LOGD("Starting recorder thread");
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
|
||||
recorder);
|
||||
if (!ok) {
|
||||
LOGE("Could not start recorder thread");
|
||||
goto error_avio_close;
|
||||
}
|
||||
|
||||
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_close_output_file(struct sc_recorder *recorder) {
|
||||
return true;
|
||||
|
||||
error_avio_close:
|
||||
avio_close(recorder->ctx->pb);
|
||||
error_avformat_free_context:
|
||||
avformat_free_context(recorder->ctx);
|
||||
}
|
||||
error_cond_destroy:
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
|
||||
static bool
|
||||
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
while (!recorder->video_codec && !recorder->stopped) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
const AVCodec *codec = recorder->video_codec;
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (codec) {
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
stream->codecpar->codec_id = codec->id;
|
||||
stream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||
stream->codecpar->width = recorder->declared_frame_size.width;
|
||||
stream->codecpar->height = recorder->declared_frame_size.height;
|
||||
|
||||
recorder->video_stream_index = stream->index;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_wait_audio_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
while (!recorder->audio_codec && !recorder->audio_disabled
|
||||
&& !recorder->stopped) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
if (recorder->audio_disabled) {
|
||||
// Reset audio flag. From there, the recorder thread may access this
|
||||
// flag without any mutex.
|
||||
recorder->audio = false;
|
||||
}
|
||||
|
||||
const AVCodec *codec = recorder->audio_codec;
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (codec) {
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
|
||||
stream->codecpar->codec_id = codec->id;
|
||||
stream->codecpar->ch_layout.nb_channels = 2;
|
||||
stream->codecpar->sample_rate = 48000;
|
||||
|
||||
recorder->audio_stream_index = stream->index;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
|
||||
if (sc_queue_is_empty(&recorder->video_queue)) {
|
||||
// The video queue is empty
|
||||
return true;
|
||||
}
|
||||
|
||||
if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
// The audio queue is empty (when audio is enabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
// No queue is empty
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_process_header(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) {
|
||||
// If the recorder is stopped, don't process anything if there are not
|
||||
// at least video packets
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_record_packet *video_pkt;
|
||||
sc_queue_take(&recorder->video_queue, next, &video_pkt);
|
||||
|
||||
struct sc_record_packet *audio_pkt = NULL;
|
||||
if (!sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
assert(recorder->audio);
|
||||
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
|
||||
}
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
int ret = false;
|
||||
|
||||
if (video_pkt->packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first video packet is not a config packet");
|
||||
goto end;
|
||||
}
|
||||
|
||||
assert(recorder->video_stream_index >= 0);
|
||||
AVStream *video_stream =
|
||||
recorder->ctx->streams[recorder->video_stream_index];
|
||||
bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet);
|
||||
if (!ok) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (audio_pkt) {
|
||||
if (audio_pkt->packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first audio packet is not a config packet");
|
||||
goto end;
|
||||
}
|
||||
|
||||
assert(recorder->audio_stream_index >= 0);
|
||||
AVStream *audio_stream =
|
||||
recorder->ctx->streams[recorder->audio_stream_index];
|
||||
ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet);
|
||||
if (!ok) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
|
||||
if (!ok) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
sc_record_packet_delete(video_pkt);
|
||||
if (audio_pkt) {
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_process_packets(struct sc_recorder *recorder) {
|
||||
int64_t pts_origin = AV_NOPTS_VALUE;
|
||||
|
||||
bool header_written = sc_recorder_process_header(recorder);
|
||||
if (!header_written) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_record_packet *video_pkt = NULL;
|
||||
struct sc_record_packet *audio_pkt = NULL;
|
||||
|
||||
// We can write a video packet only once we received the next one so that
|
||||
// we can set its duration (next_pts - current_pts)
|
||||
struct sc_record_packet *video_pkt_previous = NULL;
|
||||
|
||||
bool error = false;
|
||||
|
||||
for (;;) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped) {
|
||||
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
|
||||
// A new packet may be assigned to video_pkt and be processed
|
||||
break;
|
||||
}
|
||||
if (recorder->audio && !audio_pkt
|
||||
&& !sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
// A new packet may be assigned to audio_pkt and be processed
|
||||
break;
|
||||
}
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
// If stopped is set, continue to process the remaining events (to
|
||||
// finish the recording) before actually stopping.
|
||||
|
||||
// If there is no audio, then the audio_queue will remain empty forever
|
||||
// and audio_pkt will always be NULL.
|
||||
assert(recorder->audio
|
||||
|| (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue)));
|
||||
|
||||
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
|
||||
sc_queue_take(&recorder->video_queue, next, &video_pkt);
|
||||
}
|
||||
|
||||
if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) {
|
||||
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
|
||||
}
|
||||
|
||||
if (recorder->stopped && !video_pkt && !audio_pkt) {
|
||||
assert(sc_queue_is_empty(&recorder->video_queue));
|
||||
assert(sc_queue_is_empty(&recorder->audio_queue));
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(video_pkt || audio_pkt); // at least one
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
// Ignore further config packets (e.g. on device orientation
|
||||
// change). The next non-config packet will have the config packet
|
||||
// data prepended.
|
||||
if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) {
|
||||
sc_record_packet_delete(video_pkt);
|
||||
video_pkt = NULL;
|
||||
}
|
||||
|
||||
if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) {
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
audio_pkt= NULL;
|
||||
}
|
||||
|
||||
if (pts_origin == AV_NOPTS_VALUE) {
|
||||
if (!recorder->audio) {
|
||||
assert(video_pkt);
|
||||
pts_origin = video_pkt->packet->pts;
|
||||
} else if (video_pkt && audio_pkt) {
|
||||
pts_origin =
|
||||
MIN(video_pkt->packet->pts, audio_pkt->packet->pts);
|
||||
} else if (recorder->stopped) {
|
||||
if (video_pkt) {
|
||||
// The recorder is stopped without audio, record the video
|
||||
// packets
|
||||
pts_origin = video_pkt->packet->pts;
|
||||
} else {
|
||||
// Fail if there is no video
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
// If the recorder is stopped while one of the streams has no
|
||||
// packets, then we must avoid a live-loop and correctly record
|
||||
// the stream having packets.
|
||||
pts_origin = video_pkt ? video_pkt->packet->pts
|
||||
: audio_pkt->packet->pts;
|
||||
} else {
|
||||
// We need both video and audio packets to initialize pts_origin
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
assert(pts_origin != AV_NOPTS_VALUE);
|
||||
|
||||
if (video_pkt) {
|
||||
video_pkt->packet->pts -= pts_origin;
|
||||
video_pkt->packet->dts = video_pkt->packet->pts;
|
||||
|
||||
if (video_pkt_previous) {
|
||||
// we now know the duration of the previous packet
|
||||
video_pkt_previous->packet->duration =
|
||||
video_pkt->packet->pts - video_pkt_previous->packet->pts;
|
||||
|
||||
bool ok = sc_recorder_write_video(recorder,
|
||||
video_pkt_previous->packet);
|
||||
sc_record_packet_delete(video_pkt_previous);
|
||||
if (!ok) {
|
||||
LOGE("Could not record video packet");
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
video_pkt_previous = video_pkt;
|
||||
video_pkt = NULL;
|
||||
}
|
||||
|
||||
if (audio_pkt) {
|
||||
audio_pkt->packet->pts -= pts_origin;
|
||||
audio_pkt->packet->dts = audio_pkt->packet->pts;
|
||||
|
||||
bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet);
|
||||
if (!ok) {
|
||||
LOGE("Could not record audio packet");
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
audio_pkt = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the last video packet
|
||||
struct sc_record_packet *last = video_pkt_previous;
|
||||
if (last) {
|
||||
// assign an arbitrary duration to the last packet
|
||||
last->packet->duration = 100000;
|
||||
bool ok = sc_recorder_write_video(recorder, last->packet);
|
||||
if (!ok) {
|
||||
// failing to write the last frame is not very serious, no
|
||||
// future frame may depend on it, so the resulting file
|
||||
// will still be valid
|
||||
LOGW("Could not record last packet");
|
||||
}
|
||||
sc_record_packet_delete(last);
|
||||
}
|
||||
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
error = false;
|
||||
}
|
||||
|
||||
end:
|
||||
if (video_pkt) {
|
||||
sc_record_packet_delete(video_pkt);
|
||||
}
|
||||
if (audio_pkt) {
|
||||
sc_record_packet_delete(audio_pkt);
|
||||
}
|
||||
|
||||
return !error;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_record(struct sc_recorder *recorder) {
|
||||
bool ok = sc_recorder_open_output_file(recorder);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_recorder_wait_video_stream(recorder);
|
||||
if (!ok) {
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (recorder->audio) {
|
||||
ok = sc_recorder_wait_audio_stream(recorder);
|
||||
if (!ok) {
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If recorder->stopped, process any queued packet anyway
|
||||
|
||||
ok = sc_recorder_process_packets(recorder);
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int
|
||||
run_recorder(void *data) {
|
||||
struct sc_recorder *recorder = data;
|
||||
|
||||
bool success = sc_recorder_record(recorder);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// Prevent the producer to push any new packet
|
||||
recorder->stopped = true;
|
||||
// Discard pending packets
|
||||
sc_recorder_queue_clear(&recorder->video_queue);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (success) {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name,
|
||||
recorder->filename);
|
||||
} else {
|
||||
LOGE("Recording failed to %s", recorder->filename);
|
||||
}
|
||||
|
||||
LOGD("Recorder thread ended");
|
||||
|
||||
recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
assert(codec);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
if (recorder->stopped) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->video_codec = codec;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
|
||||
sc_recorder_close(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
|
||||
avio_close(recorder->ctx->pb);
|
||||
avformat_free_context(recorder->ctx);
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
|
||||
sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
assert(!recorder->stopped);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
if (recorder->failed) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
@@ -595,9 +343,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
return false;
|
||||
}
|
||||
|
||||
rec->packet->stream_index = 0;
|
||||
|
||||
sc_queue_push(&recorder->video_queue, next, rec);
|
||||
sc_queue_push(&recorder->queue, next, rec);
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
@@ -605,189 +351,51 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
assert(codec);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio_codec = codec;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
return true;
|
||||
sc_recorder_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_open(recorder, codec);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
sc_recorder_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
sc_recorder_close(recorder);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_record_packet *rec = sc_record_packet_new(packet);
|
||||
if (!rec) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
rec->packet->stream_index = 1;
|
||||
|
||||
sc_queue_push(&recorder->audio_queue, next, rec);
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
assert(!recorder->audio_codec);
|
||||
|
||||
LOGW("Audio stream recording disabled");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio_disabled = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
sc_recorder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_push(recorder, packet);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||
sc_recorder_init(struct sc_recorder *recorder,
|
||||
const char *filename,
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size) {
|
||||
recorder->filename = strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = sc_mutex_init(&recorder->mutex);
|
||||
if (!ok) {
|
||||
goto error_free_filename;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->stream_cond);
|
||||
if (!ok) {
|
||||
goto error_queue_cond_destroy;
|
||||
}
|
||||
|
||||
recorder->audio = audio;
|
||||
|
||||
sc_queue_init(&recorder->video_queue);
|
||||
sc_queue_init(&recorder->audio_queue);
|
||||
recorder->stopped = false;
|
||||
|
||||
recorder->video_codec = NULL;
|
||||
recorder->audio_codec = NULL;
|
||||
recorder->audio_disabled = false;
|
||||
|
||||
recorder->video_stream_index = -1;
|
||||
recorder->audio_stream_index = -1;
|
||||
|
||||
recorder->format = format;
|
||||
recorder->declared_frame_size = declared_frame_size;
|
||||
|
||||
assert(cbs && cbs->on_ended);
|
||||
|
||||
recorder->cbs = cbs;
|
||||
recorder->cbs_userdata = cbs_userdata;
|
||||
|
||||
static const struct sc_packet_sink_ops video_ops = {
|
||||
.open = sc_recorder_video_packet_sink_open,
|
||||
.close = sc_recorder_video_packet_sink_close,
|
||||
.push = sc_recorder_video_packet_sink_push,
|
||||
static const struct sc_packet_sink_ops ops = {
|
||||
.open = sc_recorder_packet_sink_open,
|
||||
.close = sc_recorder_packet_sink_close,
|
||||
.push = sc_recorder_packet_sink_push,
|
||||
};
|
||||
|
||||
recorder->video_packet_sink.ops = &video_ops;
|
||||
|
||||
if (audio) {
|
||||
static const struct sc_packet_sink_ops audio_ops = {
|
||||
.open = sc_recorder_audio_packet_sink_open,
|
||||
.close = sc_recorder_audio_packet_sink_close,
|
||||
.push = sc_recorder_audio_packet_sink_push,
|
||||
.disable = sc_recorder_audio_packet_sink_disable,
|
||||
};
|
||||
|
||||
recorder->audio_packet_sink.ops = &audio_ops;
|
||||
}
|
||||
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
|
||||
recorder);
|
||||
if (!ok) {
|
||||
LOGE("Could not start recorder thread");
|
||||
goto error_stream_cond_destroy;
|
||||
}
|
||||
recorder->packet_sink.ops = &ops;
|
||||
|
||||
return true;
|
||||
|
||||
error_stream_cond_destroy:
|
||||
sc_cond_destroy(&recorder->stream_cond);
|
||||
error_queue_cond_destroy:
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
error_free_filename:
|
||||
free(recorder->filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_stop(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_join(struct sc_recorder *recorder) {
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder) {
|
||||
sc_cond_destroy(&recorder->stream_cond);
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
free(recorder->filename);
|
||||
}
|
||||
|
||||
@@ -20,63 +20,32 @@ struct sc_record_packet {
|
||||
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
|
||||
|
||||
struct sc_recorder {
|
||||
struct sc_packet_sink video_packet_sink;
|
||||
struct sc_packet_sink audio_packet_sink;
|
||||
|
||||
/* The audio flag is unprotected:
|
||||
* - it is initialized from sc_recorder_init() from the main thread;
|
||||
* - it may be reset once from the recorder thread if the audio is
|
||||
* disabled dynamically.
|
||||
*
|
||||
* Therefore, once the recorder thread is started, only the recorder thread
|
||||
* may access it without data races.
|
||||
*/
|
||||
bool audio;
|
||||
struct sc_packet_sink packet_sink; // packet sink trait
|
||||
|
||||
char *filename;
|
||||
enum sc_record_format format;
|
||||
AVFormatContext *ctx;
|
||||
struct sc_size declared_frame_size;
|
||||
bool header_written;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
sc_cond queue_cond;
|
||||
// set on sc_recorder_stop(), packet_sink close or recording failure
|
||||
bool stopped;
|
||||
struct sc_recorder_queue video_queue;
|
||||
struct sc_recorder_queue audio_queue;
|
||||
bool stopped; // set on recorder_close()
|
||||
bool failed; // set on packet write failure
|
||||
struct sc_recorder_queue queue;
|
||||
|
||||
// wake up the recorder thread once the video or audio codec is known
|
||||
sc_cond stream_cond;
|
||||
const AVCodec *video_codec;
|
||||
const AVCodec *audio_codec;
|
||||
// Instead of providing an audio_codec, the demuxer may notify that the
|
||||
// stream is disabled if the device could not capture audio
|
||||
bool audio_disabled;
|
||||
|
||||
int video_stream_index;
|
||||
int audio_stream_index;
|
||||
|
||||
const struct sc_recorder_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_recorder_callbacks {
|
||||
void (*on_ended)(struct sc_recorder *recorder, bool success,
|
||||
void *userdata);
|
||||
// we can write a packet only once we received the next one so that we can
|
||||
// set its duration (next_pts - current_pts)
|
||||
// "previous" is only accessed from the recorder thread, so it does not
|
||||
// need to be protected by the mutex
|
||||
struct sc_record_packet *previous;
|
||||
};
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
sc_recorder_stop(struct sc_recorder *recorder);
|
||||
|
||||
void
|
||||
sc_recorder_join(struct sc_recorder *recorder);
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size);
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder);
|
||||
|
||||
319
app/src/rtp.c
Normal file
319
app/src/rtp.c
Normal file
@@ -0,0 +1,319 @@
|
||||
#include "rtp.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/time.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
/** Downcast packet_sink to rtp */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_rtp, packet_sink)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
static struct sc_rtp_packet *
|
||||
sc_rtp_packet_new(const AVPacket *packet) {
|
||||
struct sc_rtp_packet *rtp = malloc(sizeof(*rtp));
|
||||
if (!rtp) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rtp->packet = av_packet_alloc();
|
||||
if (!rtp->packet) {
|
||||
LOG_OOM();
|
||||
free(rtp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (av_packet_ref(rtp->packet, packet)) {
|
||||
av_packet_free(&rtp->packet);
|
||||
free(rtp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return rtp;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_rtp_packet_delete(struct sc_rtp_packet *rtp) {
|
||||
av_packet_free(&rtp->packet);
|
||||
free(rtp);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_rtp_queue_clear(struct sc_rtp_queue *queue) {
|
||||
while (!sc_queue_is_empty(queue)) {
|
||||
struct sc_rtp_packet *rtp;
|
||||
sc_queue_take(queue, next, &rtp);
|
||||
sc_rtp_packet_delete(rtp);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_rtp_write_header(struct sc_rtp *rtp, const AVPacket *packet) {
|
||||
AVStream *ostream = rtp->ctx->streams[0];
|
||||
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
// copy the first packet to the extra data
|
||||
memcpy(extradata, packet->data, packet->size);
|
||||
|
||||
ostream->codecpar->extradata = extradata;
|
||||
ostream->codecpar->extradata_size = packet->size;
|
||||
|
||||
int ret = avformat_write_header(rtp->ctx, NULL);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write RTP header");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_rtp_rescale_packet(struct sc_rtp *rtp, AVPacket *packet) {
|
||||
AVStream *ostream = rtp->ctx->streams[0];
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_rtp_write(struct sc_rtp *rtp, AVPacket *packet) {
|
||||
if (!rtp->header_written) {
|
||||
if (packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first packet is not a config packet");
|
||||
return false;
|
||||
}
|
||||
bool ok = sc_rtp_write_header(rtp, packet);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
rtp->header_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet->pts == AV_NOPTS_VALUE) {
|
||||
// ignore config packets
|
||||
return true;
|
||||
}
|
||||
|
||||
sc_rtp_rescale_packet(rtp, packet);
|
||||
return av_write_frame(rtp->ctx, packet) >= 0;
|
||||
}
|
||||
|
||||
static int
|
||||
run_rtp(void *data) {
|
||||
struct sc_rtp *rtp = data;
|
||||
|
||||
for (;;) {
|
||||
sc_mutex_lock(&rtp->mutex);
|
||||
|
||||
while (!rtp->stopped && sc_queue_is_empty(&rtp->queue)) {
|
||||
sc_cond_wait(&rtp->queue_cond, &rtp->mutex);
|
||||
}
|
||||
|
||||
// if stopped is set, continue to process the remaining events (to
|
||||
// finish the streaming) before actually stopping
|
||||
|
||||
if (rtp->stopped && sc_queue_is_empty(&rtp->queue)) {
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
struct sc_rtp_packet *pkt;
|
||||
sc_queue_take(&rtp->queue, next, &pkt);
|
||||
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
|
||||
bool ok = sc_rtp_write(rtp, pkt->packet);
|
||||
sc_rtp_packet_delete(pkt);
|
||||
if (!ok) {
|
||||
LOGE("Could not send packet");
|
||||
|
||||
sc_mutex_lock(&rtp->mutex);
|
||||
rtp->failed = true;
|
||||
// discard pending packets
|
||||
sc_rtp_queue_clear(&rtp->queue);
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rtp->failed) {
|
||||
if (rtp->header_written) {
|
||||
int ret = av_write_trailer(rtp->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write RTP trailer");
|
||||
rtp->failed = true;
|
||||
}
|
||||
} else {
|
||||
// nothing has been sent
|
||||
rtp->failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rtp->failed) {
|
||||
LOGE("Streaming over RTP failed");
|
||||
} else {
|
||||
LOGI("Streaming over RTP complete");
|
||||
}
|
||||
|
||||
LOGD("RTP streaming thread ended");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_rtp_open(struct sc_rtp *rtp, const AVCodec *input_codec) {
|
||||
bool ok = sc_mutex_init(&rtp->mutex);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&rtp->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
sc_queue_init(&rtp->queue);
|
||||
rtp->stopped = false;
|
||||
rtp->failed = false;
|
||||
rtp->header_written = false;
|
||||
|
||||
int ret = avformat_alloc_output_context2(&rtp->ctx, NULL, "rtp",
|
||||
rtp->out_url);
|
||||
if (ret < 0) {
|
||||
goto error_cond_destroy;
|
||||
}
|
||||
|
||||
AVStream *ostream = avformat_new_stream(rtp->ctx, input_codec);
|
||||
if (!ostream) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
ostream->codecpar->codec_id = input_codec->id;
|
||||
ostream->codecpar->width = rtp->declared_frame_size.width;
|
||||
ostream->codecpar->height = rtp->declared_frame_size.height;
|
||||
|
||||
ret = avio_open(&rtp->ctx->pb, rtp->out_url, AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output: %s", rtp->out_url);
|
||||
// ostream will be cleaned up during context cleaning
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
LOGD("Starting RTP thread");
|
||||
ok = sc_thread_create(&rtp->thread, run_rtp, "scrcpy-rtp", rtp);
|
||||
if (!ok) {
|
||||
LOGE("Could not start RTP thread");
|
||||
goto error_avio_close;
|
||||
}
|
||||
|
||||
LOGI("Streaming started to %s", rtp->out_url);
|
||||
|
||||
return true;
|
||||
|
||||
error_avio_close:
|
||||
avio_close(rtp->ctx->pb);
|
||||
error_avformat_free_context:
|
||||
avformat_free_context(rtp->ctx);
|
||||
error_cond_destroy:
|
||||
sc_cond_destroy(&rtp->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&rtp->mutex);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_rtp_close(struct sc_rtp *rtp) {
|
||||
sc_mutex_lock(&rtp->mutex);
|
||||
rtp->stopped = true;
|
||||
sc_cond_signal(&rtp->queue_cond);
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
|
||||
sc_thread_join(&rtp->thread, NULL);
|
||||
|
||||
avio_close(rtp->ctx->pb);
|
||||
avformat_free_context(rtp->ctx);
|
||||
sc_cond_destroy(&rtp->queue_cond);
|
||||
sc_mutex_destroy(&rtp->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_rtp_push(struct sc_rtp *rtp, const AVPacket *packet) {
|
||||
sc_mutex_lock(&rtp->mutex);
|
||||
assert(!rtp->stopped);
|
||||
|
||||
if (rtp->failed) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_rtp_packet *pkt = sc_rtp_packet_new(packet);
|
||||
if (!pkt) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_queue_push(&rtp->queue, next, pkt);
|
||||
sc_cond_signal(&rtp->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&rtp->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_rtp_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_rtp *rtp = DOWNCAST(sink);
|
||||
return sc_rtp_open(rtp, codec);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_rtp_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_rtp *rtp = DOWNCAST(sink);
|
||||
sc_rtp_close(rtp);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_rtp_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_rtp *rtp = DOWNCAST(sink);
|
||||
return sc_rtp_push(rtp, packet);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_rtp_init(struct sc_rtp *rtp, const char *out_url,
|
||||
struct sc_size declared_frame_size) {
|
||||
rtp->out_url = strdup(out_url);
|
||||
if (!rtp->out_url) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
rtp->declared_frame_size = declared_frame_size;
|
||||
|
||||
static const struct sc_packet_sink_ops ops = {
|
||||
.open = sc_rtp_packet_sink_open,
|
||||
.close = sc_rtp_packet_sink_close,
|
||||
.push = sc_rtp_packet_sink_push,
|
||||
};
|
||||
|
||||
rtp->packet_sink.ops = &ops;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_rtp_destroy(struct sc_rtp *rtp) {
|
||||
free(rtp->out_url);
|
||||
}
|
||||
44
app/src/rtp.h
Normal file
44
app/src/rtp.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#ifndef SC_RTP_H
|
||||
#define SC_RTP_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#include "coords.h"
|
||||
#include "trait/packet_sink.h"
|
||||
#include "util/queue.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
struct sc_rtp_packet {
|
||||
AVPacket *packet;
|
||||
struct sc_rtp_packet *next;
|
||||
};
|
||||
|
||||
struct sc_rtp_queue SC_QUEUE(struct sc_rtp_packet);
|
||||
|
||||
struct sc_rtp {
|
||||
struct sc_packet_sink packet_sink; // packet sink trait;
|
||||
|
||||
char *out_url;
|
||||
AVFormatContext *ctx;
|
||||
struct sc_size declared_frame_size;
|
||||
bool header_written;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
sc_cond queue_cond;
|
||||
bool stopped; // set on rtp_close()
|
||||
bool failed; // set on packet write failure
|
||||
struct sc_rtp_queue queue;
|
||||
};
|
||||
|
||||
bool
|
||||
sc_rtp_init(struct sc_rtp *rtp, const char *out_url,
|
||||
struct sc_size declared_frame_size);
|
||||
|
||||
void
|
||||
sc_rtp_destroy(struct sc_rtp *rtp);
|
||||
|
||||
#endif
|
||||
149
app/src/scrcpy.c
149
app/src/scrcpy.c
@@ -21,6 +21,7 @@
|
||||
#include "keyboard_inject.h"
|
||||
#include "mouse_inject.h"
|
||||
#include "recorder.h"
|
||||
#include "rtp.h"
|
||||
#include "screen.h"
|
||||
#include "server.h"
|
||||
#ifdef HAVE_USB
|
||||
@@ -32,7 +33,6 @@
|
||||
#include "util/acksync.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
#include "util/rand.h"
|
||||
#ifdef HAVE_V4L2
|
||||
# include "v4l2_sink.h"
|
||||
#endif
|
||||
@@ -40,10 +40,10 @@
|
||||
struct scrcpy {
|
||||
struct sc_server server;
|
||||
struct sc_screen screen;
|
||||
struct sc_demuxer video_demuxer;
|
||||
struct sc_demuxer audio_demuxer;
|
||||
struct sc_demuxer demuxer;
|
||||
struct sc_decoder decoder;
|
||||
struct sc_recorder recorder;
|
||||
struct sc_rtp rtp;
|
||||
#ifdef HAVE_V4L2
|
||||
struct sc_v4l2_sink v4l2_sink;
|
||||
#endif
|
||||
@@ -156,15 +156,9 @@ event_loop(struct scrcpy *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SC_EVENT_DEVICE_DISCONNECTED:
|
||||
case EVENT_STREAM_STOPPED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SC_EVENT_DEMUXER_ERROR:
|
||||
LOGE("Demuxer error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SC_EVENT_RECORDER_ERROR:
|
||||
LOGE("Recorder error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SDL_QUIT:
|
||||
LOGD("User requested to quit");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
@@ -186,10 +180,10 @@ await_for_server(bool *connected) {
|
||||
LOGD("User requested to quit");
|
||||
*connected = false;
|
||||
return true;
|
||||
case SC_EVENT_SERVER_CONNECTION_FAILED:
|
||||
case EVENT_SERVER_CONNECTION_FAILED:
|
||||
LOGE("Server connection failed");
|
||||
return false;
|
||||
case SC_EVENT_SERVER_CONNECTED:
|
||||
case EVENT_SERVER_CONNECTED:
|
||||
LOGD("Server connected");
|
||||
*connected = true;
|
||||
return true;
|
||||
@@ -240,37 +234,11 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
||||
void *userdata) {
|
||||
(void) recorder;
|
||||
(void) userdata;
|
||||
|
||||
if (!success) {
|
||||
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||
void *userdata) {
|
||||
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
if (eos) {
|
||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||
} else {
|
||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||
void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) eos;
|
||||
(void) userdata;
|
||||
|
||||
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
||||
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -278,7 +246,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -286,7 +254,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTED);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -299,15 +267,6 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
|
||||
// event
|
||||
}
|
||||
|
||||
// Generate a scrcpy id to differentiate multiple running scrcpy instances
|
||||
static uint32_t
|
||||
scrcpy_generate_scid() {
|
||||
struct sc_rand rand;
|
||||
sc_rand_init(&rand);
|
||||
// Only use 31 bits to avoid issues with signed values on the Java-side
|
||||
return sc_rand_u32(&rand) & 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
enum scrcpy_exit_code
|
||||
scrcpy(struct scrcpy_options *options) {
|
||||
static struct scrcpy scrcpy;
|
||||
@@ -326,11 +285,11 @@ scrcpy(struct scrcpy_options *options) {
|
||||
bool server_started = false;
|
||||
bool file_pusher_initialized = false;
|
||||
bool recorder_initialized = false;
|
||||
bool rtp_initialized = false;
|
||||
#ifdef HAVE_V4L2
|
||||
bool v4l2_sink_initialized = false;
|
||||
#endif
|
||||
bool video_demuxer_started = false;
|
||||
bool audio_demuxer_started = false;
|
||||
bool demuxer_started = false;
|
||||
#ifdef HAVE_USB
|
||||
bool aoa_hid_initialized = false;
|
||||
bool hid_keyboard_initialized = false;
|
||||
@@ -342,33 +301,25 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_acksync *acksync = NULL;
|
||||
|
||||
uint32_t scid = scrcpy_generate_scid();
|
||||
|
||||
struct sc_server_params params = {
|
||||
.scid = scid,
|
||||
.req_serial = options->serial,
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.video_codec = options->video_codec,
|
||||
.audio_codec = options->audio_codec,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
.tunnel_port = options->tunnel_port,
|
||||
.max_size = options->max_size,
|
||||
.bit_rate = options->bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.lock_video_orientation = options->lock_video_orientation,
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.audio = options->audio,
|
||||
.show_touches = options->show_touches,
|
||||
.stay_awake = options->stay_awake,
|
||||
.codec_options = options->codec_options,
|
||||
.encoder_name = options->encoder_name,
|
||||
.audio_encoder_name = options->audio_encoder_name,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
@@ -447,12 +398,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_recorder *rec = NULL;
|
||||
if (options->record_filename) {
|
||||
static const struct sc_recorder_callbacks recorder_cbs = {
|
||||
.on_ended = sc_recorder_on_ended,
|
||||
};
|
||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||
options->record_format, options->audio,
|
||||
info->frame_size, &recorder_cbs, NULL)) {
|
||||
if (!sc_recorder_init(&s->recorder,
|
||||
options->record_filename,
|
||||
options->record_format,
|
||||
info->frame_size)) {
|
||||
goto end;
|
||||
}
|
||||
rec = &s->recorder;
|
||||
@@ -461,31 +410,27 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
av_log_set_callback(av_log_callback);
|
||||
|
||||
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||
.on_ended = sc_video_demuxer_on_ended,
|
||||
static const struct sc_demuxer_callbacks demuxer_cbs = {
|
||||
.on_eos = sc_demuxer_on_eos,
|
||||
};
|
||||
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
|
||||
&video_demuxer_cbs, NULL);
|
||||
|
||||
if (options->audio) {
|
||||
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||
.on_ended = sc_audio_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
||||
&audio_demuxer_cbs, NULL);
|
||||
}
|
||||
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
|
||||
|
||||
if (dec) {
|
||||
sc_demuxer_add_sink(&s->video_demuxer, &dec->packet_sink);
|
||||
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
|
||||
}
|
||||
|
||||
if (rec) {
|
||||
sc_demuxer_add_sink(&s->video_demuxer, &rec->video_packet_sink);
|
||||
if (options->audio) {
|
||||
sc_demuxer_add_sink(&s->audio_demuxer, &rec->audio_packet_sink);
|
||||
}
|
||||
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
|
||||
}
|
||||
|
||||
struct sc_rtp *rtp = NULL;
|
||||
if (!sc_rtp_init(&s->rtp, "rtp://127.0.0.1:1234", info->frame_size)) {
|
||||
goto end;
|
||||
}
|
||||
rtp = &s->rtp;
|
||||
rtp_initialized = true;
|
||||
sc_demuxer_add_sink(&s->demuxer, &rtp->packet_sink);
|
||||
|
||||
struct sc_controller *controller = NULL;
|
||||
struct sc_key_processor *kp = NULL;
|
||||
struct sc_mouse_processor *mp = NULL;
|
||||
@@ -691,24 +636,17 @@ aoa_hid_end:
|
||||
#endif
|
||||
|
||||
// now we consumed the header values, the socket receives the video stream
|
||||
// start the video demuxer
|
||||
if (!sc_demuxer_start(&s->video_demuxer)) {
|
||||
// start the demuxer
|
||||
if (!sc_demuxer_start(&s->demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
video_demuxer_started = true;
|
||||
|
||||
if (options->audio) {
|
||||
if (!sc_demuxer_start(&s->audio_demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
audio_demuxer_started = true;
|
||||
}
|
||||
demuxer_started = true;
|
||||
|
||||
ret = event_loop(s);
|
||||
LOGD("quit...");
|
||||
|
||||
// Close the window immediately on closing, because screen_destroy() may
|
||||
// only be called once the video demuxer thread is joined (it may take time)
|
||||
// only be called once the demuxer thread is joined (it may take time)
|
||||
sc_screen_hide_window(&s->screen);
|
||||
|
||||
end:
|
||||
@@ -735,9 +673,6 @@ end:
|
||||
if (file_pusher_initialized) {
|
||||
sc_file_pusher_stop(&s->file_pusher);
|
||||
}
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_stop(&s->recorder);
|
||||
}
|
||||
if (screen_initialized) {
|
||||
sc_screen_interrupt(&s->screen);
|
||||
}
|
||||
@@ -749,12 +684,8 @@ end:
|
||||
|
||||
// now that the sockets are shutdown, the demuxer and controller are
|
||||
// interrupted, we can join them
|
||||
if (video_demuxer_started) {
|
||||
sc_demuxer_join(&s->video_demuxer);
|
||||
}
|
||||
|
||||
if (audio_demuxer_started) {
|
||||
sc_demuxer_join(&s->audio_demuxer);
|
||||
if (demuxer_started) {
|
||||
sc_demuxer_join(&s->demuxer);
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
@@ -773,9 +704,8 @@ end:
|
||||
}
|
||||
#endif
|
||||
|
||||
// Destroy the screen only after the video demuxer is guaranteed to be
|
||||
// finished, because otherwise the screen could receive new frames after
|
||||
// destruction
|
||||
// Destroy the screen only after the demuxer is guaranteed to be finished,
|
||||
// because otherwise the screen could receive new frames after destruction
|
||||
if (screen_initialized) {
|
||||
sc_screen_join(&s->screen);
|
||||
sc_screen_destroy(&s->screen);
|
||||
@@ -788,8 +718,11 @@ end:
|
||||
sc_controller_destroy(&s->controller);
|
||||
}
|
||||
|
||||
if (rtp_initialized) {
|
||||
sc_rtp_destroy(&s->rtp);
|
||||
}
|
||||
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_join(&s->recorder);
|
||||
sc_recorder_destroy(&s->recorder);
|
||||
}
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||
bool need_new_event;
|
||||
if (previous_skipped) {
|
||||
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
||||
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// The EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// this new frame instead, unless the previous event failed
|
||||
need_new_event = screen->event_failed;
|
||||
} else {
|
||||
@@ -380,7 +380,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||
|
||||
if (need_new_event) {
|
||||
static SDL_Event new_frame_event = {
|
||||
.type = SC_EVENT_NEW_FRAME,
|
||||
.type = EVENT_NEW_FRAME,
|
||||
};
|
||||
|
||||
// Post the event on the UI thread
|
||||
@@ -820,7 +820,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
bool relative_mode = sc_screen_is_relative_mode(screen);
|
||||
|
||||
switch (event->type) {
|
||||
case SC_EVENT_NEW_FRAME: {
|
||||
case EVENT_NEW_FRAME: {
|
||||
bool ok = sc_screen_update_frame(screen);
|
||||
if (!ok) {
|
||||
LOGW("Frame update failed\n");
|
||||
|
||||
115
app/src/server.c
115
app/src/server.c
@@ -8,7 +8,6 @@
|
||||
#include <SDL2/SDL_platform.h>
|
||||
|
||||
#include "adb/adb.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net_intr.h"
|
||||
@@ -21,7 +20,6 @@
|
||||
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
|
||||
|
||||
#define SC_ADB_PORT_DEFAULT 5555
|
||||
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
|
||||
|
||||
static char *
|
||||
get_server_path(void) {
|
||||
@@ -73,7 +71,6 @@ sc_server_params_destroy(struct sc_server_params *params) {
|
||||
free((char *) params->crop);
|
||||
free((char *) params->codec_options);
|
||||
free((char *) params->encoder_name);
|
||||
free((char *) params->audio_encoder_name);
|
||||
free((char *) params->tcpip_dst);
|
||||
}
|
||||
|
||||
@@ -98,7 +95,6 @@ sc_server_params_copy(struct sc_server_params *dst,
|
||||
COPY(crop);
|
||||
COPY(codec_options);
|
||||
COPY(encoder_name);
|
||||
COPY(audio_encoder_name);
|
||||
COPY(tcpip_dst);
|
||||
#undef COPY
|
||||
|
||||
@@ -158,24 +154,6 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
|
||||
return !stopped;
|
||||
}
|
||||
|
||||
static const char *
|
||||
sc_server_get_codec_name(enum sc_codec codec) {
|
||||
switch (codec) {
|
||||
case SC_CODEC_H264:
|
||||
return "h264";
|
||||
case SC_CODEC_H265:
|
||||
return "h265";
|
||||
case SC_CODEC_AV1:
|
||||
return "av1";
|
||||
case SC_CODEC_OPUS:
|
||||
return "opus";
|
||||
case SC_CODEC_AAC:
|
||||
return "aac";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static sc_pid
|
||||
execute_server(struct sc_server *server,
|
||||
const struct sc_server_params *params) {
|
||||
@@ -219,25 +197,9 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = p; \
|
||||
}
|
||||
|
||||
ADD_PARAM("scid=%08x", params->scid);
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||
|
||||
if (params->bit_rate) {
|
||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||
}
|
||||
if (!params->audio) {
|
||||
ADD_PARAM("audio=false");
|
||||
} else if (params->audio_bit_rate) {
|
||||
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
|
||||
}
|
||||
if (params->video_codec != SC_CODEC_H264) {
|
||||
ADD_PARAM("video_codec=%s",
|
||||
sc_server_get_codec_name(params->video_codec));
|
||||
}
|
||||
if (params->audio_codec != SC_CODEC_OPUS) {
|
||||
ADD_PARAM("audio_codec=%s",
|
||||
sc_server_get_codec_name(params->audio_codec));
|
||||
}
|
||||
if (params->max_size) {
|
||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||
}
|
||||
@@ -273,9 +235,6 @@ execute_server(struct sc_server *server,
|
||||
if (params->encoder_name) {
|
||||
ADD_PARAM("encoder_name=%s", params->encoder_name);
|
||||
}
|
||||
if (params->audio_encoder_name) {
|
||||
ADD_PARAM("audio_encoder_name=%s", params->audio_encoder_name);
|
||||
}
|
||||
if (params->power_off_on_close) {
|
||||
ADD_PARAM("power_off_on_close=true");
|
||||
}
|
||||
@@ -405,11 +364,9 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
||||
}
|
||||
|
||||
server->serial = NULL;
|
||||
server->device_socket_name = NULL;
|
||||
server->stopped = false;
|
||||
|
||||
server->video_socket = SC_SOCKET_NONE;
|
||||
server->audio_socket = SC_SOCKET_NONE;
|
||||
server->control_socket = SC_SOCKET_NONE;
|
||||
|
||||
sc_adb_tunnel_init(&server->tunnel);
|
||||
@@ -438,9 +395,10 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
||||
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
|
||||
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
|
||||
|
||||
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||
info->frame_size.width = sc_read16be(fields);
|
||||
info->frame_size.height = sc_read16be(&fields[2]);
|
||||
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
|
||||
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -453,11 +411,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
const char *serial = server->serial;
|
||||
assert(serial);
|
||||
|
||||
bool audio = server->params.audio;
|
||||
bool control = server->params.control;
|
||||
|
||||
sc_socket video_socket = SC_SOCKET_NONE;
|
||||
sc_socket audio_socket = SC_SOCKET_NONE;
|
||||
sc_socket control_socket = SC_SOCKET_NONE;
|
||||
if (!tunnel->forward) {
|
||||
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
@@ -465,14 +421,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
audio_socket =
|
||||
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
if (audio_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
if (control) {
|
||||
control_socket =
|
||||
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
@@ -499,18 +447,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
audio_socket = net_socket();
|
||||
if (audio_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
|
||||
tunnel_port);
|
||||
if (!ok) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
if (control) {
|
||||
// we know that the device is listening, we don't need several
|
||||
// attempts
|
||||
@@ -527,8 +463,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
}
|
||||
|
||||
// we don't need the adb tunnel anymore
|
||||
sc_adb_tunnel_close(tunnel, &server->intr, serial,
|
||||
server->device_socket_name);
|
||||
sc_adb_tunnel_close(tunnel, &server->intr, serial);
|
||||
|
||||
// The sockets will be closed on stop if device_read_info() fails
|
||||
bool ok = device_read_info(&server->intr, video_socket, info);
|
||||
@@ -537,11 +472,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
}
|
||||
|
||||
assert(video_socket != SC_SOCKET_NONE);
|
||||
assert(!audio || audio_socket != SC_SOCKET_NONE);
|
||||
assert(!control || control_socket != SC_SOCKET_NONE);
|
||||
|
||||
server->video_socket = video_socket;
|
||||
server->audio_socket = audio_socket;
|
||||
server->control_socket = control_socket;
|
||||
|
||||
return true;
|
||||
@@ -553,12 +486,6 @@ fail:
|
||||
}
|
||||
}
|
||||
|
||||
if (audio_socket != SC_SOCKET_NONE) {
|
||||
if (!net_close(audio_socket)) {
|
||||
LOGW("Could not close audio socket");
|
||||
}
|
||||
}
|
||||
|
||||
if (control_socket != SC_SOCKET_NONE) {
|
||||
if (!net_close(control_socket)) {
|
||||
LOGW("Could not close control socket");
|
||||
@@ -567,8 +494,7 @@ fail:
|
||||
|
||||
if (tunnel->enabled) {
|
||||
// Always leave this function with tunnel disabled
|
||||
sc_adb_tunnel_close(tunnel, &server->intr, serial,
|
||||
server->device_socket_name);
|
||||
sc_adb_tunnel_close(tunnel, &server->intr, serial);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -838,23 +764,13 @@ run_server(void *data) {
|
||||
assert(serial);
|
||||
LOGD("Device serial: %s", serial);
|
||||
|
||||
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
|
||||
params->scid);
|
||||
if (r == -1) {
|
||||
LOG_OOM();
|
||||
goto error_connection_failed;
|
||||
}
|
||||
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
|
||||
assert(server->device_socket_name);
|
||||
|
||||
ok = push_server(&server->intr, serial);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
|
||||
server->device_socket_name, params->port_range,
|
||||
params->force_adb_forward);
|
||||
params->port_range, params->force_adb_forward);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
@@ -862,8 +778,7 @@ run_server(void *data) {
|
||||
// server will connect to our server socket
|
||||
sc_pid pid = execute_server(server, params);
|
||||
if (pid == SC_PROCESS_NONE) {
|
||||
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
|
||||
server->device_socket_name);
|
||||
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial);
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
@@ -875,8 +790,7 @@ run_server(void *data) {
|
||||
if (!ok) {
|
||||
sc_process_terminate(pid);
|
||||
sc_process_wait(pid, true); // ignore exit code
|
||||
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
|
||||
server->device_socket_name);
|
||||
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial);
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
@@ -904,11 +818,6 @@ run_server(void *data) {
|
||||
assert(server->video_socket != SC_SOCKET_NONE);
|
||||
net_interrupt(server->video_socket);
|
||||
|
||||
if (server->audio_socket != SC_SOCKET_NONE) {
|
||||
// There is no audio_socket if --no-audio is set
|
||||
net_interrupt(server->audio_socket);
|
||||
}
|
||||
|
||||
if (server->control_socket != SC_SOCKET_NONE) {
|
||||
// There is no control_socket if --no-control is set
|
||||
net_interrupt(server->control_socket);
|
||||
@@ -970,15 +879,11 @@ sc_server_destroy(struct sc_server *server) {
|
||||
if (server->video_socket != SC_SOCKET_NONE) {
|
||||
net_close(server->video_socket);
|
||||
}
|
||||
if (server->audio_socket != SC_SOCKET_NONE) {
|
||||
net_close(server->audio_socket);
|
||||
}
|
||||
if (server->control_socket != SC_SOCKET_NONE) {
|
||||
net_close(server->control_socket);
|
||||
}
|
||||
|
||||
free(server->serial);
|
||||
free(server->device_socket_name);
|
||||
sc_server_params_destroy(&server->params);
|
||||
sc_intr_destroy(&server->intr);
|
||||
sc_cond_destroy(&server->cond_stopped);
|
||||
|
||||
@@ -22,26 +22,20 @@ struct sc_server_info {
|
||||
};
|
||||
|
||||
struct sc_server_params {
|
||||
uint32_t scid;
|
||||
const char *req_serial;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec video_codec;
|
||||
enum sc_codec audio_codec;
|
||||
const char *crop;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
const char *audio_encoder_name;
|
||||
struct sc_port_range port_range;
|
||||
uint32_t tunnel_host;
|
||||
uint16_t tunnel_port;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
bool audio;
|
||||
bool show_touches;
|
||||
bool stay_awake;
|
||||
bool force_adb_forward;
|
||||
@@ -60,7 +54,6 @@ struct sc_server {
|
||||
// The internal allocated strings are copies owned by the server
|
||||
struct sc_server_params params;
|
||||
char *serial;
|
||||
char *device_socket_name;
|
||||
|
||||
sc_thread thread;
|
||||
struct sc_server_info info; // initialized once connected
|
||||
@@ -73,7 +66,6 @@ struct sc_server {
|
||||
struct sc_adb_tunnel tunnel;
|
||||
|
||||
sc_socket video_socket;
|
||||
sc_socket audio_socket;
|
||||
sc_socket control_socket;
|
||||
|
||||
const struct sc_server_callbacks *cbs;
|
||||
|
||||
@@ -19,20 +19,9 @@ struct sc_packet_sink {
|
||||
};
|
||||
|
||||
struct sc_packet_sink_ops {
|
||||
/* The codec instance is static, it is valid until the end of the program */
|
||||
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
|
||||
void (*close)(struct sc_packet_sink *sink);
|
||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||
|
||||
/*/
|
||||
* Called when the input stream has been disabled at runtime.
|
||||
*
|
||||
* If it is called, then open(), close() and push() will never be called.
|
||||
*
|
||||
* It is useful to notify the recorder that the requested audio stream has
|
||||
* finally been disabled because the device could not capture it.
|
||||
*/
|
||||
void (*disable)(struct sc_packet_sink *sink);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
|
||||
(void) userdata;
|
||||
|
||||
SDL_Event event;
|
||||
event.type = SC_EVENT_USB_DEVICE_DISCONNECTED;
|
||||
event.type = EVENT_USB_DEVICE_DISCONNECTED;
|
||||
int ret = SDL_PushEvent(&event);
|
||||
if (ret < 0) {
|
||||
LOGE("Could not post USB disconnection event: %s", SDL_GetError());
|
||||
@@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SC_EVENT_USB_DEVICE_DISCONNECTED:
|
||||
case EVENT_USB_DEVICE_DISCONNECTED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SDL_QUIT:
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
#include "rand.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "tick.h"
|
||||
|
||||
void sc_rand_init(struct sc_rand *rand) {
|
||||
sc_tick seed = sc_tick_now(); // microsecond precision
|
||||
rand->xsubi[0] = (seed >> 32) & 0xFFFF;
|
||||
rand->xsubi[1] = (seed >> 16) & 0xFFFF;
|
||||
rand->xsubi[2] = seed & 0xFFFF;
|
||||
}
|
||||
|
||||
uint32_t sc_rand_u32(struct sc_rand *rand) {
|
||||
// jrand returns a value in range [-2^31, 2^31]
|
||||
// conversion from signed to unsigned is well-defined to wrap-around
|
||||
return jrand48(rand->xsubi);
|
||||
}
|
||||
|
||||
uint64_t sc_rand_u64(struct sc_rand *rand) {
|
||||
uint32_t msb = sc_rand_u32(rand);
|
||||
uint32_t lsb = sc_rand_u32(rand);
|
||||
return ((uint64_t) msb << 32) | lsb;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
#ifndef SC_RAND_H
|
||||
#define SC_RAND_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
struct sc_rand {
|
||||
unsigned short xsubi[3];
|
||||
};
|
||||
|
||||
void sc_rand_init(struct sc_rand *rand);
|
||||
uint32_t sc_rand_u32(struct sc_rand *rand);
|
||||
uint64_t sc_rand_u64(struct sc_rand *rand);
|
||||
|
||||
#endif
|
||||
@@ -90,14 +90,13 @@ static void test_serialize_inject_touch_event(void) {
|
||||
},
|
||||
},
|
||||
.pressure = 1.0f,
|
||||
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
},
|
||||
};
|
||||
|
||||
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 32);
|
||||
assert(size == 28);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
@@ -106,8 +105,7 @@ static void test_serialize_inject_touch_event(void) {
|
||||
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
|
||||
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
||||
0xff, 0xff, // pressure
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
|
||||
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ ffmpeg_avcodec = 'avcodec-58'
|
||||
ffmpeg_avformat = 'avformat-58'
|
||||
ffmpeg_avutil = 'avutil-56'
|
||||
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
|
||||
prebuilt_sdl2 = 'SDL2-2.0.22/i686-w64-mingw32'
|
||||
prebuilt_libusb_root = 'libusb-1.0.26'
|
||||
prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32'
|
||||
|
||||
@@ -19,7 +19,7 @@ endian = 'little'
|
||||
ffmpeg_avcodec = 'avcodec-59'
|
||||
ffmpeg_avformat = 'avformat-59'
|
||||
ffmpeg_avutil = 'avutil-57'
|
||||
prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
|
||||
prebuilt_ffmpeg = 'ffmpeg-win64-5.0.1'
|
||||
prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32'
|
||||
prebuilt_libusb_root = 'libusb-1.0.26'
|
||||
prebuilt_libusb = 'libusb-1.0.26/MinGW-x64'
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24
|
||||
PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
@@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check
|
||||
|
||||
echo "[scrcpy] Building client..."
|
||||
rm -rf "$BUILDDIR"
|
||||
meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
|
||||
meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
|
||||
-Dprebuilt_server=scrcpy-server
|
||||
cd "$BUILDDIR"
|
||||
ninja
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '1.25',
|
||||
version: '1.24',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
54
release.mk
54
release.mk
@@ -24,13 +24,13 @@ SERVER_BUILD_DIR := build-server
|
||||
WIN32_BUILD_DIR := build-win32
|
||||
WIN64_BUILD_DIR := build-win64
|
||||
|
||||
VERSION := $(shell git describe --tags --always)
|
||||
|
||||
DIST := dist
|
||||
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
|
||||
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
|
||||
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
|
||||
WIN64_TARGET := $(WIN64_TARGET_DIR).zip
|
||||
WIN32_TARGET_DIR := scrcpy-win32
|
||||
WIN64_TARGET_DIR := scrcpy-win64
|
||||
|
||||
VERSION := $(shell git describe --tags --always)
|
||||
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
|
||||
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
|
||||
|
||||
RELEASE_DIR := release-$(VERSION)
|
||||
|
||||
@@ -53,13 +53,13 @@ clean:
|
||||
|
||||
test:
|
||||
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
|
||||
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
|
||||
meson "$(TEST_BUILD_DIR)" -Db_sanitize=address )
|
||||
ninja -C "$(TEST_BUILD_DIR)"
|
||||
$(GRADLE) -p server check
|
||||
|
||||
build-server:
|
||||
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
|
||||
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
|
||||
meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
|
||||
ninja -C "$(SERVER_BUILD_DIR)"
|
||||
|
||||
prepare-deps-win32:
|
||||
@@ -76,7 +76,7 @@ prepare-deps-win64:
|
||||
|
||||
build-win32: prepare-deps-win32
|
||||
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
|
||||
meson setup "$(WIN32_BUILD_DIR)" \
|
||||
meson "$(WIN32_BUILD_DIR)" \
|
||||
--cross-file cross_win32.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
@@ -85,7 +85,7 @@ build-win32: prepare-deps-win32
|
||||
|
||||
build-win64: prepare-deps-win64
|
||||
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
|
||||
meson setup "$(WIN64_BUILD_DIR)" \
|
||||
meson "$(WIN64_BUILD_DIR)" \
|
||||
--cross-file cross_win64.txt \
|
||||
--buildtype release --strip -Db_lto=true \
|
||||
-Dcompile_server=false \
|
||||
@@ -105,10 +105,10 @@ dist-win32: build-server build-win32
|
||||
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.0.22/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
|
||||
dist-win64: build-server build-win64
|
||||
@@ -119,21 +119,21 @@ dist-win64: build-server build-win64
|
||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.0.22/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
|
||||
zip-win32: dist-win32
|
||||
cd "$(DIST)"; \
|
||||
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
|
||||
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
|
||||
zip -r "../$(WIN32_TARGET)" .
|
||||
|
||||
zip-win64: dist-win64
|
||||
cd "$(DIST)"; \
|
||||
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
|
||||
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \
|
||||
zip -r "../$(WIN64_TARGET)" .
|
||||
|
||||
@@ -6,8 +6,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 12500
|
||||
versionName "1.25"
|
||||
versionCode 12400
|
||||
versionName "1.24"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
@@ -19,7 +19,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
}
|
||||
|
||||
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=1.25
|
||||
SCRCPY_VERSION_NAME=1.24
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-33}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
|
||||
@@ -20,21 +20,18 @@ BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
|
||||
|
||||
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
|
||||
CLASSES_DIR="$BUILD_DIR/classes"
|
||||
GEN_DIR="$BUILD_DIR/gen"
|
||||
SERVER_DIR=$(dirname "$0")
|
||||
SERVER_BINARY=scrcpy-server
|
||||
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
|
||||
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
|
||||
|
||||
echo "Platform: android-$PLATFORM"
|
||||
echo "Build-tools: $BUILD_TOOLS"
|
||||
echo "Build dir: $BUILD_DIR"
|
||||
|
||||
rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||
mkdir -p "$CLASSES_DIR"
|
||||
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
|
||||
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
|
||||
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
|
||||
|
||||
<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java"
|
||||
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public final class BuildConfig {
|
||||
@@ -45,15 +42,13 @@ EOF
|
||||
|
||||
echo "Generating java from aidl..."
|
||||
cd "$SERVER_DIR/src/main/aidl"
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl
|
||||
"$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" \
|
||||
android/content/IOnPrimaryClipChangedListener.aidl
|
||||
|
||||
echo "Compiling java sources..."
|
||||
cd ../java
|
||||
javac -bootclasspath "$ANDROID_JAR" \
|
||||
-cp "$LAMBDA_JAR:$GEN_DIR" \
|
||||
-d "$CLASSES_DIR" \
|
||||
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
|
||||
-source 1.8 -target 1.8 \
|
||||
com/genymobile/scrcpy/*.java \
|
||||
com/genymobile/scrcpy/wrappers/*.java
|
||||
@@ -73,7 +68,7 @@ then
|
||||
echo "Archiving..."
|
||||
cd "$BUILD_DIR"
|
||||
jar cvf "$SERVER_BINARY" classes.dex
|
||||
rm -rf classes.dex
|
||||
rm -rf classes.dex classes
|
||||
else
|
||||
# use d8
|
||||
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
|
||||
@@ -85,8 +80,7 @@ else
|
||||
|
||||
cd "$BUILD_DIR"
|
||||
mv classes.zip "$SERVER_BINARY"
|
||||
rm -rf classes
|
||||
fi
|
||||
|
||||
rm -rf "$GEN_DIR" "$CLASSES_DIR"
|
||||
|
||||
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
||||
public enum AudioCodec implements Codec {
|
||||
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
||||
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC);
|
||||
|
||||
private final int id; // 4-byte ASCII representation of the name
|
||||
private final String name;
|
||||
private final String mimeType;
|
||||
|
||||
AudioCodec(int id, String name, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.AUDIO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public static AudioCodec findByName(String name) {
|
||||
for (AudioCodec codec : values()) {
|
||||
if (codec.name.equals(name)) {
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.AudioTimestamp;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public final class AudioEncoder {
|
||||
|
||||
private static class InputTask {
|
||||
private final int index;
|
||||
|
||||
InputTask(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
private static class OutputTask {
|
||||
private final int index;
|
||||
private final MediaCodec.BufferInfo bufferInfo;
|
||||
|
||||
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
this.index = index;
|
||||
this.bufferInfo = bufferInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int CHANNELS = 2;
|
||||
|
||||
private static final int BUFFER_MS = 15; // milliseconds
|
||||
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000;
|
||||
|
||||
private final Streamer streamer;
|
||||
private final int bitRate;
|
||||
private final String encoderName;
|
||||
|
||||
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
|
||||
// So many pending tasks would lead to an unacceptable delay anyway.
|
||||
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
|
||||
private final BlockingQueue<OutputTask> outputTasks = new ArrayBlockingQueue<>(64);
|
||||
|
||||
private Thread thread;
|
||||
private HandlerThread mediaCodecThread;
|
||||
|
||||
private Thread inputThread;
|
||||
private Thread outputThread;
|
||||
|
||||
private boolean ended;
|
||||
|
||||
public AudioEncoder(Streamer streamer, int bitRate, String encoderName) {
|
||||
this.streamer = streamer;
|
||||
this.bitRate = bitRate;
|
||||
this.encoderName = encoderName;
|
||||
}
|
||||
|
||||
private static AudioFormat createAudioFormat() {
|
||||
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||
builder.setEncoding(AudioFormat.ENCODING_PCM_16BIT);
|
||||
builder.setSampleRate(SAMPLE_RATE);
|
||||
builder.setChannelMask(CHANNELS == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
@SuppressLint({"WrongConstant", "MissingPermission"})
|
||||
private static AudioRecord createAudioRecord() {
|
||||
AudioRecord.Builder builder = new AudioRecord.Builder();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
||||
builder.setContext(FakeContext.get());
|
||||
}
|
||||
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
|
||||
builder.setAudioFormat(createAudioFormat());
|
||||
builder.setBufferSizeInBytes(1024 * 1024);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(String mimeType, int bitRate) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, mimeType);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
|
||||
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
|
||||
return format;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException {
|
||||
final AudioTimestamp timestamp = new AudioTimestamp();
|
||||
long previousPts = 0;
|
||||
long nextPts = 0;
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
InputTask task = inputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
||||
int r = recorder.read(buffer, BUFFER_SIZE);
|
||||
if (r < 0) {
|
||||
throw new IOException("Could not read audio: " + r);
|
||||
}
|
||||
|
||||
long pts;
|
||||
|
||||
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
||||
if (ret == AudioRecord.SUCCESS) {
|
||||
pts = timestamp.nanoTime / 1000;
|
||||
} else {
|
||||
if (nextPts == 0) {
|
||||
Ln.w("Could not get any audio timestamp");
|
||||
}
|
||||
// compute from previous timestamp and packet size
|
||||
pts = nextPts;
|
||||
}
|
||||
|
||||
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
||||
nextPts = pts + durationMs;
|
||||
|
||||
if (previousPts != 0 && pts < previousPts) {
|
||||
// Audio PTS may come from two sources:
|
||||
// - recorder.getTimestamp() if the call works;
|
||||
// - an estimation from the previous PTS and the packet size as a fallback.
|
||||
//
|
||||
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
||||
pts = previousPts + 1;
|
||||
}
|
||||
|
||||
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
|
||||
|
||||
previousPts = pts;
|
||||
}
|
||||
}
|
||||
|
||||
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
|
||||
streamer.writeHeader();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
OutputTask task = outputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
|
||||
try {
|
||||
streamer.writePacket(buffer, task.bufferInfo);
|
||||
} finally {
|
||||
mediaCodec.releaseOutputBuffer(task.index, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
encode();
|
||||
} catch (IOException e) {
|
||||
Ln.e("Audio encoding error", e);
|
||||
} finally {
|
||||
Ln.d("Audio encoder stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void end() {
|
||||
ended = true;
|
||||
notify();
|
||||
}
|
||||
|
||||
private synchronized void waitEnded() {
|
||||
try {
|
||||
while (!ended) {
|
||||
wait();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public void encode() throws IOException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
Ln.w("Audio disabled: it is not supported before Android 11");
|
||||
streamer.writeDisableStream();
|
||||
return;
|
||||
}
|
||||
|
||||
MediaCodec mediaCodec = null;
|
||||
AudioRecord recorder = null;
|
||||
|
||||
boolean mediaCodecStarted = false;
|
||||
boolean recorderStarted = false;
|
||||
try {
|
||||
Codec codec = streamer.getCodec();
|
||||
mediaCodec = createMediaCodec(codec, encoderName);
|
||||
recorder = createAudioRecord();
|
||||
|
||||
mediaCodecThread = new HandlerThread("AudioEncoder");
|
||||
mediaCodecThread.start();
|
||||
|
||||
MediaFormat format = createFormat(codec.getMimeType(), bitRate);
|
||||
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
|
||||
recorder.startRecording();
|
||||
recorderStarted = true;
|
||||
|
||||
final MediaCodec mediaCodecRef = mediaCodec;
|
||||
final AudioRecord recorderRef = recorder;
|
||||
inputThread = new Thread(() -> {
|
||||
try {
|
||||
inputThread(mediaCodecRef, recorderRef);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
Ln.e("Audio capture error", e);
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
});
|
||||
|
||||
outputThread = new Thread(() -> {
|
||||
try {
|
||||
outputThread(mediaCodecRef);
|
||||
} catch (InterruptedException e) {
|
||||
// this is expected on close
|
||||
} catch (IOException e) {
|
||||
// Broken pipe is expected on close, because the socket is closed by the client
|
||||
if (!IO.isBrokenPipe(e)) {
|
||||
Ln.e("Audio encoding error", e);
|
||||
}
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
});
|
||||
|
||||
mediaCodec.start();
|
||||
mediaCodecStarted = true;
|
||||
inputThread.start();
|
||||
outputThread.start();
|
||||
|
||||
waitEnded();
|
||||
} catch (ConfigurationException e) {
|
||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||
} finally {
|
||||
if (!recorderStarted) {
|
||||
// Notify the client that the audio could not be captured
|
||||
streamer.writeDisableStream();
|
||||
}
|
||||
|
||||
// Cleanup everything (either at the end or on error at any step of the initialization)
|
||||
if (mediaCodecThread != null) {
|
||||
Looper looper = mediaCodecThread.getLooper();
|
||||
if (looper != null) {
|
||||
looper.quitSafely();
|
||||
}
|
||||
}
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
}
|
||||
if (outputThread != null) {
|
||||
outputThread.interrupt();
|
||||
}
|
||||
|
||||
try {
|
||||
if (mediaCodecThread != null) {
|
||||
mediaCodecThread.join();
|
||||
}
|
||||
if (inputThread != null) {
|
||||
inputThread.join();
|
||||
}
|
||||
if (outputThread != null) {
|
||||
outputThread.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Should never happen
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
if (mediaCodec != null) {
|
||||
if (mediaCodecStarted) {
|
||||
mediaCodec.stop();
|
||||
}
|
||||
mediaCodec.release();
|
||||
}
|
||||
if (recorder != null) {
|
||||
if (recorderStarted) {
|
||||
recorder.stop();
|
||||
}
|
||||
recorder.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating audio encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e(CodecUtils.buildUnknownEncoderMessage(codec, encoderName));
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
}
|
||||
}
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
}
|
||||
|
||||
private class EncoderCallback extends MediaCodec.Callback {
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||
try {
|
||||
inputTasks.put(new InputTask(index));
|
||||
} catch (InterruptedException e) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
|
||||
try {
|
||||
outputTasks.put(new OutputTask(index, bufferInfo));
|
||||
} catch (InterruptedException e) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
|
||||
Ln.e("MediaCodec error", e);
|
||||
end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public interface Codec {
|
||||
|
||||
enum Type {
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
}
|
||||
|
||||
Type getType();
|
||||
|
||||
int getId();
|
||||
|
||||
String getName();
|
||||
|
||||
String getMimeType();
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CodecOption {
|
||||
private final String key;
|
||||
private final Object value;
|
||||
private String key;
|
||||
private Object value;
|
||||
|
||||
public CodecOption(String key, Object value) {
|
||||
this.key = key;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class CodecUtils {
|
||||
|
||||
private CodecUtils() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static String buildUnknownEncoderMessage(Codec codec, String encoderName) {
|
||||
StringBuilder msg = new StringBuilder("Encoder '").append(encoderName).append("' for ").append(codec.getName()).append(" not found");
|
||||
MediaCodecInfo[] encoders = listEncoders(codec.getMimeType());
|
||||
if (encoders != null && encoders.length > 0) {
|
||||
msg.append("\nTry to use one of the available encoders:");
|
||||
String codecOption = codec.getType() == Codec.Type.VIDEO ? "video-codec" : "audio-codec";
|
||||
for (MediaCodecInfo encoder : encoders) {
|
||||
msg.append("\n scrcpy --").append(codecOption).append("=").append(codec.getName());
|
||||
msg.append(" --encoder='").append(encoder.getName()).append("'");
|
||||
}
|
||||
}
|
||||
return msg.toString();
|
||||
}
|
||||
|
||||
private static MediaCodecInfo[] listEncoders(String mimeType) {
|
||||
List<MediaCodecInfo> result = new ArrayList<>();
|
||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) {
|
||||
result.add(codecInfo);
|
||||
}
|
||||
}
|
||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public class ConfigurationException extends Exception {
|
||||
public ConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ public final class ControlMessage {
|
||||
private int metaState; // KeyEvent.META_*
|
||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
||||
private int keycode; // KeyEvent.KEYCODE_*
|
||||
private int actionButton; // MotionEvent.BUTTON_*
|
||||
private int buttons; // MotionEvent.BUTTON_*
|
||||
private long pointerId;
|
||||
private float pressure;
|
||||
@@ -61,15 +60,13 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
|
||||
int buttons) {
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_INJECT_TOUCH_EVENT;
|
||||
msg.action = action;
|
||||
msg.pointerId = pointerId;
|
||||
msg.pressure = pressure;
|
||||
msg.position = position;
|
||||
msg.actionButton = actionButton;
|
||||
msg.buttons = buttons;
|
||||
return msg;
|
||||
}
|
||||
@@ -143,10 +140,6 @@ public final class ControlMessage {
|
||||
return keycode;
|
||||
}
|
||||
|
||||
public int getActionButton() {
|
||||
return actionButton;
|
||||
}
|
||||
|
||||
public int getButtons() {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
|
||||
public class ControlMessageReader {
|
||||
|
||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||
@@ -140,9 +140,8 @@ public class ControlMessageReader {
|
||||
long pointerId = buffer.getLong();
|
||||
Position position = readPosition(buffer);
|
||||
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
|
||||
int actionButton = buffer.getInt();
|
||||
int buttons = buffer.getInt();
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectScrollEvent() {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
@@ -18,14 +16,8 @@ public class Controller {
|
||||
|
||||
private static final int DEFAULT_DEVICE_ID = 0;
|
||||
|
||||
// control_msg.h values of the pointerId field in inject_touch_event message
|
||||
private static final int POINTER_ID_MOUSE = -1;
|
||||
private static final int POINTER_ID_VIRTUAL_MOUSE = -3;
|
||||
|
||||
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private final Device device;
|
||||
private final DesktopConnection connection;
|
||||
private final DeviceMessageSender sender;
|
||||
@@ -64,7 +56,7 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
private void control() throws IOException {
|
||||
public void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && !Device.isScreenOn()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
@@ -79,39 +71,11 @@ public class Controller {
|
||||
SystemClock.sleep(500);
|
||||
}
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
while (true) {
|
||||
handleEvent();
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
sender.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
}
|
||||
sender.stop();
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
sender.join();
|
||||
}
|
||||
|
||||
public DeviceMessageSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
@@ -131,7 +95,7 @@ public class Controller {
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
@@ -211,7 +175,7 @@ public class Controller {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
@@ -228,23 +192,10 @@ public class Controller {
|
||||
Pointer pointer = pointersState.get(pointerIndex);
|
||||
pointer.setPoint(point);
|
||||
pointer.setPressure(pressure);
|
||||
|
||||
int source;
|
||||
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
|
||||
// real mouse event (forced by the client when --forward-on-click)
|
||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
|
||||
source = InputDevice.SOURCE_MOUSE;
|
||||
pointer.setUp(buttons == 0);
|
||||
} else {
|
||||
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
|
||||
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
source = InputDevice.SOURCE_TOUCHSCREEN;
|
||||
// Buttons must not be set for touch events
|
||||
buttons = 0;
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
}
|
||||
pointer.setUp(action == MotionEvent.ACTION_UP);
|
||||
|
||||
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
|
||||
|
||||
if (pointerCount == 1) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
lastTouchDown = now;
|
||||
@@ -258,60 +209,12 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/* If the input device is a mouse (on API >= 23):
|
||||
* - the first button pressed must first generate ACTION_DOWN;
|
||||
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
|
||||
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
|
||||
* - the last button released must in addition generate ACTION_UP.
|
||||
*
|
||||
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
|
||||
*/
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
if (actionButton == buttons) {
|
||||
// First button pressed: ACTION_DOWN
|
||||
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Any button pressed: ACTION_BUTTON_PRESS
|
||||
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!InputManager.setActionButton(pressEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
// Any button released: ACTION_BUTTON_RELEASE
|
||||
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
|
||||
return false;
|
||||
}
|
||||
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttons == 0) {
|
||||
// Last button released: ACTION_UP
|
||||
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
|
||||
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
|
||||
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// Right-click and middle-click only work if the source is a mouse
|
||||
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
|
||||
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
|
||||
if (source != InputDevice.SOURCE_MOUSE) {
|
||||
// Buttons must not be set for touch events
|
||||
buttons = 0;
|
||||
}
|
||||
|
||||
MotionEvent event = MotionEvent
|
||||
@@ -347,9 +250,12 @@ public class Controller {
|
||||
* Schedule a call to set power mode to off after a small delay.
|
||||
*/
|
||||
private static void schedulePowerModeOff() {
|
||||
EXECUTOR.schedule(() -> {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
EXECUTOR.schedule(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Ln.i("Forcing screen off");
|
||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
||||
}
|
||||
}, 200, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,11 @@ public final class DesktopConnection implements Closeable {
|
||||
|
||||
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
|
||||
|
||||
private static final String SOCKET_NAME_PREFIX = "scrcpy";
|
||||
private static final String SOCKET_NAME = "scrcpy";
|
||||
|
||||
private final LocalSocket videoSocket;
|
||||
private final FileDescriptor videoFd;
|
||||
|
||||
private final LocalSocket audioSocket;
|
||||
private final FileDescriptor audioFd;
|
||||
|
||||
private final LocalSocket controlSocket;
|
||||
private final InputStream controlInputStream;
|
||||
private final OutputStream controlOutputStream;
|
||||
@@ -30,10 +27,9 @@ public final class DesktopConnection implements Closeable {
|
||||
private final ControlMessageReader reader = new ControlMessageReader();
|
||||
private final DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException {
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
||||
this.videoSocket = videoSocket;
|
||||
this.controlSocket = controlSocket;
|
||||
this.audioSocket = audioSocket;
|
||||
if (controlSocket != null) {
|
||||
controlInputStream = controlSocket.getInputStream();
|
||||
controlOutputStream = controlSocket.getOutputStream();
|
||||
@@ -42,7 +38,6 @@ public final class DesktopConnection implements Closeable {
|
||||
controlOutputStream = null;
|
||||
}
|
||||
videoFd = videoSocket.getFileDescriptor();
|
||||
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
|
||||
}
|
||||
|
||||
private static LocalSocket connect(String abstractName) throws IOException {
|
||||
@@ -51,59 +46,41 @@ public final class DesktopConnection implements Closeable {
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
private static String getSocketName(int scid) {
|
||||
if (scid == -1) {
|
||||
// If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone
|
||||
return SOCKET_NAME_PREFIX;
|
||||
}
|
||||
|
||||
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
|
||||
}
|
||||
|
||||
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
|
||||
String socketName = getSocketName(scid);
|
||||
|
||||
LocalSocket videoSocket = null;
|
||||
LocalSocket audioSocket = null;
|
||||
public static DesktopConnection open(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
|
||||
LocalSocket videoSocket;
|
||||
LocalSocket controlSocket = null;
|
||||
try {
|
||||
if (tunnelForward) {
|
||||
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
||||
videoSocket = localServerSocket.accept();
|
||||
if (sendDummyByte) {
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
}
|
||||
if (audio) {
|
||||
audioSocket = localServerSocket.accept();
|
||||
}
|
||||
if (control) {
|
||||
controlSocket = localServerSocket.accept();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(socketName);
|
||||
if (audio) {
|
||||
audioSocket = connect(socketName);
|
||||
if (tunnelForward) {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||
try {
|
||||
videoSocket = localServerSocket.accept();
|
||||
if (sendDummyByte) {
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
}
|
||||
if (control) {
|
||||
controlSocket = connect(socketName);
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(SOCKET_NAME);
|
||||
if (control) {
|
||||
try {
|
||||
controlSocket = connect(SOCKET_NAME);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException e) {
|
||||
if (videoSocket != null) {
|
||||
videoSocket.close();
|
||||
}
|
||||
if (audioSocket != null) {
|
||||
audioSocket.close();
|
||||
}
|
||||
if (controlSocket != null) {
|
||||
controlSocket.close();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
|
||||
return new DesktopConnection(videoSocket, controlSocket);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
@@ -136,10 +113,6 @@ public final class DesktopConnection implements Closeable {
|
||||
return videoFd;
|
||||
}
|
||||
|
||||
public FileDescriptor getAudioFd() {
|
||||
return audioFd;
|
||||
}
|
||||
|
||||
public ControlMessage receiveControlMessage() throws IOException {
|
||||
ControlMessage msg = reader.next();
|
||||
while (msg == null) {
|
||||
|
||||
@@ -61,12 +61,12 @@ public final class Device {
|
||||
|
||||
private final boolean supportsInputEvents;
|
||||
|
||||
public Device(Options options) throws ConfigurationException {
|
||||
public Device(Options options) {
|
||||
displayId = options.getDisplayId();
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
Ln.e(buildUnknownDisplayIdMessage(displayId));
|
||||
throw new ConfigurationException("Unknown display id: " + displayId);
|
||||
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
|
||||
throw new InvalidDisplayIdException(displayId, displayIds);
|
||||
}
|
||||
|
||||
int displayInfoFlags = displayInfo.getFlags();
|
||||
@@ -130,18 +130,6 @@ public final class Device {
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildUnknownDisplayIdMessage(int displayId) {
|
||||
StringBuilder msg = new StringBuilder("Display ").append(displayId).append(" not found");
|
||||
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
|
||||
if (displayIds != null && displayIds.length > 0) {
|
||||
msg.append("\nTry to use one of the available display ids:");
|
||||
for (int id : displayIds) {
|
||||
msg.append("\n scrcpy --display=").append(id);
|
||||
}
|
||||
}
|
||||
return msg.toString();
|
||||
}
|
||||
|
||||
public synchronized void setMaxSize(int newMaxSize) {
|
||||
maxSize = newMaxSize;
|
||||
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
|
||||
@@ -289,26 +277,6 @@ public final class Device {
|
||||
* @param mode one of the {@code POWER_MODE_*} constants
|
||||
*/
|
||||
public static boolean setScreenPowerMode(int mode) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Change the power mode for all physical displays
|
||||
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
|
||||
if (physicalDisplayIds == null) {
|
||||
Ln.e("Could not get physical display ids");
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean allOk = true;
|
||||
for (long physicalDisplayId : physicalDisplayIds) {
|
||||
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
if (!ok) {
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
||||
// Older Android versions, only 1 display
|
||||
IBinder d = SurfaceControl.getBuiltInDisplay();
|
||||
if (d == null) {
|
||||
Ln.e("Could not get built-in display");
|
||||
|
||||
@@ -6,8 +6,6 @@ public final class DeviceMessageSender {
|
||||
|
||||
private final DesktopConnection connection;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private String clipboardText;
|
||||
|
||||
private long ack;
|
||||
@@ -26,8 +24,8 @@ public final class DeviceMessageSender {
|
||||
notify();
|
||||
}
|
||||
|
||||
private void loop() throws IOException, InterruptedException {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
public void loop() throws IOException, InterruptedException {
|
||||
while (true) {
|
||||
String text;
|
||||
long sequence;
|
||||
synchronized (this) {
|
||||
@@ -51,28 +49,4 @@ public final class DeviceMessageSender {
|
||||
}
|
||||
}
|
||||
}
|
||||
public void start() {
|
||||
thread = new Thread(() -> {
|
||||
try {
|
||||
loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
} finally {
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void join() throws InterruptedException {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.ContextWrapper;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.android.shell";
|
||||
|
||||
private static final FakeContext INSTANCE = new FakeContext();
|
||||
|
||||
public static FakeContext get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private FakeContext() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPackageName() {
|
||||
return PACKAGE_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOpPackageName() {
|
||||
return PACKAGE_NAME;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.S)
|
||||
@Override
|
||||
public AttributionSource getAttributionSource() {
|
||||
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
|
||||
builder.setPackageName(PACKAGE_NAME);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,4 @@ public final class IO {
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static boolean isBrokenPipe(IOException e) {
|
||||
Throwable cause = e.getCause();
|
||||
return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public class InvalidDisplayIdException extends RuntimeException {
|
||||
|
||||
private final int displayId;
|
||||
private final int[] availableDisplayIds;
|
||||
|
||||
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
|
||||
super("There is no display having id " + displayId);
|
||||
this.displayId = displayId;
|
||||
this.availableDisplayIds = availableDisplayIds;
|
||||
}
|
||||
|
||||
public int getDisplayId() {
|
||||
return displayId;
|
||||
}
|
||||
|
||||
public int[] getAvailableDisplayIds() {
|
||||
return availableDisplayIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
|
||||
public class InvalidEncoderException extends RuntimeException {
|
||||
|
||||
private final String name;
|
||||
private final MediaCodecInfo[] availableEncoders;
|
||||
|
||||
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
|
||||
super("There is no encoder having name '" + name + '"');
|
||||
this.name = name;
|
||||
this.availableEncoders = availableEncoders;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public MediaCodecInfo[] getAvailableEncoders() {
|
||||
return availableEncoders;
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,9 @@ import android.graphics.Rect;
|
||||
import java.util.List;
|
||||
|
||||
public class Options {
|
||||
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int scid = -1; // 31-bit non-negative value, or -1
|
||||
private boolean audio = true;
|
||||
private int maxSize;
|
||||
private VideoCodec videoCodec = VideoCodec.H264;
|
||||
private AudioCodec audioCodec = AudioCodec.OPUS;
|
||||
private int bitRate = 8000000;
|
||||
private int audioBitRate = 196000;
|
||||
private int maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private boolean tunnelForward;
|
||||
@@ -24,7 +18,6 @@ public class Options {
|
||||
private boolean stayAwake;
|
||||
private List<CodecOption> codecOptions;
|
||||
private String encoderName;
|
||||
private String audioEncoderName;
|
||||
private boolean powerOffScreenOnClose;
|
||||
private boolean clipboardAutosync = true;
|
||||
private boolean downsizeOnError = true;
|
||||
@@ -35,7 +28,6 @@ public class Options {
|
||||
private boolean sendDeviceMeta = true; // send device name and size
|
||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
||||
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
@@ -45,22 +37,6 @@ public class Options {
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
public int getScid() {
|
||||
return scid;
|
||||
}
|
||||
|
||||
public void setScid(int scid) {
|
||||
this.scid = scid;
|
||||
}
|
||||
|
||||
public boolean getAudio() {
|
||||
return audio;
|
||||
}
|
||||
|
||||
public void setAudio(boolean audio) {
|
||||
this.audio = audio;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
@@ -69,22 +45,6 @@ public class Options {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public VideoCodec getVideoCodec() {
|
||||
return videoCodec;
|
||||
}
|
||||
|
||||
public void setVideoCodec(VideoCodec videoCodec) {
|
||||
this.videoCodec = videoCodec;
|
||||
}
|
||||
|
||||
public AudioCodec getAudioCodec() {
|
||||
return audioCodec;
|
||||
}
|
||||
|
||||
public void setAudioCodec(AudioCodec audioCodec) {
|
||||
this.audioCodec = audioCodec;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
}
|
||||
@@ -93,14 +53,6 @@ public class Options {
|
||||
this.bitRate = bitRate;
|
||||
}
|
||||
|
||||
public int getAudioBitRate() {
|
||||
return audioBitRate;
|
||||
}
|
||||
|
||||
public void setAudioBitRate(int audioBitRate) {
|
||||
this.audioBitRate = audioBitRate;
|
||||
}
|
||||
|
||||
public int getMaxFps() {
|
||||
return maxFps;
|
||||
}
|
||||
@@ -181,14 +133,6 @@ public class Options {
|
||||
this.encoderName = encoderName;
|
||||
}
|
||||
|
||||
public String getAudioEncoderName() {
|
||||
return audioEncoderName;
|
||||
}
|
||||
|
||||
public void setAudioEncoderName(String audioEncoderName) {
|
||||
this.audioEncoderName = audioEncoderName;
|
||||
}
|
||||
|
||||
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
|
||||
this.powerOffScreenOnClose = powerOffScreenOnClose;
|
||||
}
|
||||
@@ -252,12 +196,4 @@ public class Options {
|
||||
public void setSendDummyByte(boolean sendDummyByte) {
|
||||
this.sendDummyByte = sendDummyByte;
|
||||
}
|
||||
|
||||
public boolean getSendCodecId() {
|
||||
return sendCodecId;
|
||||
}
|
||||
|
||||
public void setSendCodecId(boolean sendCodecId) {
|
||||
this.sendCodecId = sendCodecId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.genymobile.scrcpy;
|
||||
import java.util.Objects;
|
||||
|
||||
public class Position {
|
||||
private final Point point;
|
||||
private final Size screenSize;
|
||||
private Point point;
|
||||
private Size screenSize;
|
||||
|
||||
public Position(Point point, Size screenSize) {
|
||||
this.point = point;
|
||||
|
||||
@@ -5,14 +5,17 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -24,25 +27,26 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
// Keep the values in descending order
|
||||
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
|
||||
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
|
||||
private final AtomicBoolean rotationChanged = new AtomicBoolean();
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
private final Device device;
|
||||
private final Streamer streamer;
|
||||
private final String encoderName;
|
||||
private final List<CodecOption> codecOptions;
|
||||
private final int bitRate;
|
||||
private final int maxFps;
|
||||
private final boolean sendFrameMeta;
|
||||
private final boolean downsizeOnError;
|
||||
private long ptsOrigin;
|
||||
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(Device device, Streamer streamer, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
boolean downsizeOnError) {
|
||||
this.device = device;
|
||||
this.streamer = streamer;
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
this.bitRate = bitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
@@ -59,96 +63,75 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
public void streamScreen() throws IOException, ConfigurationException {
|
||||
Codec codec = streamer.getCodec();
|
||||
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
|
||||
MediaFormat format = createFormat(codec.getMimeType(), bitRate, maxFps, codecOptions);
|
||||
IBinder display = createDisplay();
|
||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
Workarounds.prepareMainLooper();
|
||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||
// <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
// <https://github.com/Genymobile/scrcpy/issues/2656>
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
|
||||
internalStreamScreen(device, fd);
|
||||
}
|
||||
|
||||
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
|
||||
device.setRotationListener(this);
|
||||
|
||||
streamer.writeHeader();
|
||||
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
MediaCodec codec = createCodec(encoderName);
|
||||
IBinder display = createDisplay();
|
||||
ScreenInfo screenInfo = device.getScreenInfo();
|
||||
Rect contentRect = screenInfo.getContentRect();
|
||||
|
||||
// include the locked video orientation
|
||||
Rect videoRect = screenInfo.getVideoSize().toRect();
|
||||
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width());
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height());
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
int layerStack = device.getLayerStack();
|
||||
setSize(format, videoRect.width(), videoRect.height());
|
||||
|
||||
Surface surface = null;
|
||||
try {
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
surface = mediaCodec.createInputSurface();
|
||||
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
int videoRotation = screenInfo.getVideoRotation();
|
||||
int layerStack = device.getLayerStack();
|
||||
configure(codec, format);
|
||||
surface = codec.createInputSurface();
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
codec.start();
|
||||
|
||||
mediaCodec.start();
|
||||
|
||||
alive = encode(mediaCodec, streamer);
|
||||
alive = encode(codec, fd);
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
mediaCodec.stop();
|
||||
codec.stop();
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
if (!prepareRetry(device, screenInfo)) {
|
||||
if (!downsizeOnError || firstFrameSent) {
|
||||
// Fail immediately
|
||||
throw e;
|
||||
}
|
||||
Ln.i("Retrying...");
|
||||
|
||||
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
|
||||
if (newMaxSize == 0) {
|
||||
// Definitively fail
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Retry with a smaller device size
|
||||
Ln.i("Retrying with -m" + newMaxSize + "...");
|
||||
device.setMaxSize(newMaxSize);
|
||||
alive = true;
|
||||
} finally {
|
||||
mediaCodec.reset();
|
||||
destroyDisplay(display);
|
||||
codec.release();
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
}
|
||||
}
|
||||
} while (alive);
|
||||
} finally {
|
||||
mediaCodec.release();
|
||||
device.setRotationListener(null);
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean prepareRetry(Device device, ScreenInfo screenInfo) {
|
||||
if (firstFrameSent) {
|
||||
++consecutiveErrors;
|
||||
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
||||
// Definitively fail
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait a bit to increase the probability that retrying will fix the problem
|
||||
SystemClock.sleep(50);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!downsizeOnError) {
|
||||
// Must fail immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
|
||||
|
||||
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
|
||||
Ln.i("newMaxSize = " + newMaxSize);
|
||||
if (newMaxSize == 0) {
|
||||
// Must definitively fail
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry with a smaller device size
|
||||
Ln.i("Retrying with -m" + newMaxSize + "...");
|
||||
device.setMaxSize(newMaxSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int chooseMaxSizeFallback(Size failedSize) {
|
||||
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
|
||||
for (int value : MAX_SIZE_FALLBACK) {
|
||||
@@ -161,30 +144,30 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
|
||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
try {
|
||||
if (consumeRotationChange()) {
|
||||
// must restart encoding with new size
|
||||
break;
|
||||
}
|
||||
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
if (outputBufferId >= 0) {
|
||||
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
||||
|
||||
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||
if (!isConfig) {
|
||||
// If this is not a config packet, then it contains a frame
|
||||
firstFrameSent = true;
|
||||
consecutiveErrors = 0;
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
|
||||
streamer.writePacket(codecBuffer, bufferInfo);
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
||||
// If this is not a config packet, then it contains a frame
|
||||
firstFrameSent = true;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
@@ -196,19 +179,52 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return !eof;
|
||||
}
|
||||
|
||||
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
|
||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||
headerBuffer.clear();
|
||||
|
||||
long pts;
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
pts = PACKET_FLAG_CONFIG; // non-media data packet
|
||||
} else {
|
||||
if (ptsOrigin == 0) {
|
||||
ptsOrigin = bufferInfo.presentationTimeUs;
|
||||
}
|
||||
pts = bufferInfo.presentationTimeUs - ptsOrigin;
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||
pts |= PACKET_FLAG_KEY_FRAME;
|
||||
}
|
||||
}
|
||||
|
||||
headerBuffer.putLong(pts);
|
||||
headerBuffer.putInt(packetSize);
|
||||
headerBuffer.flip();
|
||||
IO.writeFully(fd, headerBuffer);
|
||||
}
|
||||
|
||||
private static MediaCodecInfo[] listEncoders() {
|
||||
List<MediaCodecInfo> result = new ArrayList<>();
|
||||
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
|
||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
|
||||
result.add(codecInfo);
|
||||
}
|
||||
}
|
||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||
}
|
||||
|
||||
private static MediaCodec createCodec(String encoderName) throws IOException {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e(CodecUtils.buildUnknownEncoderMessage(codec, encoderName));
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
MediaCodecInfo[] encoders = listEncoders();
|
||||
throw new InvalidEncoderException(encoderName, encoders);
|
||||
}
|
||||
}
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||
return codec;
|
||||
}
|
||||
|
||||
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
|
||||
@@ -228,9 +244,9 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, videoMimeType);
|
||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||
@@ -262,6 +278,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return SurfaceControl.createDisplay("scrcpy", secure);
|
||||
}
|
||||
|
||||
private static void configure(MediaCodec codec, MediaFormat format) {
|
||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
}
|
||||
|
||||
private static void setSize(MediaFormat format, int width, int height) {
|
||||
format.setInteger(MediaFormat.KEY_WIDTH, width);
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, height);
|
||||
}
|
||||
|
||||
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
|
||||
SurfaceControl.openTransaction();
|
||||
try {
|
||||
@@ -272,4 +297,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static void destroyDisplay(IBinder display) {
|
||||
SurfaceControl.destroyDisplay(display);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
@@ -58,108 +59,103 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||
final Device device = new Device(options);
|
||||
List<CodecOption> codecOptions = options.getCodecOptions();
|
||||
|
||||
Thread initThread = startInitThread(options);
|
||||
|
||||
int scid = options.getScid();
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
boolean control = options.getControl();
|
||||
boolean audio = options.getAudio();
|
||||
boolean sendDummyByte = options.getSendDummyByte();
|
||||
|
||||
Workarounds.prepareMainLooper();
|
||||
|
||||
// Workarounds must be applied for Meizu phones:
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/240>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/365>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/2656>
|
||||
//
|
||||
// But only apply when strictly necessary, since workarounds can cause other issues:
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
||||
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
|
||||
|
||||
// Before Android 11, audio is not supported.
|
||||
// Since Android 12, we can properly set a context on the AudioRecord.
|
||||
// Only on Android 11 we must fill app info for the AudioRecord to work.
|
||||
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
|
||||
|
||||
if (mustFillAppInfo) {
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
|
||||
Controller controller = null;
|
||||
AudioEncoder audioEncoder = null;
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
||||
try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) {
|
||||
if (options.getSendDeviceMeta()) {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
}
|
||||
|
||||
if (control) {
|
||||
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller.start();
|
||||
|
||||
final Controller controllerRef = controller;
|
||||
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(),
|
||||
options.getSendFrameMeta());
|
||||
audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioEncoderName());
|
||||
audioEncoder.start();
|
||||
}
|
||||
|
||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
|
||||
options.getSendFrameMeta());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
|
||||
options.getEncoderName(), options.getDownsizeOnError());
|
||||
|
||||
Thread controllerThread = null;
|
||||
Thread deviceMessageSenderThread = null;
|
||||
if (control) {
|
||||
final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
|
||||
// asynchronous
|
||||
controllerThread = startController(controller);
|
||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||
|
||||
device.setClipboardListener(new Device.ClipboardListener() {
|
||||
@Override
|
||||
public void onClipboardTextChanged(String text) {
|
||||
controller.getSender().pushClipboardText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen();
|
||||
screenEncoder.streamScreen(device, connection.getVideoFd());
|
||||
} catch (IOException e) {
|
||||
// Broken pipe is expected on close, because the socket is closed by the client
|
||||
if (!IO.isBrokenPipe(e)) {
|
||||
Ln.e("Video encoding error", e);
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (controllerThread != null) {
|
||||
controllerThread.interrupt();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Ln.d("Screen streaming stopped");
|
||||
initThread.interrupt();
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.stop();
|
||||
}
|
||||
if (controller != null) {
|
||||
controller.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
initThread.join();
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.join();
|
||||
if (deviceMessageSenderThread != null) {
|
||||
deviceMessageSenderThread.interrupt();
|
||||
}
|
||||
if (controller != null) {
|
||||
controller.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Thread startInitThread(final Options options) {
|
||||
Thread thread = new Thread(() -> initAndCleanUp(options));
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
initAndCleanUp(options);
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startController(final Controller controller) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
controller.control();
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Controller stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sender.loop();
|
||||
} catch (IOException | InterruptedException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Device message sender stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
@SuppressWarnings("MethodLength")
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("Missing client version");
|
||||
@@ -182,35 +178,10 @@ public final class Server {
|
||||
String key = arg.substring(0, equalIndex);
|
||||
String value = arg.substring(equalIndex + 1);
|
||||
switch (key) {
|
||||
case "scid":
|
||||
int scid = Integer.parseInt(value, 0x10);
|
||||
if (scid < -1) {
|
||||
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
|
||||
}
|
||||
options.setScid(scid);
|
||||
break;
|
||||
case "log_level":
|
||||
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
|
||||
options.setLogLevel(level);
|
||||
break;
|
||||
case "audio":
|
||||
boolean audio = Boolean.parseBoolean(value);
|
||||
options.setAudio(audio);
|
||||
break;
|
||||
case "video_codec":
|
||||
VideoCodec videoCodec = VideoCodec.findByName(value);
|
||||
if (videoCodec == null) {
|
||||
throw new IllegalArgumentException("Video codec " + value + " not supported");
|
||||
}
|
||||
options.setVideoCodec(videoCodec);
|
||||
break;
|
||||
case "audio_codec":
|
||||
AudioCodec audioCodec = AudioCodec.findByName(value);
|
||||
if (audioCodec == null) {
|
||||
throw new IllegalArgumentException("Audio codec " + value + " not supported");
|
||||
}
|
||||
options.setAudioCodec(audioCodec);
|
||||
break;
|
||||
case "max_size":
|
||||
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
|
||||
options.setMaxSize(maxSize);
|
||||
@@ -219,10 +190,6 @@ public final class Server {
|
||||
int bitRate = Integer.parseInt(value);
|
||||
options.setBitRate(bitRate);
|
||||
break;
|
||||
case "audio_bit_rate":
|
||||
int audioBitRate = Integer.parseInt(value);
|
||||
options.setAudioBitRate(audioBitRate);
|
||||
break;
|
||||
case "max_fps":
|
||||
int maxFps = Integer.parseInt(value);
|
||||
options.setMaxFps(maxFps);
|
||||
@@ -264,10 +231,6 @@ public final class Server {
|
||||
options.setEncoderName(value);
|
||||
}
|
||||
break;
|
||||
case "audio_encoder_name":
|
||||
if (!value.isEmpty()) {
|
||||
options.setAudioEncoderName(value);
|
||||
}
|
||||
case "power_off_on_close":
|
||||
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
||||
@@ -300,17 +263,12 @@ public final class Server {
|
||||
boolean sendDummyByte = Boolean.parseBoolean(value);
|
||||
options.setSendDummyByte(sendDummyByte);
|
||||
break;
|
||||
case "send_codec_id":
|
||||
boolean sendCodecId = Boolean.parseBoolean(value);
|
||||
options.setSendCodecId(sendCodecId);
|
||||
break;
|
||||
case "raw_video_stream":
|
||||
boolean rawVideoStream = Boolean.parseBoolean(value);
|
||||
if (rawVideoStream) {
|
||||
options.setSendDeviceMeta(false);
|
||||
options.setSendFrameMeta(false);
|
||||
options.setSendDummyByte(false);
|
||||
options.setSendCodecId(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -338,19 +296,41 @@ public final class Server {
|
||||
return new Rect(x, y, x + width, y + height);
|
||||
}
|
||||
|
||||
private static void suggestFix(Throwable e) {
|
||||
if (e instanceof InvalidDisplayIdException) {
|
||||
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
|
||||
int[] displayIds = idie.getAvailableDisplayIds();
|
||||
if (displayIds != null && displayIds.length > 0) {
|
||||
Ln.e("Try to use one of the available display ids:");
|
||||
for (int id : displayIds) {
|
||||
Ln.e(" scrcpy --display " + id);
|
||||
}
|
||||
}
|
||||
} else if (e instanceof InvalidEncoderException) {
|
||||
InvalidEncoderException iee = (InvalidEncoderException) e;
|
||||
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
|
||||
if (encoders != null && encoders.length > 0) {
|
||||
Ln.e("Try to use one of the available encoders:");
|
||||
for (MediaCodecInfo encoder : encoders) {
|
||||
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String... args) throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Ln.e("Exception on thread " + t, e);
|
||||
suggestFix(e);
|
||||
}
|
||||
});
|
||||
|
||||
Options options = createOptions(args);
|
||||
|
||||
Ln.initLogLevel(options.getLogLevel());
|
||||
|
||||
try {
|
||||
scrcpy(options);
|
||||
} catch (ConfigurationException e) {
|
||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||
}
|
||||
scrcpy(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class Streamer {
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
|
||||
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
|
||||
|
||||
private final FileDescriptor fd;
|
||||
private final Codec codec;
|
||||
private final boolean sendCodecId;
|
||||
private final boolean sendFrameMeta;
|
||||
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
|
||||
this.fd = fd;
|
||||
this.codec = codec;
|
||||
this.sendCodecId = sendCodecId;
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
public Codec getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void writeHeader() throws IOException {
|
||||
if (sendCodecId) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codec.getId());
|
||||
buffer.flip();
|
||||
IO.writeFully(fd, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeDisableStream() throws IOException {
|
||||
// Writing 0 (32-bit) as codec-id means that the device disables the stream (because it could not capture)
|
||||
byte[] zeros = new byte[4];
|
||||
IO.writeFully(fd, zeros, 0, zeros.length);
|
||||
}
|
||||
|
||||
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
if (codec == AudioCodec.OPUS) {
|
||||
fixOpusConfigPacket(codecBuffer, bufferInfo);
|
||||
}
|
||||
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
}
|
||||
|
||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||
headerBuffer.clear();
|
||||
|
||||
long ptsAndFlags;
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
|
||||
} else {
|
||||
ptsAndFlags = bufferInfo.presentationTimeUs;
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
|
||||
}
|
||||
}
|
||||
|
||||
headerBuffer.putLong(ptsAndFlags);
|
||||
headerBuffer.putInt(packetSize);
|
||||
headerBuffer.flip();
|
||||
IO.writeFully(fd, headerBuffer);
|
||||
}
|
||||
|
||||
private static void fixOpusConfigPacket(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||
// Here is an example of the config packet received for an OPUS stream:
|
||||
//
|
||||
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
|
||||
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
|
||||
// 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....|
|
||||
// 00000020 00 00 00 |... |
|
||||
// ------------------------------------------------------------------------------
|
||||
// 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....|
|
||||
// 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS|
|
||||
// 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............|
|
||||
// 00000050 00 00 00 |...|
|
||||
//
|
||||
// Each "section" is prefixed by a 64-bit ID and a 64-bit length.
|
||||
|
||||
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||
if (!isConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (buffer.remaining() >= 16) {
|
||||
long id = buffer.getLong();
|
||||
long sizeLong = buffer.getLong();
|
||||
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
|
||||
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
|
||||
}
|
||||
int size = (int) sizeLong;
|
||||
if (id == AOPUSHDR) {
|
||||
if (buffer.remaining() < size) {
|
||||
throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")");
|
||||
}
|
||||
// Set the buffer to point to the OPUS header slice
|
||||
buffer.limit(buffer.position() + size);
|
||||
return;
|
||||
}
|
||||
|
||||
buffer.position(buffer.position() + size);
|
||||
}
|
||||
|
||||
throw new IOException("OPUS header not found");
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
||||
public enum VideoCodec implements Codec {
|
||||
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
|
||||
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
|
||||
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
|
||||
|
||||
private final int id; // 4-byte ASCII representation of the name
|
||||
private final String name;
|
||||
private final String mimeType;
|
||||
|
||||
VideoCodec(int id, String name, String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.VIDEO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public static VideoCodec findByName(String name) {
|
||||
for (VideoCodec codec : values()) {
|
||||
if (codec.name.equals(name)) {
|
||||
return codec;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.ContextWrapper;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class Workarounds {
|
||||
private Workarounds() {
|
||||
@@ -48,7 +50,7 @@ public final class Workarounds {
|
||||
Object appBindData = appBindDataConstructor.newInstance();
|
||||
|
||||
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||
applicationInfo.packageName = FakeContext.PACKAGE_NAME;
|
||||
applicationInfo.packageName = "com.genymobile.scrcpy";
|
||||
|
||||
// appBindData.appInfo = applicationInfo;
|
||||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
||||
@@ -60,10 +62,11 @@ public final class Workarounds {
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
|
||||
Application app = Application.class.newInstance();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
baseField.setAccessible(true);
|
||||
baseField.set(app, FakeContext.get());
|
||||
// Context ctx = activityThread.getSystemContext();
|
||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
||||
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
|
||||
|
||||
Application app = Instrumentation.newApplication(Application.class, ctx);
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.genymobile.scrcpy.Ln;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@@ -49,10 +48,10 @@ public class ActivityManager {
|
||||
Object[] args;
|
||||
if (getContentProviderExternalMethodNewVersion) {
|
||||
// new version
|
||||
args = new Object[]{name, Process.ROOT_UID, token, null};
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
||||
} else {
|
||||
// old version
|
||||
args = new Object[]{name, Process.ROOT_UID, token};
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token};
|
||||
}
|
||||
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
||||
Object providerHolder = method.invoke(manager, args);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -60,22 +58,22 @@ public class ClipboardManager {
|
||||
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
|
||||
}
|
||||
if (alternativeMethod) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
||||
}
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
||||
} else {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +106,11 @@ public class ClipboardManager {
|
||||
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||
}
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
||||
} else
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
}
|
||||
|
||||
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
import com.genymobile.scrcpy.SettingsException;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.AttributionSource;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@@ -55,10 +51,11 @@ public class ContentProvider implements Closeable {
|
||||
@SuppressLint("PrivateApi")
|
||||
private Method getCallMethod() throws NoSuchMethodException {
|
||||
if (callMethod == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
|
||||
try {
|
||||
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
|
||||
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
|
||||
callMethodVersion = 0;
|
||||
} else {
|
||||
} catch (NoSuchMethodException | ClassNotFoundException e0) {
|
||||
// old versions
|
||||
try {
|
||||
callMethod = provider.getClass()
|
||||
@@ -78,29 +75,40 @@ public class ContentProvider implements Closeable {
|
||||
return callMethod;
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private Object getAttributionSource()
|
||||
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
|
||||
if (attributionSource == null) {
|
||||
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
|
||||
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
|
||||
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
|
||||
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
|
||||
}
|
||||
|
||||
return attributionSource;
|
||||
}
|
||||
|
||||
private Bundle call(String callMethod, String arg, Bundle extras)
|
||||
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
||||
try {
|
||||
Method method = getCallMethod();
|
||||
Object[] args;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
|
||||
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
|
||||
} else {
|
||||
switch (callMethodVersion) {
|
||||
case 1:
|
||||
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 2:
|
||||
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
default:
|
||||
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
|
||||
break;
|
||||
}
|
||||
switch (callMethodVersion) {
|
||||
case 0:
|
||||
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 1:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 2:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
default:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
||||
break;
|
||||
}
|
||||
return (Bundle) method.invoke(provider, args);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
throw e;
|
||||
}
|
||||
@@ -139,7 +147,7 @@ public class ContentProvider implements Closeable {
|
||||
public String getValue(String table, String key) throws SettingsException {
|
||||
String method = getGetMethod(table);
|
||||
Bundle arg = new Bundle();
|
||||
arg.putInt(CALL_METHOD_USER_KEY, Process.ROOT_UID);
|
||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||
try {
|
||||
Bundle bundle = call(method, key, arg);
|
||||
if (bundle == null) {
|
||||
@@ -155,7 +163,7 @@ public class ContentProvider implements Closeable {
|
||||
public void putValue(String table, String key, String value) throws SettingsException {
|
||||
String method = getPutMethod(table);
|
||||
Bundle arg = new Bundle();
|
||||
arg.putInt(CALL_METHOD_USER_KEY, Process.ROOT_UID);
|
||||
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
|
||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||
try {
|
||||
call(method, key, arg);
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.view.InputEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -18,7 +17,6 @@ public final class InputManager {
|
||||
private Method injectInputEventMethod;
|
||||
|
||||
private static Method setDisplayIdMethod;
|
||||
private static Method setActionButtonMethod;
|
||||
|
||||
public InputManager(android.hardware.input.InputManager manager) {
|
||||
this.manager = manager;
|
||||
@@ -58,22 +56,4 @@ public final class InputManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
|
||||
if (setActionButtonMethod == null) {
|
||||
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
|
||||
}
|
||||
return setActionButtonMethod;
|
||||
}
|
||||
|
||||
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
|
||||
try {
|
||||
Method method = getSetActionButtonMethod();
|
||||
method.invoke(motionEvent, actionButton);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Cannot set action button on MotionEvent", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import java.lang.reflect.Method;
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public final class ServiceManager {
|
||||
|
||||
public static final String PACKAGE_NAME = "com.android.shell";
|
||||
public static final int USER_ID = 0;
|
||||
|
||||
private static final Method GET_SERVICE_METHOD;
|
||||
static {
|
||||
try {
|
||||
|
||||
@@ -30,8 +30,6 @@ public final class SurfaceControl {
|
||||
|
||||
private static Method getBuiltInDisplayMethod;
|
||||
private static Method setDisplayPowerModeMethod;
|
||||
private static Method getPhysicalDisplayTokenMethod;
|
||||
private static Method getPhysicalDisplayIdsMethod;
|
||||
|
||||
private SurfaceControl() {
|
||||
// only static methods
|
||||
@@ -100,6 +98,7 @@ public final class SurfaceControl {
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay() {
|
||||
|
||||
try {
|
||||
Method method = getGetBuiltInDisplayMethod();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@@ -115,40 +114,6 @@ public final class SurfaceControl {
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayTokenMethod == null) {
|
||||
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
|
||||
}
|
||||
return getPhysicalDisplayTokenMethod;
|
||||
}
|
||||
|
||||
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayTokenMethod();
|
||||
return (IBinder) method.invoke(null, physicalDisplayId);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
|
||||
if (getPhysicalDisplayIdsMethod == null) {
|
||||
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
|
||||
}
|
||||
return getPhysicalDisplayIdsMethod;
|
||||
}
|
||||
|
||||
public static long[] getPhysicalDisplayIds() {
|
||||
try {
|
||||
Method method = getGetPhysicalDisplayIdsMethod();
|
||||
return (long[]) method.invoke(null);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
|
||||
if (setDisplayPowerModeMethod == null) {
|
||||
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
|
||||
|
||||
@@ -94,8 +94,7 @@ public class ControlMessageReaderTest {
|
||||
dos.writeShort(1080);
|
||||
dos.writeShort(1920);
|
||||
dos.writeShort(0xffff); // pressure
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@@ -113,7 +112,6 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
|
||||
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
|
||||
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
|
||||
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
|
||||
}
|
||||
|
||||
|
||||
BIN
u/.ninja_deps
BIN
u/.ninja_deps
Binary file not shown.
62
u/.ninja_log
62
u/.ninja_log
@@ -1,62 +0,0 @@
|
||||
# ninja log v5
|
||||
0 243 1542027815 build.ninja ef03cd8523486e97
|
||||
0 58 1542027815 app/app@@scrcpy@exe/src_str_util.c.o 2aa692e7aa83914
|
||||
0 87 1542027815 app/app@@scrcpy@exe/src_sys_unix_net.c.o 7ea14bd07e90ff97
|
||||
0 91 1542027815 app/app@@scrcpy@exe/src_sys_unix_command.c.o dd44ba15cc3d6a7e
|
||||
1 113 1542027815 app/app@@scrcpy@exe/src_command.c.o 1eaa0f061a5c0447
|
||||
0 114 1542027815 app/app@@scrcpy@exe/src_server.c.o 8b376071b5e0aaf1
|
||||
1 117 1542027815 app/app@@scrcpy@exe/src_controller.c.o 907de440054c77e7
|
||||
1 146 1542027815 app/app@@scrcpy@exe/src_control_event.c.o fcfa5a6c322ebf8b
|
||||
91 202 1542027815 app/app@@scrcpy@exe/src_device.c.o 9ac9441f4f2e4d54
|
||||
58 215 1542027815 app/app@@scrcpy@exe/src_convert.c.o 9de268e9b915094e
|
||||
115 219 1542027815 app/app@@scrcpy@exe/src_fps_counter.c.o 22b968c51acd256b
|
||||
114 235 1542027815 app/app@@scrcpy@exe/src_file_handler.c.o 11e303a26f189d9a
|
||||
117 286 1542027815 app/app@@scrcpy@exe/src_frames.c.o 3c5c4dbee035e5ab
|
||||
88 319 1542027815 app/app@@scrcpy@exe/src_decoder.c.o 60c1438cf7786895
|
||||
202 338 1542027815 app/app@@scrcpy@exe/src_lock_util.c.o 9265bcc92f144427
|
||||
215 367 1542027815 app/app@@scrcpy@exe/src_net.c.o 718f65aa73583163
|
||||
220 408 1542027815 app/app@@scrcpy@exe/src_recorder.c.o 676a7500fb0d45cb
|
||||
1 470 1542027815 app/app@@scrcpy@exe/src_tiny_xpm.c.o 91851ad29940a4b1
|
||||
286 485 1542027815 app/app@@test_control_event_queue@exe/tests_test_control_event_queue.c.o 46bff52a98c0b5ca
|
||||
319 487 1542027815 app/app@@test_control_event_queue@exe/src_control_event.c.o 76492af89a914173
|
||||
1 488 1542027815 app/app@@scrcpy@exe/src_main.c.o e7dc8583797471c5
|
||||
367 488 1542027815 app/app@@test_control_event_serialize@exe/src_control_event.c.o fcff2e1105474edf
|
||||
0 497 1542027815 app/app@@scrcpy@exe/src_screen.c.o 329c18ec2111c8ff
|
||||
408 515 1542027815 app/app@@test_strutil@exe/tests_test_strutil.c.o 15440f4bca20c50d
|
||||
338 517 1542027815 app/app@@test_control_event_serialize@exe/tests_test_control_event_serialize.c.o baa0b48891372fcc
|
||||
470 525 1542027815 app/app@@test_strutil@exe/src_str_util.c.o fcb3a91d36e23e11
|
||||
525 561 1542027815 app/test_strutil 3448478dadf99adf
|
||||
487 582 1542027815 app/test_control_event_queue bfca00bc894d3c4f
|
||||
517 606 1542027815 app/test_control_event_serialize e06ab4ce04dd4fad
|
||||
147 638 1542027815 app/app@@scrcpy@exe/src_input_manager.c.o 1fe285b256bf5908
|
||||
236 713 1542027815 app/app@@scrcpy@exe/src_scrcpy.c.o 8b0bae90b272da98
|
||||
713 891 1542027816 app/scrcpy 8fba96817bb2802c
|
||||
485 5716 1542027820 server/scrcpy-server.jar 8511d30842df298f
|
||||
0 264 1542027826 build.ninja ef03cd8523486e97
|
||||
1 31 1542027826 app/app@@scrcpy@exe/src_fps_counter.c.o 22b968c51acd256b
|
||||
1 44 1542027826 app/app@@scrcpy@exe/src_file_handler.c.o 11e303a26f189d9a
|
||||
1 47 1542027826 app/app@@scrcpy@exe/src_controller.c.o 907de440054c77e7
|
||||
2 50 1542027826 app/app@@scrcpy@exe/src_frames.c.o 3c5c4dbee035e5ab
|
||||
2 50 1542027826 app/app@@scrcpy@exe/src_recorder.c.o 676a7500fb0d45cb
|
||||
1 65 1542027826 app/app@@scrcpy@exe/src_decoder.c.o 60c1438cf7786895
|
||||
31 82 1542027826 app/app@@scrcpy@exe/src_server.c.o 8b376071b5e0aaf1
|
||||
2 108 1542027826 app/app@@scrcpy@exe/src_input_manager.c.o 1fe285b256bf5908
|
||||
2 129 1542027826 app/app@@scrcpy@exe/src_screen.c.o 329c18ec2111c8ff
|
||||
2 162 1542027826 app/app@@scrcpy@exe/src_scrcpy.c.o 8b0bae90b272da98
|
||||
1 339 1542027826 app/app@@scrcpy@exe/src_main.c.o e7dc8583797471c5
|
||||
339 538 1542027827 app/scrcpy 8fba96817bb2802c
|
||||
44 753 1542027827 server/scrcpy-server.jar 8511d30842df298f
|
||||
0 276 1542027871 build.ninja ef03cd8523486e97
|
||||
1 37 1542027872 app/app@@scrcpy@exe/src_file_handler.c.o 11e303a26f189d9a
|
||||
1 42 1542027872 app/app@@scrcpy@exe/src_controller.c.o 907de440054c77e7
|
||||
1 45 1542027872 app/app@@scrcpy@exe/src_fps_counter.c.o 22b968c51acd256b
|
||||
2 49 1542027872 app/app@@scrcpy@exe/src_recorder.c.o 676a7500fb0d45cb
|
||||
1 52 1542027872 app/app@@scrcpy@exe/src_frames.c.o 3c5c4dbee035e5ab
|
||||
0 64 1542027872 app/app@@scrcpy@exe/src_decoder.c.o 60c1438cf7786895
|
||||
37 80 1542027872 app/app@@scrcpy@exe/src_server.c.o 8b376071b5e0aaf1
|
||||
1 128 1542027872 app/app@@scrcpy@exe/src_input_manager.c.o 1fe285b256bf5908
|
||||
2 138 1542027872 app/app@@scrcpy@exe/src_screen.c.o 329c18ec2111c8ff
|
||||
2 150 1542027872 app/app@@scrcpy@exe/src_scrcpy.c.o 8b0bae90b272da98
|
||||
1 370 1542027872 app/app@@scrcpy@exe/src_main.c.o e7dc8583797471c5
|
||||
370 578 1542027872 app/scrcpy 8fba96817bb2802c
|
||||
42 688 1542027872 server/scrcpy-server.jar 8511d30842df298f
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user