diff --git a/BUILD.md b/BUILD.md
index 87078b71..5f473ce2 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -14,7 +14,8 @@ First, you need to install the required packages:
# for Debian/Ubuntu
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
gcc git pkg-config meson ninja-build libsdl2-dev \
- libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev
+ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
+ libusb-1.0-0 libusb-1.0-0-dev
```
Then clone the repo and execute the installation script
@@ -88,11 +89,12 @@ Install the required packages from your package manager.
```bash
# runtime dependencies
-sudo apt install ffmpeg libsdl2-2.0-0 adb
+sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
# client build dependencies
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
- libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev
+ libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
+ libusb-1.0-0-dev
# server build dependencies
sudo apt install openjdk-11-jdk
@@ -114,7 +116,7 @@ pip3 install meson
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies
-sudo dnf install SDL2-devel ffms2-devel meson gcc make
+sudo dnf install SDL2-devel ffms2-devel libusb-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
@@ -268,10 +270,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- - [`scrcpy-server-v1.18`][direct-scrcpy-server]
- _(SHA-256: 641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3)_
+ - [`scrcpy-server-v1.20`][direct-scrcpy-server]
+ _(SHA-256: b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34)_
-[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18
+[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
diff --git a/DEVELOP.md b/DEVELOP.md
index d11f139e..d200c3fd 100644
--- a/DEVELOP.md
+++ b/DEVELOP.md
@@ -76,7 +76,7 @@ The server uses 3 threads:
- the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control messages_ (typically,
keyboard and mouse events) from the client;
- - the **receiver** thread (managed by the controller), sending _device messges_
+ - the **receiver** thread (managed by the controller), sending _device messages_
to the clients (currently, it is only used to send the device clipboard
content).
diff --git a/FAQ.it.md b/FAQ.it.md
index 5c5830ce..01a87091 100644
--- a/FAQ.it.md
+++ b/FAQ.it.md
@@ -139,7 +139,23 @@ Potresti anche dover configurare il [comportamento di ridimensionamento][scaling
[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
diff --git a/FAQ.md b/FAQ.md
index c1e39a39..d5f0e3ee 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -118,13 +118,17 @@ In developer options, enable:
### Special characters do not work
-Injecting text input is [limited to ASCII characters][text-input]. A trick
-allows to also inject some [accented characters][accented-characters], but
-that's all. See [#37].
+The default text injection method is [limited to ASCII characters][text-input].
+A trick allows to also inject some [accented characters][accented-characters],
+but that's all. See [#37].
+
+Since scrcpy v1.20 on Linux, it is possible to simulate a [physical
+keyboard][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
## Client issues
@@ -153,6 +157,26 @@ You may also need to configure the [scaling behavior]:
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
+### Issue with Wayland
+
+By default, SDL uses x11 on Linux. The [video driver] can be changed via the
+`SDL_VIDEODRIVER` environment variable:
+
+[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver
+
+```bash
+export SDL_VIDEODRIVER=wayland
+scrcpy
+```
+
+On some distributions (at least Fedora), the package `libdecor` must be
+installed manually.
+
+See issues [#2554] and [#2559].
+
+[#2554]: https://github.com/Genymobile/scrcpy/issues/2554
+[#2559]: https://github.com/Genymobile/scrcpy/issues/2559
+
### KWin compositor crashes
@@ -198,6 +222,27 @@ scrcpy -m 800
You could also try another [encoder](README.md#encoder).
+If you encounter this exception on Android 12, then just upgrade to scrcpy >=
+1.18 (see [#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
+
+
## Command line on Windows
Some Windows users are not familiar with the command line. Here is how to open a
@@ -238,5 +283,6 @@ to add some arguments.
This FAQ is available in other languages:
- - [Italiano (Italiano, `it`) - v1.17](FAQ.it.md)
+ - [Italiano (Italiano, `it`) - v1.19](FAQ.it.md)
- [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md)
+ - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.18](FAQ.zh-Hans.md)
diff --git a/FAQ.zh-Hans.md b/FAQ.zh-Hans.md
new file mode 100644
index 00000000..136b5f2e
--- /dev/null
+++ b/FAQ.zh-Hans.md
@@ -0,0 +1,240 @@
+只有原版的[FAQ](FAQ.md)会保持更新。
+本文根据[d6aaa5]翻译。
+
+[d6aaa5]:https://github.com/Genymobile/scrcpy/blob/d6aaa5bf9aa3710660c683b6e3e0ed971ee44af5/FAQ.md
+
+# 常见问题
+
+这里是一些常见的问题以及他们的状态。
+
+## `adb` 相关问题
+
+`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果`adb` 执行失败了, scrcpy 就无法工作。
+
+在这种情况中,将会输出这个错误:
+
+> ERROR: "adb push" returned with value 1
+
+这通常不是 _scrcpy_ 的bug,而是你的环境的问题。
+
+要找出原因,请执行以下操作:
+
+```bash
+adb devices
+```
+
+### 找不到`adb`
+
+
+你的`PATH`中需要能访问到`adb`。
+
+在Windows上,当前目录会包含在`PATH`中,并且`adb.exe`也包含在发行版中,因此它应该是开箱即用(直接解压就可以)的。
+
+
+### 设备未授权
+
+参见这里 [stackoverflow][device-unauthorized].
+
+[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
+
+
+### 未检测到设备
+
+> adb: error: failed to get feature set: no devices/emulators found
+
+确认已经正确启用 [adb debugging][enable-adb].
+
+如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上).
+
+[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
+[drivers]: https://developer.android.com/studio/run/oem-usb.html
+
+
+### 已连接多个设备
+
+如果连接了多个设备,您将遇到以下错误:
+
+> adb: error: failed to get feature set: 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]。
+
+
+[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
+
+
+## 客户端相关问题
+
+### 效果很差
+
+如果你的客户端窗口分辨率比你的设备屏幕小,则可能出现效果差的问题,尤其是在文本上(参见 [#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
+```
+
+你也可以尝试另一种 [编码器](README.md#encoder)。
+
+
+## Windows命令行
+
+一些Windows用户不熟悉命令行。以下是如何打开终端并带参数执行`scrcpy`:
+
+ 1. 按下 Windows+r,打开一个对话框。
+ 2. 输入 `cmd` 并按 Enter,这样就打开了一个终端。
+ 3. 通过输入以下命令,切换到你的 _scrcpy_ 所在的目录 (根据你的实际位置修改路径):
+
+ ```bat
+ cd C:\Users\user\Downloads\scrcpy-win64-xxx
+ ```
+
+ 然后按 Enter
+ 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/
diff --git a/README.it.md b/README.it.md
index 37416f1d..acb31a88 100644
--- a/README.it.md
+++ b/README.it.md
@@ -1,6 +1,6 @@
_Apri il [README](README.md) originale e sempre aggiornato._
-# scrcpy (v1.17)
+# scrcpy (v1.19)
Questa applicazione fornisce la visualizzazione e il controllo dei dispositivi Android collegati via USB (o [via TCP/IP][article-tcpip]). Non richiede alcun accesso _root_.
Funziona su _GNU/Linux_, _Windows_ e _macOS_.
@@ -205,10 +205,11 @@ Se anche `--max-size` è specificata, il ridimensionamento è applicato dopo il
Per bloccare l'orientamento della trasmissione:
```bash
-scrcpy --lock-video-orientation 0 # orientamento naturale
-scrcpy --lock-video-orientation 1 # 90° antiorario
-scrcpy --lock-video-orientation 2 # 180°
-scrcpy --lock-video-orientation 3 # 90° orario
+scrcpy --lock-video-orientation # orientamento iniziale (corrente)
+scrcpy --lock-video-orientation=0 # orientamento naturale
+scrcpy --lock-video-orientation=1 # 90° antiorario
+scrcpy --lock-video-orientation=2 # 180°
+scrcpy --lock-video-orientation=3 # 90° orario
```
Questo influisce sull'orientamento della registrazione.
@@ -231,7 +232,9 @@ Per elencare i codificatori disponibili puoi immettere un nome di codificatore n
scrcpy --encoder _
```
-### Registrazione
+### Cattura
+
+#### Registrazione
È possibile registrare lo schermo durante la trasmissione:
@@ -253,6 +256,75 @@ I "fotogrammi saltati" sono registrati nonostante non siano mostrati in tempo re
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
+#### v4l2loopback
+
+Su Linux è possibile inviare il flusso video ad un dispositivo v4l2 loopback, cosicchè un dispositivo Android possa essere aperto come una webcam da qualsiasi strumento compatibile con v4l2.
+
+Il modulo `v4l2loopback` deve essere installato:
+
+```bash
+sudo apt install v4l2loopback-dkms
+```
+
+Per creare un dispositvo v4l2:
+
+```bash
+sudo modprobe v4l2loopback
+```
+
+Questo creerà un nuovo dispositivo video in `/dev/videoN` dove `N` è un intero (più [opzioni](https://github.com/umlaeute/v4l2loopback#options) sono disponibili per crere più dispositivi o dispositivi con ID specifici).
+
+Per elencare i dispositvi attivati:
+
+```bash
+# necessita del pacchetto v4l-utils
+v4l2-ctl --list-devices
+
+# semplice ma potrebbe essere sufficiente
+ls /dev/video*
+```
+
+Per avviare scrcpy utilizzando un v4l2 sink:
+
+```bash
+scrcpy --v4l2-sink=/dev/videoN
+scrcpy --v4l2-sink=/dev/videoN --no-display # disabilita la finestra di trasmissione
+scrcpy --v4l2-sink=/dev/videoN -N # versione corta
+```
+
+(sostituisci `N` con l'ID del dispositivo, controlla con `ls /dev/video*`)
+
+Una volta abilitato, puoi aprire il tuo flusso video con uno strumento compatibile con v4l2:
+
+```bash
+ffplay -i /dev/videoN
+vlc v4l2:///dev/videoN # VLC potrebbe aggiungere del ritardo per il buffer
+```
+
+Per esempio potresti catturare il video in [OBS].
+
+[OBS]: https://obsproject.com/
+
+
+#### Buffering
+
+È possibile aggiungere del buffer. Questo aumenta la latenza ma riduce il jitter (vedi [#2464]).
+
+[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
+
+L'opzione è disponibile per il buffer della visualizzazione:
+
+```bash
+scrcpy --display-buffer=50 # aggiungi 50 ms di buffer per la visualizzazione
+```
+
+e per il V4L2 sink:
+
+```bash
+scrcpy --v4l2-buffer=500 # aggiungi 50 ms di buffer per il v4l2 sink
+```
+
+
### Connessione
#### Wireless
@@ -479,16 +551,6 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
-#### Renderizzare i fotogrammi scaduti
-
-Per minimizzare la latenza _scrcpy_ renderizza sempre l'ultimo fotogramma decodificato disponibile in maniera predefinita e tralascia quelli precedenti.
-
-Per forzare la renderizzazione di tutti i fotogrammi (a costo di una possibile latenza superiore), utilizzare:
-
-```bash
-scrcpy --render-expired-frames
-```
-
#### Mostrare i tocchi
Per le presentazioni può essere utile mostrare i tocchi fisici (sul dispositivo fisico).
@@ -607,14 +669,14 @@ Non c'è alcuna risposta visiva, un log è stampato nella console.
#### Trasferimento di file verso il dispositivo
-Per trasferire un file in `/sdcard/` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_.
+Per trasferire un file in `/sdcard/Download` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_.
Non c'è alcuna risposta visiva, un log è stampato nella console.
La cartella di destinazione può essere cambiata all'avvio:
```bash
-scrcpy --push-target=/sdcard/Download/
+scrcpy --push-target=/sdcard/Movies/
```
@@ -653,10 +715,10 @@ _[Super] è il pulsante Windows o Cmd._
| Rotazione schermo a sinistra | MOD+← _(sinistra)_
| Rotazione schermo a destra | MOD+→ _(destra)_
| Ridimensiona finestra a 1:1 (pixel-perfect) | MOD+g
- | Ridimensiona la finestra per rimuovere i bordi neri | MOD+w \| _Doppio click¹_
+ | Ridimensiona la finestra per rimuovere i bordi neri | MOD+w \| _Doppio click sinistro¹_
| Premi il tasto `HOME` | MOD+h \| _Click centrale_
| Premi il tasto `BACK` | MOD+b \| _Click destro²_
- | Premi il tasto `APP_SWITCH` | MOD+s
+ | Premi il tasto `APP_SWITCH` | MOD+s \| _4° click³_
| Premi il tasto `MENU` (sblocca lo schermo) | MOD+m
| Premi il tasto `VOLUME_UP` | MOD+↑ _(su)_
| Premi il tasto `VOLUME_DOWN` | MOD+↓ _(giù)_
@@ -665,18 +727,26 @@ _[Super] è il pulsante Windows o Cmd._
| Spegni lo schermo del dispositivo (continua a trasmettere) | MOD+o
| Accendi lo schermo del dispositivo | MOD+Shift+o
| Ruota lo schermo del dispositivo | MOD+r
- | Espandi il pannello delle notifiche | MOD+n
- | Chiudi il pannello delle notifiche | MOD+Shift+n
- | Copia negli appunti³ | MOD+c
- | Taglia negli appunti³ | MOD+x
- | Sincronizza gli appunti e incolla³ | MOD+v
+ | Espandi il pannello delle notifiche | MOD+n \| _5° click³_
+ | Espandi il pannello delle impostazioni | MOD+n+n \| _Doppio 5° click³_
+ | Chiudi pannelli | MOD+Shift+n
+ | Copia negli appunti⁴ | MOD+c
+ | Taglia negli appunti⁴ | MOD+x
+ | Sincronizza gli appunti e incolla⁴ | MOD+v
| Inietta il testo degli appunti del computer | MOD+Shift+v
| Abilita/Disabilita il contatore FPS (su stdout) | MOD+i
| Pizzica per zoomare | Ctrl+_click e trascina_
_¹Doppio click sui bordi neri per rimuoverli._
_²Il tasto destro accende lo schermo se era spento, preme BACK in caso contrario._
-_³Solo in Android >= 7._
+_³4° e 5° pulsante del mouse, se il tuo mouse ne dispone._
+_⁴Solo in Android >= 7._
+
+Le scorciatoie con pulsanti ripetuti sono eseguite rilasciando e premendo il pulsante una seconda volta. Per esempio, per eseguire "Espandi il pannello delle impostazioni":
+
+1. Premi e tieni premuto MOD.
+2. Poi premi due volte n.
+3. Infine rilascia MOD.
Tutte le scorciatoie Ctrl+_tasto_ sono inoltrate al dispositivo, così sono gestite dall'applicazione attiva.
diff --git a/README.jp.md b/README.jp.md
index e42c528e..a97ef765 100644
--- a/README.jp.md
+++ b/README.jp.md
@@ -1,6 +1,6 @@
_Only the original [README](README.md) is guaranteed to be up-to-date._
-# scrcpy (v1.17)
+# scrcpy (v1.19)
このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。
@@ -103,18 +103,21 @@ scoop install adb # まだ入手していない場合
brew install scrcpy
```
-`PATH`から`adb`へのアクセスが必要です。もしまだ持っていない場合:
+`PATH`からアクセス可能な`adb`が必要です。もし持っていない場合はインストールしてください。
```bash
-# Homebrew >= 2.6.0
-brew install --cask android-platform-tools
-
-# Homebrew < 2.6.0
-brew cask install android-platform-tools
+brew install android-platform-tools
```
-また、[アプリケーションをビルド][BUILD]することも可能です。
+`adb`は[MacPorts]からでもインストールできます。
+```bash
+sudo port install scrcpy
+```
+
+[MacPorts]: https://www.macports.org/
+
+また、[アプリケーションをビルド][BUILD]することも可能です。
## 実行
@@ -184,10 +187,11 @@ scrcpy --crop 1224:1440:0:0 # オフセット位置(0,0)で1224x1440
ミラーリングの向きをロックするには:
```bash
-scrcpy --lock-video-orientation 0 # 自然な向き
-scrcpy --lock-video-orientation 1 # 90°反時計回り
-scrcpy --lock-video-orientation 2 # 180°
-scrcpy --lock-video-orientation 3 # 90°時計回り
+scrcpy --lock-video-orientation # 現在の向き
+scrcpy --lock-video-orientation=0 # 自然な向き
+scrcpy --lock-video-orientation=1 # 90°反時計回り
+scrcpy --lock-video-orientation=2 # 180°
+scrcpy --lock-video-orientation=3 # 90°時計回り
```
この設定は録画の向きに影響します。
@@ -210,7 +214,9 @@ scrcpy --encoder OMX.qcom.video.encoder.avc
scrcpy --encoder _
```
-### 録画
+### キャプチャ
+
+#### 録画
ミラーリング中に画面の録画をすることが可能です:
@@ -233,6 +239,77 @@ scrcpy -Nr file.mkv
[パケット遅延のバリエーション]: 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
+```
### 接続
@@ -457,16 +534,6 @@ scrcpy -Sw
```
-#### 期限切れフレームをレンダリングする
-
-初期状態では、待ち時間を最小限にするために、_scrcpy_ は最後にデコードされたフレームをレンダリングし、前のフレームを削除します。
-
-全フレームのレンダリングを強制するには(待ち時間が長くなる可能性があります):
-
-```bash
-scrcpy --render-expired-frames
-```
-
#### タッチを表示
プレゼンテーションの場合(物理デバイス上で)物理的なタッチを表示すると便利な場合があります。
@@ -586,14 +653,14 @@ APKをインストールするには、(`.apk`で終わる)APKファイルを _s
#### デバイスにファイルを送る
-デバイスの`/sdcard/`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。
+デバイスの`/sdcard/Download`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。
見た目のフィードバックはありません。コンソールにログが出力されます。
転送先ディレクトリを起動時に変更することができます:
```bash
-scrcpy --push-target /sdcard/foo/bar/
+scrcpy --push-target=/sdcard/Movies/
```
@@ -634,7 +701,7 @@ _[Super]は通常WindowsもしくはCmdキー
| ウィンドウサイズを変更して黒い境界線を削除 | MOD+w \| _ダブルクリック¹_
| `HOME`をクリック | MOD+h \| _真ん中クリック_
| `BACK`をクリック | MOD+b \| _右クリック²_
- | `APP_SWITCH`をクリック | MOD+s
+ | `APP_SWITCH`をクリック | MOD+s \| _4クリック³_
| `MENU` (画面のアンロック)をクリック | MOD+m
| `VOLUME_UP`をクリック | MOD+↑ _(上)_
| `VOLUME_DOWN`をクリック | MOD+↓ _(下)_
@@ -643,7 +710,8 @@ _[Super]は通常WindowsもしくはCmdキー
| デバイス画面をオフにする(ミラーリングしたまま) | MOD+o
| デバイス画面をオンにする | MOD+Shift+o
| デバイス画面を回転する | MOD+r
- | 通知パネルを展開する | MOD+n
+ | 通知パネルを展開する | MOD+n \| _5ボタンクリック³_
+ | 設定パネルを展開する | MOD+n+n \| _5ダブルクリック³_
| 通知パネルを折りたたむ | MOD+Shift+n
| クリップボードへのコピー³ | MOD+c
| クリップボードへのカット³ | MOD+x
@@ -654,11 +722,17 @@ _[Super]は通常WindowsもしくはCmdキー
_¹黒い境界線を削除するため、境界線上でダブルクリック_
_²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._
-_³Android 7以上のみ._
+_³4と5はマウスのボタンです、もしあなたのマウスにボタンがあれば使えます._
+_⁴Android 7以上のみ._
+
+キーを繰り返すショートカットはキーを離して2回目を押したら実行されます。例えば「設定パネルを展開する」を実行する場合は以下のように操作する。
+
+ 1. MOD キーを押し、押したままにする.
+ 2. その後に nキーを2回押す.
+ 3. 最後に MODキーを離す.
全てのCtrl+_キー_ ショートカットはデバイスに転送されます、そのためアクティブなアプリケーションによって処理されます。
-
## カスタムパス
特定の _adb_ バイナリを使用する場合、そのパスを環境変数`ADB`で構成します:
diff --git a/README.md b/README.md
index cf41756b..cbcfd45a 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,37 @@
-# scrcpy (v1.18)
+# scrcpy (v1.20)
+
+
[Read in another language](#translations)
-This application provides display and control of Android devices connected on
-USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
+This application provides display and control of Android devices connected via
+USB (or [over TCP/IP](#wireless)). It does not require any _root_ access.
It works on _GNU/Linux_, _Windows_ and _macOS_.

It focuses on:
- - **lightness** (native, displays only the device screen)
- - **performance** (30~60fps)
- - **quality** (1920×1080 or above)
- - **low latency** ([35~70ms][lowlatency])
- - **low startup time** (~1 second to display the first image)
- - **non-intrusiveness** (nothing is left installed on the device)
+ - **lightness**: native, displays only the device screen
+ - **performance**: 30~120fps, depending on the device
+ - **quality**: 1920×1080 or above
+ - **low latency**: [35~70ms][lowlatency]
+ - **low startup time**: ~1 second to display the first image
+ - **non-intrusiveness**: nothing is left installed on the device
+ - **user benefits**: no account, no ads, no internet required
+ - **freedom**: free and open source software
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
+Its features include:
+ - [recording](#recording)
+ - mirroring with [device screen off](#turn-screen-off)
+ - [copy-paste](#copy-paste) in both directions
+ - [configurable quality](#capture-configuration)
+ - device screen [as a webcam (V4L2)](#v4l2loopback) (Linux-only)
+ - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid)
+ (Linux-only)
+ - and more…
## Requirements
@@ -88,10 +101,10 @@ process][BUILD_simple]).
For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available:
- - [`scrcpy-win64-v1.18.zip`][direct-win64]
- _(SHA-256: 37212f5087fe6f3e258f1d44fa5c02207496b30e1d7ec442cbcf8358910a5c63)_
+ - [`scrcpy-win64-v1.20.zip`][direct-win64]
+ _(SHA-256: 548532b616288bcaeceff6881ad5e6f0928e5ae2b48c380385f03627401cfdba)_
-[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-win64-v1.18.zip
+[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-win64-v1.20.zip
It is also available in [Chocolatey]:
@@ -318,7 +331,27 @@ vlc v4l2:///dev/videoN # VLC might add some buffering delay
For example, you could capture the video within [OBS].
-[OBS]: https://obsproject.com/fr
+[OBS]: https://obsproject.com/
+
+
+#### Buffering
+
+It is possible to add buffering. This increases latency but reduces jitter (see
+[#2464]).
+
+[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
+
+The option is available for display buffering:
+
+```bash
+scrcpy --display-buffer=50 # add 50 ms buffering for display
+```
+
+and V4L2 sink:
+
+```bash
+scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink
+```
### Connection
@@ -562,6 +595,14 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
+#### Power off on close
+
+To turn the device screen off when closing scrcpy:
+
+```bash
+scrcpy --power-off-on-close
+```
+
#### Show touches
@@ -653,6 +694,39 @@ content (if supported by the app) relative to the center of the screen.
Concretely, scrcpy generates additional touch events from a "virtual finger" at
a location inverted through the center of the screen.
+#### Physical keyboard simulation (HID)
+
+By default, scrcpy uses Android key or text injection: it works everywhere, but
+is limited to ASCII.
+
+On Linux, scrcpy can simulate a physical USB keyboard on Android to provide a
+better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual
+keyboard is disabled and it works for all characters and IME.
+
+[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
+
+However, it only works if the device is connected by USB, and is currently only
+supported on Linux.
+
+To enable this mode:
+
+```bash
+scrcpy --hid-keyboard
+scrcpy -K # short version
+```
+
+If it fails for some reason (for example because the device is not connected via
+USB), it automatically fallbacks to the default mode (with a log in the
+console). This allows to use the same command line options when connected over
+USB and TCP/IP.
+
+In this mode, raw key events (scancodes) are sent to the device, independently
+of the host key mapping. Therefore, if your keyboard layout does not match, it
+must be configured on the Android device, in Settings → System → Languages and
+input → [Physical keyboard].
+
+[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
+
#### Text injection preference
@@ -672,6 +746,9 @@ scrcpy --prefer-text
(but this will break keyboard behavior in games)
+This option has no effect on HID keyboard (all key events are sent as
+scancodes in this mode).
+
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
@@ -687,6 +764,9 @@ To avoid forwarding repeated key events:
scrcpy --no-key-repeat
```
+This option has no effect on HID keyboard (key repeat is handled by Android
+directly in this mode).
+
#### Right-click and middle-click
@@ -771,7 +851,7 @@ _[Super] is typically the Windows or Cmd key._
| Turn device screen on | MOD+Shift+o
| Rotate device screen | MOD+r
| Expand notification panel | MOD+n \| _5th-click³_
- | Expand settings panel | MOD+n+n \| _Double-5th-click³_
+ | Expand settings panel | MOD+n+n \| _Double-5th-click³_
| Collapse panels | MOD+Shift+n
| Copy to clipboard⁴ | MOD+c
| Cut to clipboard⁴ | MOD+x
@@ -779,6 +859,8 @@ _[Super] is typically the Windows or Cmd key._
| Inject computer clipboard text | MOD+Shift+v
| Enable/disable FPS counter (on stdout) | MOD+i
| Pinch-to-zoom | Ctrl+_click-and-move_
+ | Drag & drop APK file | Install APK from computer
+ | Drag & drop non-APK file | [Push file to device](#push-file-to-device)
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
@@ -808,7 +890,7 @@ ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server` file, configure its path in
`SCRCPY_SERVER_PATH`.
-[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
+To override the icon, configure its path in `SCRCPY_ICON_PATH`.
## Why _scrcpy_?
@@ -869,12 +951,13 @@ This README is available in other languages:
- [Русский (Russian, `ru`) - v1.18](README.ru.md)
- [Indonesian (Indonesia, `id`) - v1.16](README.id.md)
-- [Italiano (Italiano, `it`) - v1.17](README.it.md)
-- [日本語 (Japanese, `jp`) - v1.17](README.jp.md)
+- [Italiano (Italiano, `it`) - v1.19](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.17](README.pt-br.md)
+- [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md)
- [Español (Spanish, `sp`) - v1.17](README.sp.md)
-- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md)
+- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.20](README.zh-Hans.md)
- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md)
+- [Turkish (Turkish, `tr`) - v1.18](README.tr.md)
Only this README file is guaranteed to be up-to-date.
diff --git a/README.pt-br.md b/README.pt-br.md
index 3549f0fb..cdfeafeb 100644
--- a/README.pt-br.md
+++ b/README.pt-br.md
@@ -1,6 +1,6 @@
_Apenas o [README](README.md) original é garantido estar atualizado._
-# scrcpy (v1.17)
+# 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_.
@@ -38,6 +38,18 @@ controlá-lo usando teclado e mouse.
+### 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):
@@ -67,9 +79,7 @@ Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link].
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
-Você também pode [compilar o app manualmente][BUILD] (não se preocupe, não é tão
-difícil).
-
+Você também pode [compilar o app manualmente][BUILD] ([processo simplificado][BUILD_simple]).
### Windows
@@ -113,13 +123,18 @@ brew install scrcpy
Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem:
```bash
-# Homebrew >= 2.6.0
-brew install --cask android-platform-tools
-
-# Homebrew < 2.6.0
-brew cask install android-platform-tools
+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].
@@ -195,10 +210,11 @@ Se `--max-size` também for especificado, o redimensionamento é aplicado após
Para travar a orientação do espelhamento:
```bash
-scrcpy --lock-video-orientation 0 # orientação natural
-scrcpy --lock-video-orientation 1 # 90° sentido anti-horário
-scrcpy --lock-video-orientation 2 # 180°
-scrcpy --lock-video-orientation 3 # 90° sentido horário
+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.
@@ -222,7 +238,9 @@ erro dará os encoders disponíveis:
scrcpy --encoder _
```
-### Gravando
+### Captura
+
+#### Gravando
É possível gravar a tela enquanto ocorre o espelhamento:
@@ -246,6 +264,79 @@ 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
@@ -488,18 +579,6 @@ scrcpy -Sw
```
-#### Renderizar frames expirados
-
-Por padrão, para minimizar a latência, _scrcpy_ sempre renderiza o último frame decodificado
-disponível, e descarta o anterior.
-
-Para forçar a renderização de todos os frames (com o custo de um possível aumento de
-latência), use:
-
-```bash
-scrcpy --render-expired-frames
-```
-
#### Mostrar toques
Para apresentações, pode ser útil mostrar toques físicos (no dispositivo
@@ -647,7 +726,7 @@ Não existe feedback visual, um log é imprimido no console.
#### Enviar arquivo para dispositivo
-Para enviar um arquivo para `/sdcard/` no dispositivo, arraste e solte um arquivo (não-APK) para a
+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.
@@ -694,12 +773,12 @@ _[Super] é tipicamente a tecla Windows ou Cmd.
| Mudar modo de tela cheia | MOD+f
| Rotacionar display para esquerda | MOD+← _(esquerda)_
| Rotacionar display para direita | MOD+→ _(direita)_
- | Redimensionar janela para 1:1 (pixel-perfect) | MOD+g
- | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo¹_
+ | Redimensionar janela para 1:1 (pixel-perfeito) | MOD+g
+ | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo-esquerdo¹_
| Clicar em `HOME` | MOD+h \| _Clique-do-meio_
| Clicar em `BACK` | MOD+b \| _Clique-direito²_
- | Clicar em `APP_SWITCH` | MOD+s
- | Clicar em `MENU` (desbloquear tela | MOD+m
+ | Clicar em `APP_SWITCH` | MOD+s \| _Clique-do-4.°³_
+ | Clicar em `MENU` (desbloquear tela) | MOD+m
| Clicar em `VOLUME_UP` | MOD+↑ _(cima)_
| Clicar em `VOLUME_DOWN` | MOD+↓ _(baixo)_
| Clicar em `POWER` | MOD+p
@@ -707,18 +786,27 @@ _[Super] é tipicamente a tecla Windows ou Cmd.
| Desligar tela do dispositivo (continuar espelhando) | MOD+o
| Ligar tela do dispositivo | MOD+Shift+o
| Rotacionar tela do dispositivo | MOD+r
- | Expandir painel de notificação | MOD+n
- | Colapsar painel de notificação | MOD+Shift+n
- | Copiar para área de transferência³ | MOD+c
- | Recortar para área de transferência³ | MOD+x
- | Sincronizar áreas de transferência e colar³ | MOD+v
+ | Expandir painel de notificação | MOD+n \| _Clique-do-5.°³_
+ | Expandir painel de configurção | MOD+n+n \| _Clique-duplo-do-5.°³_
+ | Colapsar paineis | MOD+Shift+n
+ | Copiar para área de transferência⁴ | MOD+c
+ | Recortar para área de transferência⁴ | MOD+x
+ | Sincronizar áreas de transferência e colar⁴ | MOD+v
| Injetar texto da área de transferência do computador | MOD+Shift+v
| Ativar/desativar contador de FPS (em stdout) | MOD+i
- | Pinçar para dar zoom | Ctrl+_clicar-e-mover_
+ | Pinçar para dar zoom | Ctrl+_Clicar-e-mover_
-_¹Clique-duplo em bordas pretas para removê-las._
-_²Clique-direito liga a tela se ela estiver desligada, pressiona BACK caso contrário._
-_³Apenas em Android >= 7._
+_¹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 MOD.
+ 2. Depois click duas vezes n.
+ 3. Finalmente, solte MOD.
Todos os atalhos Ctrl+_tecla_ são encaminhados para o dispositivo, para que eles sejam
tratados pela aplicação ativa.
@@ -729,7 +817,9 @@ tratados pela aplicação ativa.
Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente
`ADB`:
- ADB=/caminho/para/adb scrcpy
+```bash
+ADB=/caminho/para/adb scrcpy
+```
Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em
`SCRCPY_SERVER_PATH`.
@@ -751,8 +841,6 @@ Um colega me desafiou a encontrar um nome tão impronunciável quanto [gnirehtet
Veja [BUILD].
-[BUILD]: BUILD.md
-
## Problemas comuns
diff --git a/README.tr.md b/README.tr.md
new file mode 100644
index 00000000..15c56b27
--- /dev/null
+++ b/README.tr.md
@@ -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
+
+
+
+### Ö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 MOD+f 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 MOD+←_(sol)_ ve
+MOD+→ _(sağ)_ ile dinamik olarak değiştirilebilir.
+
+_scrcpy_'de 3 farklı döndürme olduğuna dikkat edin:
+
+- MOD+r 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 MOD+←/MOD+→)
+ 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 MOD+o kısayolunu kullanabilirsiniz.
+
+Tekrar açmak için ise MOD+Shift+o 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 MOD+p), 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
+
+MOD+r 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 Ctrl kısayolları cihaza iletilir:
+
+- Ctrl+c genelde kopyalar
+- Ctrl+x genelde keser
+- Ctrl+v 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_ Ctrl+c 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ü):
+
+- MOD+c `KOPYALA`
+- MOD+x `KES`
+- MOD+v `YAPIŞTIR` (bilgisayar ve cihaz arasındaki
+ pano senkronizasyonundan sonra)
+
+Bunlara ek olarak, MOD+Shift+v 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
+(Ctrl+v ya da MOD+v 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
+Ctrl+v ve MOD+v tuşları da
+pano içeriğini tuş basma eylemleri şeklinde gönderir
+(MOD+Shift+v ile aynı şekilde).
+
+#### İki parmak ile yakınlaştırma
+
+"İki parmak ile yakınlaştırma" için: Ctrl+_tıkla-ve-sürükle_.
+
+Daha açıklayıcı şekilde, Ctrl 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, MOD kısayol tamamlayıcısıdır. Varsayılan olarak
+(sol) Alt veya (sol) Super 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
+```
+
+_[Super] tuşu genelde Windows veya Cmd tuşudur._
+
+[super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
+
+| Action | Shortcut |
+| ------------------------------------------------ | :-------------------------------------------------------- |
+| Tam ekran modunu değiştirme | MOD+f |
+| Ekranı sola çevirme | MOD+← _(sol)_ |
+| Ekranı sağa çevirme | MOD+→ _(sağ)_ |
+| Pencereyi 1:1 oranına çevirme (pixel-perfect) | MOD+g |
+| Penceredeki siyah kenarlıkları kaldırma | MOD+w \| _Çift-sol-tık¹_ |
+| `ANA EKRAN` tuşu | MOD+h \| _Orta-tık_ |
+| `GERİ` tuşu | MOD+b \| _Sağ-tık²_ |
+| `UYGULAMA_DEĞİŞTİR` tuşu | MOD+s \| _4.tık³_ |
+| `MENÜ` tuşu (ekran kilidini açma) | MOD+m |
+| `SES_AÇ` tuşu | MOD+↑ _(yukarı)_ |
+| `SES_KIS` tuşu | MOD+↓ _(aşağı)_ |
+| `GÜÇ` tuşu | MOD+p |
+| Gücü açma | _Sağ-tık²_ |
+| Cihaz ekranını kapatma (ekran yakalama durmadan) | MOD+o |
+| Cihaz ekranını açma | MOD+Shift+o |
+| Cihaz ekranını dönderme | MOD+r |
+| Bildirim panelini genişletme | MOD+n \| _5.tık³_ |
+| Ayarlar panelini genişletme | MOD+n+n \| _Çift-5.tık³_ |
+| Panelleri kapatma | MOD+Shift+n |
+| Panoya kopyalama⁴ | MOD+c |
+| Panoya kesme⁴ | MOD+x |
+| Panoları senkronize ederek yapıştırma⁴ | MOD+v |
+| Bilgisayar panosundaki metini girme | MOD+Shift+v |
+| FPS sayacını açma/kapatma (terminalde) | MOD+i |
+| İki parmakla yakınlaştırma | Ctrl+_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. MOD tuşuna basın ve basılı tutun.
+2. n tuşuna iki defa basın.
+3. MOD tuşuna basmayı bırakın.
+
+Tüm Ctrl+_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-2021 Romain Vimont
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+## 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/
diff --git a/README.zh-Hans.md b/README.zh-Hans.md
index bdd8023c..b96d6d5a 100644
--- a/README.zh-Hans.md
+++ b/README.zh-Hans.md
@@ -2,27 +2,41 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._
只有原版的[README](README.md)会保持最新。
-本文根据[ed130e05]进行翻译。
+Current version is based on [65b023a]
-[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md
+本文根据[65b023a]进行翻译。
-# scrcpy (v1.17)
+[65b023a]: https://github.com/Genymobile/scrcpy/blob/65b023ac6d586593193fd5290f65e25603b68e02/README.md
+
+# scrcpy (v1.20)
+
+
本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。

-它专注于:
+本应用专注于:
- - **轻量** (原生,仅显示设备屏幕)
- - **性能** (30~60fps)
- - **质量** (分辨率可达 1920×1080 或更高)
- - **低延迟** ([35~70ms][lowlatency])
- - **快速启动** (最快 1 秒内即可显示第一帧)
- - **无侵入性** (不会在设备上遗留任何程序)
+ - **轻量**: 原生,仅显示设备屏幕
+ - **性能**: 30~120fps,取决于设备
+ - **质量**: 分辨率可达 1920×1080 或更高
+ - **低延迟**: [35~70ms][lowlatency]
+ - **快速启动**: 最快 1 秒内即可显示第一帧
+ - **无侵入性**: 不会在设备上遗留任何程序
+ - **用户利益**: 无需帐号,无广告,无需联网
+ - **自由**: 自由和开源软件
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
+功能:
+ - [屏幕录制](#屏幕录制)
+ - 镜像时[关闭设备屏幕](#关闭设备屏幕)
+ - 双向[复制粘贴](#复制粘贴)
+ - [可配置显示质量](#采集设置)
+ - 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux)
+ - [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux)
+ - 更多 ……
## 系统要求
@@ -41,6 +55,17 @@ _Only the original [README](README.md) is guaranteed to be up-to-date._
+### 概要
+
+ - Linux: `apt install scrcpy`
+ - Windows: [下载][direct-win64]
+ - macOS: `brew install scrcpy`
+
+从源代码编译: [构建][BUILD] ([简化过程][BUILD_simple])
+
+[BUILD]: BUILD.md
+[BUILD_simple]: BUILD.md#simple
+
### Linux
在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上:
@@ -70,13 +95,12 @@ apt install scrcpy
[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
-您也可以[自行构建][BUILD] (不必担心,这并不困难)。
-
+您也可以[自行构建][BUILD] ([简化过程][BUILD_simple])。
### Windows
-在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
+在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。
- [README](README.md#windows)
@@ -114,13 +138,17 @@ brew install scrcpy
你还需要在 `PATH` 内有 `adb`。如果还没有:
```bash
-# Homebrew >= 2.6.0
-brew install --cask android-platform-tools
-
-# Homebrew < 2.6.0
-brew cask install android-platform-tools
+brew install android-platform-tools
```
+或者通过 [MacPorts],该方法同时设置好 adb:
+
+```bash
+sudo port install scrcpy
+```
+
+[MacPorts]: https://www.macports.org/
+
您也可以[自行构建][BUILD]。
@@ -140,7 +168,7 @@ scrcpy --help
## 功能介绍
-### 捕获设置
+### 采集设置
#### 降低分辨率
@@ -158,7 +186,7 @@ scrcpy -m 1024 # 简写
#### 修改码率
-默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps):
+默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps):
```bash
scrcpy --bit-rate 2M
@@ -167,7 +195,7 @@ scrcpy -b 2M # 简写
#### 限制帧率
-要限制捕获的帧率:
+要限制采集的帧率:
```bash
scrcpy --max-fps 15
@@ -194,10 +222,11 @@ scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素
要锁定镜像画面的方向:
```bash
-scrcpy --lock-video-orientation 0 # 自然方向
-scrcpy --lock-video-orientation 1 # 逆时针旋转 90°
-scrcpy --lock-video-orientation 2 # 180°
-scrcpy --lock-video-orientation 3 # 顺时针旋转 90°
+scrcpy --lock-video-orientation # 初始(目前)方向
+scrcpy --lock-video-orientation=0 # 自然方向
+scrcpy --lock-video-orientation=1 # 逆时针旋转 90°
+scrcpy --lock-video-orientation=2 # 180°
+scrcpy --lock-video-orientation=3 # 顺时针旋转 90°
```
只影响录制的方向。
@@ -219,7 +248,9 @@ scrcpy --encoder OMX.qcom.video.encoder.avc
scrcpy --encoder _
```
-### 屏幕录制
+### 采集
+
+#### 屏幕录制
可以在镜像的同时录制视频:
@@ -241,6 +272,75 @@ scrcpy -Nr file.mkv
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
+#### v4l2loopback
+
+在 Linux 上,可以将视频流发送至 v4l2 回环 (loopback) 设备,因此可以使用任何 v4l2 工具像摄像头一样打开安卓设备。
+
+需安装 `v4l2loopback` 模块:
+
+```bash
+sudo apt install v4l2loopback-dkms
+```
+
+创建一个 v4l2 设备:
+
+```bash
+sudo modprobe v4l2loopback
+```
+
+这样会在 `/dev/videoN` 创建一个新的视频设备,其中 `N` 是整数。 ([更多选项](https://github.com/umlaeute/v4l2loopback#options) 可以用来创建多个设备或者特定 ID 的设备)。
+
+列出已启用的设备:
+
+```bash
+# 需要 v4l-utils 包
+v4l2-ctl --list-devices
+
+# 简单但或许足够
+ls /dev/video*
+```
+
+使用一个 v4l2 漏开启 scrcpy:
+
+```bash
+scrcpy --v4l2-sink=/dev/videoN
+scrcpy --v4l2-sink=/dev/videoN --no-display # 禁用窗口镜像
+scrcpy --v4l2-sink=/dev/videoN -N # 简写
+```
+
+(将 `N` 替换为设备 ID,使用 `ls /dev/video*` 命令查看)
+
+启用之后,可以使用 v4l2 工具打开视频流:
+
+```bash
+ffplay -i /dev/videoN
+vlc v4l2:///dev/videoN # VLC 可能存在一些缓冲延迟
+```
+
+例如,可以在 [OBS] 中采集视频。
+
+[OBS]: https://obsproject.com/
+
+
+#### 缓冲
+
+可以加入缓冲,会增加延迟,但可以减少抖动 (见 [#2464])。
+
+[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
+
+对于显示缓冲:
+
+```bash
+scrcpy --display-buffer=50 # 为显示增加 50 毫秒的缓冲
+```
+
+对于 V4L2 漏:
+
+```bash
+scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲
+```
+
+
### 连接
#### 无线
@@ -249,16 +349,17 @@ _Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接
1. 将设备和电脑连接至同一 Wi-Fi。
2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令:
+
```bash
adb shell ip route | awk '{print $9}'
```
-3. 启用设备的网络 adb 功能 `adb tcpip 5555`。
+3. 启用设备的网络 adb 功能: `adb tcpip 5555`。
4. 断开设备的 USB 连接。
-5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_.
+5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_。
6. 正常运行 `scrcpy`。
-可能需要降低码率和分辨率:
+可能降低码率和分辨率会更好一些:
```bash
scrcpy --bit-rate 2M --max-size 800
@@ -327,7 +428,7 @@ scrcpy --force-adb-forward
```
-类似无线网络连接,可能需要降低画面质量:
+类似地,对于无线连接,可能需要降低画面质量:
```
scrcpy -b2M -m800 --max-fps 15
@@ -353,7 +454,7 @@ scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
#### 无边框
-关闭边框:
+禁用窗口边框:
```bash
scrcpy --window-borderless
@@ -369,7 +470,7 @@ scrcpy --always-on-top
#### 全屏
-您可以通过如下命令直接全屏启动scrcpy:
+您可以通过如下命令直接全屏启动 scrcpy:
```bash
scrcpy --fullscreen
@@ -394,7 +495,7 @@ scrcpy --rotation 1
也可以使用 MOD+← _(左箭头)_ 和 MOD+→ _(右箭头)_ 随时更改。
-需要注意的是, _scrcpy_ 有三个不同的方向:
+需要注意的是, _scrcpy_ 中有三类旋转方向:
- MOD+r 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。
- [`--lock-video-orientation`](#锁定屏幕方向) 改变镜像的朝向 (设备传输到电脑的画面的朝向)。这会影响录制。
- `--rotation` (或 MOD+←/MOD+→) 只旋转窗口的内容。这只影响显示,不影响录制。
@@ -404,7 +505,7 @@ scrcpy --rotation 1
#### 只读
-禁用电脑对设备的控制 (如键盘输入、鼠标事件和文件拖放):
+禁用电脑对设备的控制 (任何可与设备交互的方式:如键盘输入、鼠标事件和文件拖放):
```bash
scrcpy --no-control
@@ -430,14 +531,14 @@ adb shell dumpsys display # 在输出中搜索 “mDisplayId=”
#### 保持常亮
-阻止设备在连接时休眠:
+阻止设备在连接时一段时间后休眠:
```bash
scrcpy --stay-awake
scrcpy -w
```
-程序关闭时会恢复设备原来的设置。
+scrcpy 关闭时会恢复设备原来的设置。
#### 关闭设备屏幕
@@ -451,7 +552,7 @@ scrcpy -S
或者在任何时候按 MOD+o。
-要重新打开屏幕,按下 MOD+Shift+o.
+要重新打开屏幕,按下 MOD+Shift+o。
在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 MOD+p),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。
@@ -462,20 +563,17 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw
```
+#### 退出时息屏
-#### 渲染过期帧
-
-默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。
-
-强制渲染所有帧 (可能导致延迟变高):
+scrcpy 退出时关闭设备屏幕:
```bash
-scrcpy --render-expired-frames
+scrcpy --power-off-on-close
```
#### 显示触摸
-在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。
+在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。
Android 在 _开发者选项_ 中提供了这项功能。
@@ -538,10 +636,32 @@ scrcpy --disable-screensaver
更准确的说,在按住鼠标左键时按住 Ctrl。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。
-实际上,_scrcpy_ 会在以屏幕中心对称的位置上生成由“虚拟手指”发出的额外触摸事件。
+实际上,_scrcpy_ 会在关于屏幕中心对称的位置上用“虚拟手指”发出触摸事件。
+#### 物理键盘模拟 (HID)
-#### 文字注入偏好
+默认情况下,scrcpy 使用安卓按键或文本注入,这在任何情况都可以使用,但仅限于ASCII字符。
+
+在 Linux 上,scrcpy 可以模拟为 Android 上的物理 USB 键盘,以提供更好地输入体验 (使用 [USB HID over AOAv2][hid-aoav2]):禁用虚拟键盘,并适用于任何字符和输入法。
+
+[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
+
+不过,这种方法仅支持 USB 连接以及 Linux平台。
+
+启用 HID 模式:
+
+```bash
+scrcpy --hid-keyboard
+scrcpy -K # 简写
+```
+
+如果失败了 (如设备未通过 USB 连接),则自动回退至默认模式 (终端中会输出日志)。这即允许通过 USB 和 TCP/IP 连接时使用相同的命令行参数。
+
+在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。
+
+[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
+
+#### 文本注入偏好
打字的时候,系统会产生两种[事件][textevents]:
- _按键事件_ ,代表一个按键被按下或松开。
@@ -557,13 +677,15 @@ scrcpy --prefer-text
(这会导致键盘在游戏中工作不正常)
+该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。
+
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
#### 按键重复
-默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这可能会导致性能问题。
+默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这通常没有实际用途,且可能会导致性能问题。
避免转发重复按键事件:
@@ -571,10 +693,11 @@ scrcpy --prefer-text
scrcpy --no-key-repeat
```
+该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。
#### 右键和中键
-默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
+默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备:
```bash
scrcpy --forward-all-clicks
@@ -587,27 +710,27 @@ scrcpy --forward-all-clicks
将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。
-该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。
+不会有视觉反馈,终端会输出一条日志。
#### 将文件推送至设备
-要推送文件到设备的 `/sdcard/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
+要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。
-该操作没有可见的响应,只会在控制台输出日志。
+不会有视觉反馈,终端会输出一条日志。
在启动时可以修改目标目录:
```bash
-scrcpy --push-target /sdcard/foo/bar/
+scrcpy --push-target=/sdcard/Movies/
```
### 音频转发
-_Scrcpy_ 不支持音频。请使用 [sndcpy].
+_Scrcpy_ 不支持音频。请使用 [sndcpy]。
-另外请阅读 [issue #14]。
+另见 [issue #14]。
[sndcpy]: https://github.com/rom1v/sndcpy
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
@@ -632,36 +755,46 @@ _[Super] 键通常是指 Windows 或 Cmd 键。
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
- | 操作 | 快捷键 |
- | --------------------------------- | :------------------------------------------- |
- | 全屏 | MOD+f |
- | 向左旋转屏幕 | MOD+← _(左箭头)_ |
- | 向右旋转屏幕 | MOD+→ _(右箭头)_ |
- | 将窗口大小重置为1:1 (匹配像素) | MOD+g |
- | 将窗口大小重置为消除黑边 | MOD+w \| _双击¹_ |
- | 点按 `主屏幕` | MOD+h \| _鼠标中键_ |
- | 点按 `返回` | MOD+b \| _鼠标右键²_ |
- | 点按 `切换应用` | MOD+s |
- | 点按 `菜单` (解锁屏幕) | MOD+m |
- | 点按 `音量+` | MOD+↑ _(上箭头)_ |
- | 点按 `音量-` | MOD+↓ _(下箭头)_ |
- | 点按 `电源` | MOD+p |
- | 打开屏幕 | _鼠标右键²_ |
- | 关闭设备屏幕 (但继续在电脑上显示) | MOD+o |
- | 打开设备屏幕 | MOD+Shift+o |
- | 旋转设备屏幕 | MOD+r |
- | 展开通知面板 | MOD+n |
- | 收起通知面板 | MOD+Shift+n |
- | 复制到剪贴板³ | MOD+c |
- | 剪切到剪贴板³ | MOD+x |
- | 同步剪贴板并粘贴³ | MOD+v |
- | 注入电脑剪贴板文本 | MOD+Shift+v |
- | 打开/关闭FPS显示 (在 stdout) | MOD+i |
- | 捏拉缩放 | Ctrl+_按住并移动鼠标_ |
+ | 操作 | 快捷键
+ | --------------------------------- | :-------------------------------------------
+ | 全屏 | MOD+f
+ | 向左旋转屏幕 | MOD+← _(左箭头)_
+ | 向右旋转屏幕 | MOD+→ _(右箭头)_
+ | 将窗口大小重置为1:1 (匹配像素) | MOD+g
+ | 将窗口大小重置为消除黑边 | MOD+w \| _双击左键¹_
+ | 点按 `主屏幕` | MOD+h \| _中键_
+ | 点按 `返回` | MOD+b \| _右键²_
+ | 点按 `切换应用` | MOD+s \| _第4键³_
+ | 点按 `菜单` (解锁屏幕) | MOD+m
+ | 点按 `音量+` | MOD+↑ _(上箭头)_
+ | 点按 `音量-` | MOD+↓ _(下箭头)_
+ | 点按 `电源` | MOD+p
+ | 打开屏幕 | _鼠标右键²_
+ | 关闭设备屏幕 (但继续在电脑上显示) | MOD+o
+ | 打开设备屏幕 | MOD+Shift+o
+ | 旋转设备屏幕 | MOD+r
+ | 展开通知面板 | MOD+n \| _第5键³_
+ | 展开设置面板 | MOD+n+n \| _双击第5键³_
+ | 收起通知面板 | MOD+Shift+n
+ | 复制到剪贴板⁴ | MOD+c
+ | 剪切到剪贴板⁴ | MOD+x
+ | 同步剪贴板并粘贴⁴ | MOD+v
+ | 注入电脑剪贴板文本 | MOD+Shift+v
+ | 打开/关闭FPS显示 (至标准输出) | MOD+i
+ | 捏拉缩放 | Ctrl+_按住并移动鼠标_
+ | 拖放 APK 文件 | 从电脑安装 APK 文件
+ | 拖放非 APK 文件 | [将文件推送至设备](#push-file-to-device)
-_¹双击黑边可以去除黑边_
-_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
-_³需要安卓版本 Android >= 7。_
+_¹双击黑边可以去除黑边。_
+_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_
+_³鼠标的第4键和第5键。_
+_⁴需要安卓版本 Android >= 7。_
+
+有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”:
+
+ 1. 按下 MOD 不放。
+ 2. 双击 n。
+ 3. 松开 MOD。
所有的 Ctrl+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。
@@ -670,18 +803,20 @@ _³需要安卓版本 Android >= 7。_
要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`:
- ADB=/path/to/adb scrcpy
+```bash
+ADB=/path/to/adb scrcpy
+```
要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。
-[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
+要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。
## 为什么叫 _scrcpy_ ?
一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。
-[`strcpy`] 复制一个 **str**ing; `scrcpy` 复制一个 **scr**een。
+[`strcpy`] 复制一个 **str**ing (字符串); `scrcpy` 复制一个 **scr**een (屏幕)。
[gnirehtet]: https://github.com/Genymobile/gnirehtet
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
@@ -689,14 +824,12 @@ _³需要安卓版本 Android >= 7。_
## 如何构建?
-请查看[BUILD]。
-
-[BUILD]: BUILD.md
+请查看 [BUILD]。
## 常见问题
-请查看[FAQ](FAQ.md)。
+请查看 [FAQ](FAQ.md)。
## 开发者
diff --git a/app/meson.build b/app/meson.build
index 0663c641..4894babc 100644
--- a/app/meson.build
+++ b/app/meson.build
@@ -1,36 +1,54 @@
src = [
'src/main.c',
'src/adb.c',
+ 'src/adb_tunnel.c',
'src/cli.c',
+ 'src/clock.c',
'src/compat.c',
'src/control_msg.c',
'src/controller.c',
'src/decoder.c',
'src/device_msg.c',
- 'src/event_converter.c',
+ 'src/icon.c',
'src/file_handler.c',
'src/fps_counter.c',
+ 'src/frame_buffer.c',
'src/input_manager.c',
+ 'src/keyboard_inject.c',
+ 'src/mouse_inject.c',
'src/opengl.c',
+ 'src/options.c',
'src/receiver.c',
'src/recorder.c',
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/stream.c',
- 'src/tiny_xpm.c',
'src/video_buffer.c',
+ 'src/util/file.c',
+ 'src/util/intr.c',
'src/util/log.c',
'src/util/net.c',
+ 'src/util/net_intr.c',
'src/util/process.c',
- 'src/util/str_util.c',
+ 'src/util/process_intr.c',
+ 'src/util/strbuf.c',
+ 'src/util/str.c',
+ 'src/util/term.c',
'src/util/thread.c',
+ 'src/util/tick.c',
]
if host_machine.system() == 'windows'
- src += [ 'src/sys/win/process.c' ]
+ src += [
+ 'src/sys/win/file.c',
+ 'src/sys/win/process.c',
+ ]
else
- src += [ 'src/sys/unix/process.c' ]
+ src += [
+ 'src/sys/unix/file.c',
+ 'src/sys/unix/process.c',
+ ]
endif
v4l2_support = host_machine.system() == 'linux'
@@ -38,6 +56,14 @@ if v4l2_support
src += [ 'src/v4l2_sink.c' ]
endif
+aoa_hid_support = host_machine.system() == 'linux'
+if aoa_hid_support
+ src += [
+ 'src/aoa_hid.c',
+ 'src/hid_keyboard.c',
+ ]
+endif
+
check_functions = [
'strdup'
]
@@ -58,8 +84,11 @@ if not get_option('crossbuild_windows')
dependencies += dependency('libavdevice')
endif
-else
+ if aoa_hid_support
+ dependencies += dependency('libusb-1.0')
+ endif
+else
# cross-compile mingw32 build (from Linux to Windows)
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin'
@@ -136,6 +165,9 @@ conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == '
# enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support)
+# enable HID over AOA support (linux only)
+conf.set('HAVE_AOA_HID', aoa_hid_support)
+
configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src')
@@ -147,6 +179,9 @@ executable('scrcpy', src,
c_args: [])
install_man('scrcpy.1')
+install_data('../data/icon.png',
+ rename: 'scrcpy.png',
+ install_dir: 'share/icons/hicolor/256x256/apps')
### TESTS
@@ -163,12 +198,20 @@ if get_option('buildtype') == 'debug'
['test_cli', [
'tests/test_cli.c',
'src/cli.c',
- 'src/util/str_util.c',
+ 'src/options.c',
+ 'src/util/str.c',
+ 'src/util/strbuf.c',
+ 'src/util/term.c',
+ ]],
+ ['test_clock', [
+ 'tests/test_clock.c',
+ 'src/clock.c',
]],
['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
- 'src/util/str_util.c',
+ 'src/util/str.c',
+ 'src/util/strbuf.c',
]],
['test_device_msg_deserialize', [
'tests/test_device_msg_deserialize.c',
@@ -177,9 +220,14 @@ if get_option('buildtype') == 'debug'
['test_queue', [
'tests/test_queue.c',
]],
- ['test_strutil', [
- 'tests/test_strutil.c',
- 'src/util/str_util.c',
+ ['test_strbuf', [
+ 'tests/test_strbuf.c',
+ 'src/util/strbuf.c',
+ ]],
+ ['test_str', [
+ 'tests/test_str.c',
+ 'src/util/str.c',
+ 'src/util/strbuf.c',
]],
]
diff --git a/app/scrcpy.1 b/app/scrcpy.1
index 62dc9677..399fd172 100644
--- a/app/scrcpy.1
+++ b/app/scrcpy.1
@@ -56,6 +56,12 @@ The list of possible display ids can be listed by "adb shell dumpsys display"
Default is 0.
+.TP
+.BI "\-\-display\-buffer ms
+Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
+
+Default is 0 (no buffering).
+
.TP
.BI "\-\-encoder " name
Use a specific MediaCodec encoder (must be a H.264 encoder).
@@ -76,6 +82,14 @@ Start in fullscreen.
.B \-h, \-\-help
Print this help.
+.TP
+.B \-K, \-\-hid\-keyboard
+Simulate a physical keyboard by using HID over AOAv2.
+
+This provides a better experience for IME users, and allows to generate non-ASCII characters, contrary to the default injection method.
+
+It may only work over USB, and is currently only supported on Linux.
+
.TP
.B \-\-legacy\-paste
Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v).
@@ -83,8 +97,8 @@ 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
-.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 otation counterclockwise.
+.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".
@@ -122,6 +136,10 @@ Set the TCP port (range) used by the client to listen.
Default is 27183:27199.
+.TP
+.B \-\-power\-off\-on\-close
+Turn the device screen off when closing scrcpy.
+
.TP
.B \-\-prefer\-text
Inject alpha characters and space as text events instead of key events.
@@ -189,7 +207,15 @@ It only shows physical touches (not clicks from scrcpy).
.BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device.
-It requires to lock the video orientation (see --lock-video-orientation).
+It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
+
+.TP
+.BI "\-\-v4l2-buffer " ms
+Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
+
+This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
+
+Default is 0 (no buffering).
.TP
.BI "\-V, \-\-verbosity " value
@@ -240,7 +266,7 @@ Default is 0 (automatic).
.SH SHORTCUTS
In the following list, MOD is the shortcut modifier. By default, it's (left)
-Alt or (left) Super, but it can be configured by \-\-shortcut-mod (see above).
+Alt or (left) Super, but it can be configured by \fB\-\-shortcut\-mod\fR (see above).
.TP
.B MOD+f
@@ -342,6 +368,10 @@ Pinch-to-zoom from the center of the screen
.B Drag & drop APK file
Install APK from computer
+.TP
+.B Drag & drop non-APK file
+Push file to device (see \fB\-\-push\-target\fR)
+
.SH Environment variables
diff --git a/app/src/adb.c b/app/src/adb.c
index 5bb9df30..6251174e 100644
--- a/app/src/adb.c
+++ b/app/src/adb.c
@@ -5,8 +5,9 @@
#include
#include
+#include "util/file.h"
#include "util/log.h"
-#include "util/str_util.h"
+#include "util/str.h"
static const char *adb_command;
@@ -68,7 +69,7 @@ show_adb_installation_msg() {
{"pacman", "pacman -S android-tools"},
};
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
- if (search_executable(pkg_managers[i].binary)) {
+ if (sc_file_executable_exists(pkg_managers[i].binary)) {
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
return;
}
@@ -80,7 +81,7 @@ show_adb_installation_msg() {
}
static void
-show_adb_err_msg(enum process_result err, const char *const argv[]) {
+show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
#define MAX_COMMAND_STRING_LEN 1024
char *buf = malloc(MAX_COMMAND_STRING_LEN);
if (!buf) {
@@ -89,18 +90,18 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
}
switch (err) {
- case PROCESS_ERROR_GENERIC:
+ case SC_PROCESS_ERROR_GENERIC:
argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN);
LOGE("Failed to execute: %s", buf);
break;
- case PROCESS_ERROR_MISSING_BINARY:
+ case SC_PROCESS_ERROR_MISSING_BINARY:
argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN);
LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)");
show_adb_installation_msg();
break;
- case PROCESS_SUCCESS:
+ case SC_PROCESS_SUCCESS:
// do nothing
break;
}
@@ -108,14 +109,15 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
free(buf);
}
-process_t
-adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
+sc_pid
+adb_execute_p(const char *serial, const char *const adb_cmd[],
+ size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr) {
int i;
- process_t process;
+ sc_pid pid;
const char **argv = malloc((len + 4) * sizeof(*argv));
if (!argv) {
- return PROCESS_NONE;
+ return SC_PROCESS_NONE;
}
argv[0] = get_adb_command();
@@ -129,17 +131,23 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
memcpy(&argv[i], adb_cmd, len * sizeof(const char *));
argv[len + i] = NULL;
- enum process_result r = process_execute(argv, &process);
- if (r != PROCESS_SUCCESS) {
+ enum sc_process_result r =
+ sc_process_execute_p(argv, &pid, pin, pout, perr);
+ if (r != SC_PROCESS_SUCCESS) {
show_adb_err_msg(r, argv);
- process = PROCESS_NONE;
+ pid = SC_PROCESS_NONE;
}
free(argv);
- return process;
+ return pid;
}
-process_t
+sc_pid
+adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
+ return adb_execute_p(serial, adb_cmd, len, NULL, NULL, NULL);
+}
+
+sc_pid
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name) {
char local[4 + 5 + 1]; // tcp:PORT
@@ -150,7 +158,7 @@ adb_forward(const char *serial, uint16_t local_port,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
-process_t
+sc_pid
adb_forward_remove(const char *serial, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port);
@@ -158,7 +166,7 @@ adb_forward_remove(const char *serial, uint16_t local_port) {
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
-process_t
+sc_pid
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
@@ -169,7 +177,7 @@ adb_reverse(const char *serial, const char *device_socket_name,
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
-process_t
+sc_pid
adb_reverse_remove(const char *serial, const char *device_socket_name) {
char remote[108 + 14 + 1]; // localabstract:NAME
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
@@ -177,50 +185,93 @@ adb_reverse_remove(const char *serial, const char *device_socket_name) {
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
-process_t
+sc_pid
adb_push(const char *serial, const char *local, const char *remote) {
#ifdef __WINDOWS__
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
- local = strquote(local);
+ local = sc_str_quote(local);
if (!local) {
- return PROCESS_NONE;
+ return SC_PROCESS_NONE;
}
- remote = strquote(remote);
+ remote = sc_str_quote(remote);
if (!remote) {
free((void *) local);
- return PROCESS_NONE;
+ return SC_PROCESS_NONE;
}
#endif
const char *const adb_cmd[] = {"push", local, remote};
- process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
+ sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) remote);
free((void *) local);
#endif
- return proc;
+ return pid;
}
-process_t
+sc_pid
adb_install(const char *serial, const char *local) {
#ifdef __WINDOWS__
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
- local = strquote(local);
+ local = sc_str_quote(local);
if (!local) {
- return PROCESS_NONE;
+ return SC_PROCESS_NONE;
}
#endif
const char *const adb_cmd[] = {"install", "-r", local};
- process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
+ sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__
free((void *) local);
#endif
- return proc;
+ return pid;
+}
+
+static ssize_t
+adb_execute_for_output(const char *serial, const char *const adb_cmd[],
+ size_t adb_cmd_len, char *buf, size_t buf_len,
+ const char *name) {
+ sc_pipe pout;
+ sc_pid pid = adb_execute_p(serial, adb_cmd, adb_cmd_len, NULL, &pout, NULL);
+
+ ssize_t r = sc_pipe_read_all(pout, buf, buf_len);
+ sc_pipe_close(pout);
+
+ if (!sc_process_check_success(pid, name, true)) {
+ return -1;
+ }
+
+ return r;
+}
+
+static size_t
+truncate_first_line(char *data, size_t len) {
+ data[len - 1] = '\0';
+ char *eol = strpbrk(data, "\r\n");
+ if (eol) {
+ *eol = '\0';
+ len = eol - data;
+ }
+ return len;
+}
+
+char *
+adb_get_serialno(void) {
+ char buf[128];
+
+ const char *const adb_cmd[] = {"get-serialno"};
+ ssize_t r = adb_execute_for_output(NULL, adb_cmd, ARRAY_LEN(adb_cmd),
+ buf, sizeof(buf), "get-serialno");
+ if (r <= 0) {
+ return NULL;
+ }
+
+ truncate_first_line(buf, r);
+ return strdup(buf);
}
diff --git a/app/src/adb.h b/app/src/adb.h
index e27f34fa..085b3e6b 100644
--- a/app/src/adb.h
+++ b/app/src/adb.h
@@ -8,27 +8,35 @@
#include "util/process.h"
-process_t
+sc_pid
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
-process_t
+sc_pid
+adb_execute_p(const char *serial, const char *const adb_cmd[],
+ size_t len, sc_pipe *pin, sc_pipe *pout, sc_pipe *perr);
+
+sc_pid
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name);
-process_t
+sc_pid
adb_forward_remove(const char *serial, uint16_t local_port);
-process_t
+sc_pid
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
-process_t
+sc_pid
adb_reverse_remove(const char *serial, const char *device_socket_name);
-process_t
+sc_pid
adb_push(const char *serial, const char *local, const char *remote);
-process_t
+sc_pid
adb_install(const char *serial, const char *local);
+// Return the result of "adb get-serialno".
+char *
+adb_get_serialno(void);
+
#endif
diff --git a/app/src/adb_tunnel.c b/app/src/adb_tunnel.c
new file mode 100644
index 00000000..f02eb83e
--- /dev/null
+++ b/app/src/adb_tunnel.c
@@ -0,0 +1,192 @@
+#include "adb_tunnel.h"
+
+#include
+
+#include "adb.h"
+#include "util/log.h"
+#include "util/net_intr.h"
+#include "util/process_intr.h"
+
+#define SC_SOCKET_NAME "scrcpy"
+
+static bool
+enable_tunnel_reverse(struct sc_intr *intr, const char *serial,
+ uint16_t local_port) {
+ sc_pid pid = adb_reverse(serial, SC_SOCKET_NAME, local_port);
+ return sc_process_check_success_intr(intr, pid, "adb reverse");
+}
+
+static bool
+disable_tunnel_reverse(struct sc_intr *intr, const char *serial) {
+ sc_pid pid = adb_reverse_remove(serial, SC_SOCKET_NAME);
+ return sc_process_check_success_intr(intr, pid, "adb reverse --remove");
+}
+
+static bool
+enable_tunnel_forward(struct sc_intr *intr, const char *serial,
+ uint16_t local_port) {
+ sc_pid pid = adb_forward(serial, local_port, SC_SOCKET_NAME);
+ return sc_process_check_success_intr(intr, pid, "adb forward");
+}
+
+static bool
+disable_tunnel_forward(struct sc_intr *intr, const char *serial,
+ uint16_t local_port) {
+ sc_pid pid = adb_forward_remove(serial, local_port);
+ return sc_process_check_success_intr(intr, pid, "adb forward --remove");
+}
+
+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);
+}
+
+static bool
+enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
+ struct sc_intr *intr, const char *serial,
+ struct sc_port_range port_range) {
+ uint16_t port = port_range.first;
+ for (;;) {
+ if (!enable_tunnel_reverse(intr, serial, port)) {
+ // the command itself failed, it will fail on any port
+ return false;
+ }
+
+ // At the application level, the device part is "the server" because it
+ // serves video stream and control. However, at the network level, the
+ // client listens and the server connects to the client. That way, the
+ // client can listen before starting the server app, so there is no
+ // need to try to connect until the server socket is listening on the
+ // device.
+ sc_socket server_socket = net_socket();
+ if (server_socket != SC_SOCKET_NONE) {
+ bool ok = listen_on_port(intr, server_socket, port);
+ if (ok) {
+ // success
+ tunnel->server_socket = server_socket;
+ tunnel->local_port = port;
+ tunnel->enabled = true;
+ return true;
+ }
+
+ net_close(server_socket);
+ }
+
+ if (sc_intr_is_interrupted(intr)) {
+ // Stop immediately
+ return false;
+ }
+
+ // failure, disable tunnel and try another port
+ if (!disable_tunnel_reverse(intr, serial)) {
+ LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
+ }
+
+ // check before incrementing to avoid overflow on port 65535
+ if (port < port_range.last) {
+ LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
+ port, (uint16_t) (port + 1));
+ port++;
+ continue;
+ }
+
+ if (port_range.first == port_range.last) {
+ LOGE("Could not listen on port %" PRIu16, port_range.first);
+ } else {
+ LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
+ port_range.first, port_range.last);
+ }
+ return false;
+ }
+}
+
+static bool
+enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
+ struct sc_intr *intr, const char *serial,
+ struct sc_port_range port_range) {
+ tunnel->forward = true;
+
+ uint16_t port = port_range.first;
+ for (;;) {
+ if (enable_tunnel_forward(intr, serial, port)) {
+ // success
+ tunnel->local_port = port;
+ tunnel->enabled = true;
+ return true;
+ }
+
+ if (sc_intr_is_interrupted(intr)) {
+ // Stop immediately
+ return false;
+ }
+
+ if (port < port_range.last) {
+ LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
+ port, (uint16_t) (port + 1));
+ port++;
+ continue;
+ }
+
+ if (port_range.first == port_range.last) {
+ LOGE("Could not forward port %" PRIu16, port_range.first);
+ } else {
+ LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
+ port_range.first, port_range.last);
+ }
+ return false;
+ }
+}
+
+void
+sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
+ tunnel->enabled = false;
+ tunnel->forward = false;
+ tunnel->server_socket = SC_SOCKET_NONE;
+ tunnel->local_port = 0;
+}
+
+bool
+sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
+ 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, port_range)) {
+ return true;
+ }
+
+ // if "adb reverse" does not work (e.g. over "adb connect"), it
+ // fallbacks to "adb forward", so the app socket is the client
+
+ LOGW("'adb reverse' failed, fallback to 'adb forward'");
+ }
+
+ return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range);
+}
+
+bool
+sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
+ const char *serial) {
+ assert(tunnel->enabled);
+
+ bool ret;
+ if (tunnel->forward) {
+ ret = disable_tunnel_forward(intr, serial, tunnel->local_port);
+ } else {
+ ret = disable_tunnel_reverse(intr, serial);
+
+ assert(tunnel->server_socket != SC_SOCKET_NONE);
+ if (!net_close(tunnel->server_socket)) {
+ LOGW("Could not close server socket");
+ }
+
+ // server_socket is never used anymore
+ }
+
+ // Consider tunnel disabled even if the command failed
+ tunnel->enabled = false;
+
+ return ret;
+}
diff --git a/app/src/adb_tunnel.h b/app/src/adb_tunnel.h
new file mode 100644
index 00000000..12e3cf17
--- /dev/null
+++ b/app/src/adb_tunnel.h
@@ -0,0 +1,47 @@
+#ifndef SC_ADB_TUNNEL_H
+#define SC_ADB_TUNNEL_H
+
+#include "common.h"
+
+#include
+#include
+
+#include "options.h"
+#include "util/intr.h"
+#include "util/net.h"
+
+struct sc_adb_tunnel {
+ bool enabled;
+ bool forward; // use "adb forward" instead of "adb reverse"
+ sc_socket server_socket; // only used if !forward
+ uint16_t local_port;
+};
+
+/**
+ * Initialize the adb tunnel struct to default values
+ */
+void
+sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
+
+/**
+ * Open a tunnel
+ *
+ * Blocking calls may be interrupted asynchronously via `intr`.
+ *
+ * If `force_adb_forward` is not set, then attempts to set up an "adb reverse"
+ * tunnel first. Only if it fails (typical on old Android version connected via
+ * TCP/IP), use "adb forward".
+ */
+bool
+sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
+ 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);
+
+#endif
diff --git a/app/src/aoa_hid.c b/app/src/aoa_hid.c
new file mode 100644
index 00000000..4c0b2bda
--- /dev/null
+++ b/app/src/aoa_hid.c
@@ -0,0 +1,385 @@
+#include "util/log.h"
+
+#include
+#include
+
+#include "aoa_hid.h"
+
+// See .
+#define ACCESSORY_REGISTER_HID 54
+#define ACCESSORY_SET_HID_REPORT_DESC 56
+#define ACCESSORY_SEND_HID_EVENT 57
+#define ACCESSORY_UNREGISTER_HID 55
+
+#define DEFAULT_TIMEOUT 1000
+
+static void
+sc_hid_event_log(const struct sc_hid_event *event) {
+ // HID Event: [00] FF FF FF FF...
+ assert(event->size);
+ unsigned buffer_size = event->size * 3 + 1;
+ char *buffer = malloc(buffer_size);
+ if (!buffer) {
+ return;
+ }
+ for (unsigned i = 0; i < event->size; ++i) {
+ snprintf(buffer + i * 3, 4, " %02x", event->buffer[i]);
+ }
+ LOGV("HID Event: [%d]%s", event->accessory_id, buffer);
+ free(buffer);
+}
+
+void
+sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
+ unsigned char *buffer, uint16_t buffer_size) {
+ hid_event->accessory_id = accessory_id;
+ hid_event->buffer = buffer;
+ hid_event->size = buffer_size;
+ hid_event->delay = 0;
+}
+
+void
+sc_hid_event_destroy(struct sc_hid_event *hid_event) {
+ free(hid_event->buffer);
+}
+
+static inline void
+log_libusb_error(enum libusb_error errcode) {
+ LOGW("libusb error: %s", libusb_strerror(errcode));
+}
+
+static bool
+accept_device(libusb_device *device, const char *serial) {
+ // do not log any USB error in this function, it is expected that many USB
+ // devices available on the computer have permission restrictions
+
+ struct libusb_device_descriptor desc;
+ libusb_get_device_descriptor(device, &desc);
+
+ if (!desc.iSerialNumber) {
+ return false;
+ }
+
+ libusb_device_handle *handle;
+ int result = libusb_open(device, &handle);
+ if (result < 0) {
+ return false;
+ }
+
+ char buffer[128];
+ result = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber,
+ (unsigned char *) buffer,
+ sizeof(buffer));
+ libusb_close(handle);
+ if (result < 0) {
+ return false;
+ }
+
+ buffer[sizeof(buffer) - 1] = '\0'; // just in case
+
+ // accept the device if its serial matches
+ return !strcmp(buffer, serial);
+}
+
+static libusb_device *
+sc_aoa_find_usb_device(const char *serial) {
+ if (!serial) {
+ return NULL;
+ }
+
+ libusb_device **list;
+ libusb_device *result = NULL;
+ ssize_t count = libusb_get_device_list(NULL, &list);
+ if (count < 0) {
+ log_libusb_error((enum libusb_error) count);
+ return NULL;
+ }
+
+ for (size_t i = 0; i < (size_t) count; ++i) {
+ libusb_device *device = list[i];
+
+ if (accept_device(device, serial)) {
+ result = libusb_ref_device(device);
+ break;
+ }
+ }
+ libusb_free_device_list(list, 1);
+ return result;
+}
+
+static int
+sc_aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) {
+ int result = libusb_open(device, handle);
+ if (result < 0) {
+ log_libusb_error((enum libusb_error) result);
+ return result;
+ }
+ return 0;
+}
+
+bool
+sc_aoa_init(struct sc_aoa *aoa, const char *serial) {
+ cbuf_init(&aoa->queue);
+
+ if (!sc_mutex_init(&aoa->mutex)) {
+ return false;
+ }
+
+ if (!sc_cond_init(&aoa->event_cond)) {
+ sc_mutex_destroy(&aoa->mutex);
+ return false;
+ }
+
+ if (libusb_init(&aoa->usb_context) != LIBUSB_SUCCESS) {
+ sc_cond_destroy(&aoa->event_cond);
+ sc_mutex_destroy(&aoa->mutex);
+ return false;
+ }
+
+ aoa->usb_device = sc_aoa_find_usb_device(serial);
+ if (!aoa->usb_device) {
+ LOGW("USB device of serial %s not found", serial);
+ libusb_exit(aoa->usb_context);
+ sc_mutex_destroy(&aoa->mutex);
+ sc_cond_destroy(&aoa->event_cond);
+ return false;
+ }
+
+ if (sc_aoa_open_usb_handle(aoa->usb_device, &aoa->usb_handle) < 0) {
+ LOGW("Open USB handle failed");
+ libusb_unref_device(aoa->usb_device);
+ libusb_exit(aoa->usb_context);
+ sc_cond_destroy(&aoa->event_cond);
+ sc_mutex_destroy(&aoa->mutex);
+ return false;
+ }
+
+ aoa->stopped = false;
+
+ return true;
+}
+
+void
+sc_aoa_destroy(struct sc_aoa *aoa) {
+ // Destroy remaining events
+ struct sc_hid_event event;
+ while (cbuf_take(&aoa->queue, &event)) {
+ sc_hid_event_destroy(&event);
+ }
+
+ libusb_close(aoa->usb_handle);
+ libusb_unref_device(aoa->usb_device);
+ libusb_exit(aoa->usb_context);
+ sc_cond_destroy(&aoa->event_cond);
+ sc_mutex_destroy(&aoa->mutex);
+}
+
+static bool
+sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id,
+ uint16_t report_desc_size) {
+ uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
+ uint8_t request = ACCESSORY_REGISTER_HID;
+ //
+ // value (arg0): accessory assigned ID for the HID device
+ // index (arg1): total length of the HID report descriptor
+ uint16_t value = accessory_id;
+ uint16_t index = report_desc_size;
+ unsigned char *buffer = NULL;
+ uint16_t length = 0;
+ int result = libusb_control_transfer(aoa->usb_handle, request_type, request,
+ value, index, buffer, length,
+ DEFAULT_TIMEOUT);
+ if (result < 0) {
+ log_libusb_error((enum libusb_error) result);
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
+ const unsigned char *report_desc,
+ uint16_t report_desc_size) {
+ uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
+ uint8_t request = ACCESSORY_SET_HID_REPORT_DESC;
+ /**
+ * If the HID descriptor is longer than the endpoint zero max packet size,
+ * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC
+ * commands. The data for the descriptor must be sent sequentially
+ * if multiple packets are needed.
+ *
+ *
+ * libusb handles packet abstraction internally, so we don't need to care
+ * about bMaxPacketSize0 here.
+ *
+ * See
+ */
+ // value (arg0): accessory assigned ID for the HID device
+ // index (arg1): offset of data (buffer) in descriptor
+ uint16_t value = accessory_id;
+ uint16_t index = 0;
+ // libusb_control_transfer expects a pointer to non-const
+ unsigned char *buffer = (unsigned char *) report_desc;
+ uint16_t length = report_desc_size;
+ int result = libusb_control_transfer(aoa->usb_handle, request_type, request,
+ value, index, buffer, length,
+ DEFAULT_TIMEOUT);
+ if (result < 0) {
+ log_libusb_error((enum libusb_error) result);
+ return false;
+ }
+
+ return true;
+}
+
+bool
+sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
+ const unsigned char *report_desc, uint16_t report_desc_size) {
+ bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size);
+ if (!ok) {
+ return false;
+ }
+
+ ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc,
+ report_desc_size);
+ if (!ok) {
+ if (!sc_aoa_unregister_hid(aoa, accessory_id)) {
+ LOGW("Could not unregister HID");
+ }
+ return false;
+ }
+
+ return true;
+}
+
+static bool
+sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) {
+ uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
+ uint8_t request = ACCESSORY_SEND_HID_EVENT;
+ //
+ // value (arg0): accessory assigned ID for the HID device
+ // index (arg1): 0 (unused)
+ uint16_t value = event->accessory_id;
+ uint16_t index = 0;
+ unsigned char *buffer = event->buffer;
+ uint16_t length = event->size;
+ int result = libusb_control_transfer(aoa->usb_handle, request_type, request,
+ value, index, buffer, length,
+ DEFAULT_TIMEOUT);
+ if (result < 0) {
+ log_libusb_error((enum libusb_error) result);
+ return false;
+ }
+
+ return true;
+}
+
+bool
+sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) {
+ uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
+ uint8_t request = ACCESSORY_UNREGISTER_HID;
+ //
+ // value (arg0): accessory assigned ID for the HID device
+ // index (arg1): 0
+ uint16_t value = accessory_id;
+ uint16_t index = 0;
+ unsigned char *buffer = NULL;
+ uint16_t length = 0;
+ int result = libusb_control_transfer(aoa->usb_handle, request_type, request,
+ value, index, buffer, length,
+ DEFAULT_TIMEOUT);
+ if (result < 0) {
+ log_libusb_error((enum libusb_error) result);
+ return false;
+ }
+
+ return true;
+}
+
+bool
+sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) {
+ if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
+ sc_hid_event_log(event);
+ }
+
+ sc_mutex_lock(&aoa->mutex);
+ bool was_empty = cbuf_is_empty(&aoa->queue);
+ bool res = cbuf_push(&aoa->queue, *event);
+ if (was_empty) {
+ sc_cond_signal(&aoa->event_cond);
+ }
+ sc_mutex_unlock(&aoa->mutex);
+ return res;
+}
+
+static int
+run_aoa_thread(void *data) {
+ struct sc_aoa *aoa = data;
+
+ for (;;) {
+ sc_mutex_lock(&aoa->mutex);
+ while (!aoa->stopped && cbuf_is_empty(&aoa->queue)) {
+ sc_cond_wait(&aoa->event_cond, &aoa->mutex);
+ }
+ if (aoa->stopped) {
+ // Stop immediately, do not process further events
+ sc_mutex_unlock(&aoa->mutex);
+ break;
+ }
+ struct sc_hid_event event;
+ bool non_empty = cbuf_take(&aoa->queue, &event);
+ assert(non_empty);
+ (void) non_empty;
+
+ assert(event.delay >= 0);
+ if (event.delay) {
+ // Wait during the specified delay before injecting the HID event
+ sc_tick deadline = sc_tick_now() + event.delay;
+ bool timed_out = false;
+ while (!aoa->stopped && !timed_out) {
+ timed_out = !sc_cond_timedwait(&aoa->event_cond, &aoa->mutex,
+ deadline);
+ }
+ if (aoa->stopped) {
+ sc_mutex_unlock(&aoa->mutex);
+ break;
+ }
+ }
+
+ sc_mutex_unlock(&aoa->mutex);
+
+ bool ok = sc_aoa_send_hid_event(aoa, &event);
+ sc_hid_event_destroy(&event);
+ if (!ok) {
+ LOGW("Could not send HID event to USB device");
+ }
+ }
+ return 0;
+}
+
+bool
+sc_aoa_start(struct sc_aoa *aoa) {
+ LOGD("Starting AOA thread");
+
+ bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "aoa_thread", aoa);
+ if (!ok) {
+ LOGC("Could not start AOA thread");
+ return false;
+ }
+
+ return true;
+}
+
+void
+sc_aoa_stop(struct sc_aoa *aoa) {
+ sc_mutex_lock(&aoa->mutex);
+ aoa->stopped = true;
+ sc_cond_signal(&aoa->event_cond);
+ sc_mutex_unlock(&aoa->mutex);
+}
+
+void
+sc_aoa_join(struct sc_aoa *aoa) {
+ sc_thread_join(&aoa->thread, NULL);
+}
diff --git a/app/src/aoa_hid.h b/app/src/aoa_hid.h
new file mode 100644
index 00000000..24cef502
--- /dev/null
+++ b/app/src/aoa_hid.h
@@ -0,0 +1,66 @@
+#ifndef SC_AOA_HID_H
+#define SC_AOA_HID_H
+
+#include
+#include
+
+#include
+
+#include "util/cbuf.h"
+#include "util/thread.h"
+#include "util/tick.h"
+
+struct sc_hid_event {
+ uint16_t accessory_id;
+ unsigned char *buffer;
+ uint16_t size;
+ sc_tick delay;
+};
+
+// Takes ownership of buffer
+void
+sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
+ unsigned char *buffer, uint16_t buffer_size);
+
+void
+sc_hid_event_destroy(struct sc_hid_event *hid_event);
+
+struct sc_hid_event_queue CBUF(struct sc_hid_event, 64);
+
+struct sc_aoa {
+ libusb_context *usb_context;
+ libusb_device *usb_device;
+ libusb_device_handle *usb_handle;
+ sc_thread thread;
+ sc_mutex mutex;
+ sc_cond event_cond;
+ bool stopped;
+ struct sc_hid_event_queue queue;
+};
+
+bool
+sc_aoa_init(struct sc_aoa *aoa, const char *serial);
+
+void
+sc_aoa_destroy(struct sc_aoa *aoa);
+
+bool
+sc_aoa_start(struct sc_aoa *aoa);
+
+void
+sc_aoa_stop(struct sc_aoa *aoa);
+
+void
+sc_aoa_join(struct sc_aoa *aoa);
+
+bool
+sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
+ const unsigned char *report_desc, uint16_t report_desc_size);
+
+bool
+sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id);
+
+bool
+sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event);
+
+#endif
diff --git a/app/src/cli.c b/app/src/cli.c
index 3eab8d27..1550c706 100644
--- a/app/src/cli.c
+++ b/app/src/cli.c
@@ -4,308 +4,773 @@
#include
#include
#include
+#include
#include
-#include "scrcpy.h"
+#include "options.h"
#include "util/log.h"
-#include "util/str_util.h"
+#include "util/str.h"
+#include "util/strbuf.h"
+#include "util/term.h"
#define STR_IMPL_(x) #x
#define STR(x) STR_IMPL_(x)
+#define OPT_RENDER_EXPIRED_FRAMES 1000
+#define OPT_WINDOW_TITLE 1001
+#define OPT_PUSH_TARGET 1002
+#define OPT_ALWAYS_ON_TOP 1003
+#define OPT_CROP 1004
+#define OPT_RECORD_FORMAT 1005
+#define OPT_PREFER_TEXT 1006
+#define OPT_WINDOW_X 1007
+#define OPT_WINDOW_Y 1008
+#define OPT_WINDOW_WIDTH 1009
+#define OPT_WINDOW_HEIGHT 1010
+#define OPT_WINDOW_BORDERLESS 1011
+#define OPT_MAX_FPS 1012
+#define OPT_LOCK_VIDEO_ORIENTATION 1013
+#define OPT_DISPLAY_ID 1014
+#define OPT_ROTATION 1015
+#define OPT_RENDER_DRIVER 1016
+#define OPT_NO_MIPMAPS 1017
+#define OPT_CODEC_OPTIONS 1018
+#define OPT_FORCE_ADB_FORWARD 1019
+#define OPT_DISABLE_SCREENSAVER 1020
+#define OPT_SHORTCUT_MOD 1021
+#define OPT_NO_KEY_REPEAT 1022
+#define OPT_FORWARD_ALL_CLICKS 1023
+#define OPT_LEGACY_PASTE 1024
+#define OPT_ENCODER_NAME 1025
+#define OPT_POWER_OFF_ON_CLOSE 1026
+#define OPT_V4L2_SINK 1027
+#define OPT_DISPLAY_BUFFER 1028
+#define OPT_V4L2_BUFFER 1029
+
+struct sc_option {
+ char shortopt;
+ int longopt_id; // either shortopt or longopt_id is non-zero
+ const char *longopt;
+ // no argument: argdesc == NULL && !optional_arg
+ // optional argument: argdesc != NULL && optional_arg
+ // required argument: argdesc != NULL && !optional_arg
+ const char *argdesc;
+ bool optional_arg;
+ const char *text; // if NULL, the option does not appear in the help
+};
+
+#define MAX_EQUIVALENT_SHORTCUTS 3
+struct sc_shortcut {
+ const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1];
+ const char *text;
+};
+
+struct sc_getopt_adapter {
+ char *optstring;
+ struct option *longopts;
+};
+
+static const struct sc_option options[] = {
+ {
+ .longopt_id = OPT_ALWAYS_ON_TOP,
+ .longopt = "always-on-top",
+ .text = "Make scrcpy window always on top (above other windows).",
+ },
+ {
+ .shortopt = 'b',
+ .longopt = "bit-rate",
+ .argdesc = "value",
+ .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 " STR(DEFAULT_BIT_RATE) ".",
+ },
+ {
+ .longopt_id = OPT_CODEC_OPTIONS,
+ .longopt = "codec-options",
+ .argdesc = "key[:type]=value[,...]",
+ .text = "Set a list of comma-separated key:type=value options for the "
+ "device encoder.\n"
+ "The possible values for 'type' are 'int' (default), 'long', "
+ "'float' and 'string'.\n"
+ "The list of possible codec options is available in the "
+ "Android documentation: "
+ "",
+ },
+ {
+ .longopt_id = OPT_CROP,
+ .longopt = "crop",
+ .argdesc = "width:height:x:y",
+ .text = "Crop the device screen on the server.\n"
+ "The values are expressed in the device natural orientation "
+ "(typically, portrait for a phone, landscape for a tablet). "
+ "Any --max-size value is cmoputed on the cropped size.",
+ },
+ {
+ .longopt_id = OPT_DISABLE_SCREENSAVER,
+ .longopt = "disable-screensaver",
+ .text = "Disable screensaver while scrcpy is running.",
+ },
+ {
+ .longopt_id = OPT_DISPLAY_ID,
+ .longopt = "display",
+ .argdesc = "id",
+ .text = "Specify the display id to mirror.\n"
+ "The list of possible display ids can be listed by:\n"
+ " adb shell dumpsys display\n"
+ "(search \"mDisplayId=\" in the output)\n"
+ "Default is 0.",
+ },
+ {
+ .longopt_id = OPT_DISPLAY_BUFFER,
+ .longopt = "display-buffer",
+ .argdesc = "ms",
+ .text = "Add a buffering delay (in milliseconds) before displaying. "
+ "This increases latency to compensate for jitter.\n"
+ "Default is 0 (no buffering).",
+ },
+ {
+ .longopt_id = OPT_ENCODER_NAME,
+ .longopt = "encoder",
+ .argdesc = "name",
+ .text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
+ },
+ {
+ .longopt_id = OPT_FORCE_ADB_FORWARD,
+ .longopt = "force-adb-forward",
+ .text = "Do not attempt to use \"adb reverse\" to connect to the "
+ "device.",
+ },
+ {
+ .longopt_id = OPT_FORWARD_ALL_CLICKS,
+ .longopt = "forward-all-clicks",
+ .text = "By default, right-click triggers BACK (or POWER on) and "
+ "middle-click triggers HOME. This option disables these "
+ "shortcuts and forwards the clicks to the device instead.",
+ },
+ {
+ .shortopt = 'f',
+ .longopt = "fullscreen",
+ .text = "Start in fullscreen.",
+ },
+ {
+ .shortopt = 'K',
+ .longopt = "hid-keyboard",
+ .text = "Simulate a physical keyboard by using HID over AOAv2.\n"
+ "It provides a better experience for IME users, and allows to "
+ "generate non-ASCII characters, contrary to the default "
+ "injection method.\n"
+ "It may only work over USB, and is currently only supported "
+ "on Linux.",
+ },
+ {
+ .shortopt = 'h',
+ .longopt = "help",
+ .text = "Print this help.",
+ },
+ {
+ .longopt_id = OPT_LEGACY_PASTE,
+ .longopt = "legacy-paste",
+ .text = "Inject computer clipboard text as a sequence of key events "
+ "on Ctrl+v (like MOD+Shift+v).\n"
+ "This is a workaround for some devices not behaving as "
+ "expected when setting the device clipboard programmatically.",
+ },
+ {
+ .longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
+ .longopt = "lock-video-orientation",
+ .argdesc = "value",
+ .optional_arg = true,
+ .text = "Lock video orientation to value.\n"
+ "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.\n"
+ "Default is \"unlocked\".\n"
+ "Passing the option without argument is equivalent to passing "
+ "\"initial\".",
+ },
+ {
+ .longopt_id = OPT_MAX_FPS,
+ .longopt = "max-fps",
+ .argdesc = "value",
+ .text = "Limit the frame rate of screen capture (officially supported "
+ "since Android 10, but may work on earlier versions).",
+ },
+ {
+ .shortopt = 'm',
+ .longopt = "max-size",
+ .argdesc = "value",
+ .text = "Limit both the width and height of the video to value. The "
+ "other dimension is computed so that the device aspect-ratio "
+ "is preserved.\n"
+ "Default is 0 (unlimited).",
+ },
+ {
+ .shortopt = 'n',
+ .longopt = "no-control",
+ .text = "Disable device control (mirror the device in read-only).",
+ },
+ {
+ .shortopt = 'N',
+ .longopt = "no-display",
+ .text = "Do not display device (only when screen recording "
+#ifdef HAVE_V4L2
+ "or V4L2 sink "
+#endif
+ "is enabled).",
+ },
+ {
+ .longopt_id = OPT_NO_KEY_REPEAT,
+ .longopt = "no-key-repeat",
+ .text = "Do not forward repeated key events when a key is held down.",
+ },
+ {
+ .longopt_id = OPT_NO_MIPMAPS,
+ .longopt = "no-mipmaps",
+ .text = "If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then "
+ "mipmaps are automatically generated to improve downscaling "
+ "quality. This option disables the generation of mipmaps.",
+ },
+ {
+ .shortopt = 'p',
+ .longopt = "port",
+ .argdesc = "port[:port]",
+ .text = "Set the TCP port (range) used by the client to listen.\n"
+ "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":"
+ STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".",
+ },
+ {
+ .longopt_id = OPT_POWER_OFF_ON_CLOSE,
+ .longopt = "power-off-on-close",
+ .text = "Turn the device screen off when closing scrcpy.",
+ },
+ {
+ .longopt_id = OPT_PREFER_TEXT,
+ .longopt = "prefer-text",
+ .text = "Inject alpha characters and space as text events instead of"
+ "key events.\n"
+ "This avoids issues when combining multiple keys to enter a "
+ "special character, but breaks the expected behavior of alpha "
+ "keys in games (typically WASD).",
+ },
+ {
+ .longopt_id = OPT_PUSH_TARGET,
+ .longopt = "push-target",
+ .argdesc = "path",
+ .text = "Set the target directory for pushing files to the device by "
+ "drag & drop. It is passed as is to \"adb push\".\n"
+ "Default is \"/sdcard/Download/\".",
+ },
+ {
+ .shortopt = 'r',
+ .longopt = "record",
+ .argdesc = "file.mp4",
+ .text = "Record screen to file.\n"
+ "The format is determined by the --record-format option if "
+ "set, or by the file extension (.mp4 or .mkv).",
+ },
+ {
+ .longopt_id = OPT_RECORD_FORMAT,
+ .longopt = "record-format",
+ .argdesc = "format",
+ .text = "Force recording format (either mp4 or mkv).",
+ },
+ {
+ .longopt_id = OPT_RENDER_DRIVER,
+ .longopt = "render-driver",
+ .argdesc = "name",
+ .text = "Request SDL to use the given render driver (this is just a "
+ "hint).\n"
+ "Supported names are currently \"direct3d\", \"opengl\", "
+ "\"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
+ "",
+ },
+ {
+ // deprecated
+ .longopt_id = OPT_RENDER_EXPIRED_FRAMES,
+ .longopt = "render-expired-frames",
+ },
+ {
+ .longopt_id = OPT_ROTATION,
+ .longopt = "rotation",
+ .argdesc = "value",
+ .text = "Set the initial display rotation.\n"
+ "Possible values are 0, 1, 2 and 3. Each increment adds a 90 "
+ "degrees rotation counterclockwise.",
+ },
+ {
+ .shortopt = 's',
+ .longopt = "serial",
+ .argdesc = "serial",
+ .text = "The device serial number. Mandatory only if several devices "
+ "are connected to adb.",
+ },
+ {
+ .longopt_id = OPT_SHORTCUT_MOD,
+ .longopt = "shortcut-mod",
+ .argdesc = "key[+...][,...]",
+ .text = "Specify the modifiers to use for scrcpy shortcuts.\n"
+ "Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", "
+ "\"lsuper\" and \"rsuper\".\n"
+ "A shortcut can consist in several keys, separated by '+'. "
+ "Several shortcuts can be specified, separated by ','.\n"
+ "For example, to use either LCtrl+LAlt or LSuper for scrcpy "
+ "shortcuts, pass \"lctrl+lalt,lsuper\".\n"
+ "Default is \"lalt,lsuper\" (left-Alt or left-Super).",
+ },
+ {
+ .shortopt = 'S',
+ .longopt = "turn-screen-off",
+ .text = "Turn the device screen off immediately.",
+ },
+ {
+ .shortopt = 't',
+ .longopt = "show-touches",
+ .text = "Enable \"show touches\" on start, restore the initial value "
+ "on exit.\n"
+ "It only shows physical touches (not clicks from scrcpy).",
+ },
+#ifdef HAVE_V4L2
+ {
+ .longopt_id = OPT_V4L2_SINK,
+ .longopt = "v4l2-sink",
+ .argdesc = "/dev/videoN",
+ .text = "Output to v4l2loopback device.\n"
+ "It requires to lock the video orientation (see "
+ "--lock-video-orientation).",
+ },
+ {
+ .longopt_id = OPT_V4L2_BUFFER,
+ .longopt = "v4l2-buffer",
+ .argdesc = "ms",
+ .text = "Add a buffering delay (in milliseconds) before pushing "
+ "frames. This increases latency to compensate for jitter.\n"
+ "This option is similar to --display-buffer, but specific to "
+ "V4L2 sink.\n"
+ "Default is 0 (no buffering).",
+ },
+#endif
+ {
+ .shortopt = 'V',
+ .longopt = "verbosity",
+ .argdesc = "value",
+ .text = "Set the log level (verbose, debug, info, warn or error).\n"
+#ifndef NDEBUG
+ "Default is debug.",
+#else
+ "Default is info.",
+#endif
+ },
+ {
+ .shortopt = 'v',
+ .longopt = "version",
+ .text = "Print the version of scrcpy.",
+ },
+ {
+ .shortopt = 'w',
+ .longopt = "stay-awake",
+ .text = "Keep the device on while scrcpy is running, when the device "
+ "is plugged in.",
+ },
+ {
+ .longopt_id = OPT_WINDOW_BORDERLESS,
+ .longopt = "window-borderless",
+ .text = "Disable window decorations (display borderless window)."
+ },
+ {
+ .longopt_id = OPT_WINDOW_TITLE,
+ .longopt = "window-title",
+ .argdesc = "text",
+ .text = "Set a custom window title.",
+ },
+ {
+ .longopt_id = OPT_WINDOW_X,
+ .longopt = "window-x",
+ .argdesc = "value",
+ .text = "Set the initial window horizontal position.\n"
+ "Default is \"auto\".",
+ },
+ {
+ .longopt_id = OPT_WINDOW_Y,
+ .longopt = "window-y",
+ .argdesc = "value",
+ .text = "Set the initial window vertical position.\n"
+ "Default is \"auto\".",
+ },
+ {
+ .longopt_id = OPT_WINDOW_WIDTH,
+ .longopt = "window-width",
+ .argdesc = "value",
+ .text = "Set the initial window width.\n"
+ "Default is 0 (automatic).",
+ },
+ {
+ .longopt_id = OPT_WINDOW_HEIGHT,
+ .longopt = "window-height",
+ .argdesc = "value",
+ .text = "Set the initial window height.\n"
+ "Default is 0 (automatic).",
+ },
+};
+
+static const struct sc_shortcut shortcuts[] = {
+ {
+ .shortcuts = { "MOD+f" },
+ .text = "Switch fullscreen mode",
+ },
+ {
+ .shortcuts = { "MOD+Left" },
+ .text = "Rotate display left",
+ },
+ {
+ .shortcuts = { "MOD+Right" },
+ .text = "Rotate display right",
+ },
+ {
+ .shortcuts = { "MOD+g" },
+ .text = "Resize window to 1:1 (pixel-perfect)",
+ },
+ {
+ .shortcuts = { "MOD+w", "Double-click on black borders" },
+ .text = "Resize window to remove black borders",
+ },
+ {
+ .shortcuts = { "MOD+h", "Middle-click" },
+ .text = "Click on HOME",
+ },
+ {
+ .shortcuts = {
+ "MOD+b",
+ "MOD+Backspace",
+ "Right-click (when screen is on)",
+ },
+ .text = "Click on BACK",
+ },
+ {
+ .shortcuts = { "MOD+s" },
+ .text = "Click on APP_SWITCH",
+ },
+ {
+ .shortcuts = { "MOD+m" },
+ .text = "Click on MENU",
+ },
+ {
+ .shortcuts = { "MOD+Up" },
+ .text = "Click on VOLUME_UP",
+ },
+ {
+ .shortcuts = { "MOD+Down" },
+ .text = "Click on VOLUME_DOWN",
+ },
+ {
+ .shortcuts = { "MOD+p" },
+ .text = "Click on POWER (turn screen on/off)",
+ },
+ {
+ .shortcuts = { "Right-click (when screen is off)" },
+ .text = "Power on",
+ },
+ {
+ .shortcuts = { "MOD+o" },
+ .text = "Turn device screen off (keep mirroring)",
+ },
+ {
+ .shortcuts = { "MOD+Shift+o" },
+ .text = "Turn device screen on",
+ },
+ {
+ .shortcuts = { "MOD+r" },
+ .text = "Rotate device screen",
+ },
+ {
+ .shortcuts = { "MOD+n" },
+ .text = "Expand notification panel",
+ },
+ {
+ .shortcuts = { "MOD+Shift+n" },
+ .text = "Collapse notification panel",
+ },
+ {
+ .shortcuts = { "MOD+c" },
+ .text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)",
+ },
+ {
+ .shortcuts = { "MOD+x" },
+ .text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)",
+ },
+ {
+ .shortcuts = { "MOD+v" },
+ .text = "Copy computer clipboard to device, then paste (inject PASTE "
+ "keycode, Android >= 7 only)",
+ },
+ {
+ .shortcuts = { "MOD+Shift+v" },
+ .text = "Inject computer clipboard text as a sequence of key events",
+ },
+ {
+ .shortcuts = { "MOD+i" },
+ .text = "Enable/disable FPS counter (print frames/second in logs)",
+ },
+ {
+ .shortcuts = { "Ctrl+click-and-move" },
+ .text = "Pinch-to-zoom from the center of the screen",
+ },
+ {
+ .shortcuts = { "Drag & drop APK file" },
+ .text = "Install APK from computer",
+ },
+ {
+ .shortcuts = { "Drag & drop non-APK file" },
+ .text = "Push file to device (see --push-target)",
+ },
+};
+
+static char *
+sc_getopt_adapter_create_optstring(void) {
+ struct sc_strbuf buf;
+ if (!sc_strbuf_init(&buf, 64)) {
+ return false;
+ }
+
+ for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
+ const struct sc_option *opt = &options[i];
+ if (opt->shortopt) {
+ if (!sc_strbuf_append_char(&buf, opt->shortopt)) {
+ goto error;
+ }
+ // If there is an argument, add ':'
+ if (opt->argdesc) {
+ if (!sc_strbuf_append_char(&buf, ':')) {
+ goto error;
+ }
+ // If the argument is optional, add another ':'
+ if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) {
+ goto error;
+ }
+ }
+ }
+ }
+
+ return buf.s;
+
+error:
+ free(buf.s);
+ return NULL;
+}
+
+static struct option *
+sc_getopt_adapter_create_longopts(void) {
+ struct option *longopts =
+ malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts));
+ if (!longopts) {
+ return NULL;
+ }
+
+ size_t out_idx = 0;
+ for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
+ const struct sc_option *in = &options[i];
+ if (!in->longopt) {
+ // The longopts array must only contain long options
+ continue;
+ }
+ struct option *out = &longopts[out_idx++];
+
+ out->name = in->longopt;
+
+ if (!in->argdesc) {
+ assert(!in->optional_arg);
+ out->has_arg = no_argument;
+ } else if (in->optional_arg) {
+ out->has_arg = optional_argument;
+ } else {
+ out->has_arg = required_argument;
+ }
+
+ out->flag = NULL;
+
+ // Either shortopt or longopt_id is set, but not both
+ assert(!!in->shortopt ^ !!in->longopt_id);
+ out->val = in->shortopt ? in->shortopt : in->longopt_id;
+ }
+
+ // The array must be terminated by a NULL item
+ longopts[out_idx] = (struct option) {0};
+
+ return longopts;
+}
+
+static bool
+sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) {
+ adapter->optstring = sc_getopt_adapter_create_optstring();
+ if (!adapter->optstring) {
+ return false;
+ }
+
+ adapter->longopts = sc_getopt_adapter_create_longopts();
+ if (!adapter->longopts) {
+ free(adapter->optstring);
+ return false;
+ }
+
+ return true;
+};
+
+static void
+sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) {
+ free(adapter->optstring);
+ free(adapter->longopts);
+}
+
+static void
+print_option_usage_header(const struct sc_option *opt) {
+ struct sc_strbuf buf;
+ if (!sc_strbuf_init(&buf, 64)) {
+ goto error;
+ }
+
+ bool ok = true;
+ (void) ok; // only used for assertions
+
+ if (opt->shortopt) {
+ ok = sc_strbuf_append_char(&buf, '-');
+ assert(ok);
+
+ ok = sc_strbuf_append_char(&buf, opt->shortopt);
+ assert(ok);
+
+ if (opt->longopt) {
+ ok = sc_strbuf_append_staticstr(&buf, ", ");
+ assert(ok);
+ }
+ }
+
+ if (opt->longopt) {
+ ok = sc_strbuf_append_staticstr(&buf, "--");
+ assert(ok);
+
+ if (!sc_strbuf_append_str(&buf, opt->longopt)) {
+ goto error;
+ }
+ }
+
+ if (opt->argdesc) {
+ if (opt->optional_arg && !sc_strbuf_append_char(&buf, '[')) {
+ goto error;
+ }
+
+ if (!sc_strbuf_append_char(&buf, '=')) {
+ goto error;
+ }
+
+ if (!sc_strbuf_append_str(&buf, opt->argdesc)) {
+ goto error;
+ }
+
+ if (opt->optional_arg && !sc_strbuf_append_char(&buf, ']')) {
+ goto error;
+ }
+ }
+
+ fprintf(stderr, "\n %s\n", buf.s);
+ free(buf.s);
+ return;
+
+error:
+ fprintf(stderr, "\n");
+}
+
+static void
+print_option_usage(const struct sc_option *opt, unsigned cols) {
+ assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
+
+ if (!opt->text) {
+ // Option not documented in help (for example because it is deprecated)
+ return;
+ }
+
+ print_option_usage_header(opt);
+
+ char *text = sc_str_wrap_lines(opt->text, cols, 8);
+ if (!text) {
+ fprintf(stderr, "\n");
+ return;
+ }
+
+ fprintf(stderr, "%s\n", text);
+ free(text);
+}
+
+static void
+print_shortcuts_intro(unsigned cols) {
+ char *intro = sc_str_wrap_lines(
+ "In the following list, MOD is the shortcut modifier. By default, it's "
+ "(left) Alt or (left) Super, but it can be configured by "
+ "--shortcut-mod (see above).", cols, 4);
+ if (!intro) {
+ fprintf(stderr, "\n");
+ return;
+ }
+
+ fprintf(stderr, "%s\n", intro);
+ free(intro);
+}
+
+static void
+print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) {
+ assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
+ assert(shortcut->shortcuts[0]); // At least one shortcut
+ assert(shortcut->text);
+
+ fprintf(stderr, "\n");
+
+ unsigned i = 0;
+ while (shortcut->shortcuts[i]) {
+ fprintf(stderr, " %s\n", shortcut->shortcuts[i]);
+ ++i;
+ };
+
+ char *text = sc_str_wrap_lines(shortcut->text, cols, 8);
+ if (!text) {
+ fprintf(stderr, "\n");
+ return;
+ }
+
+ fprintf(stderr, "%s\n", text);
+ free(text);
+}
+
void
scrcpy_print_usage(const char *arg0) {
- fprintf(stderr,
- "Usage: %s [options]\n"
- "\n"
- "Options:\n"
- "\n"
- " --always-on-top\n"
- " Make scrcpy window always on top (above other windows).\n"
- "\n"
- " -b, --bit-rate value\n"
- " Encode the video at the given bit-rate, expressed in bits/s.\n"
- " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
- " Default is " STR(DEFAULT_BIT_RATE) ".\n"
- "\n"
- " --codec-options key[:type]=value[,...]\n"
- " Set a list of comma-separated key:type=value options for the\n"
- " device encoder.\n"
- " The possible values for 'type' are 'int' (default), 'long',\n"
- " 'float' and 'string'.\n"
- " The list of possible codec options is available in the\n"
- " Android documentation:\n"
- " \n"
- "\n"
- " --crop width:height:x:y\n"
- " Crop the device screen on the server.\n"
- " The values are expressed in the device natural orientation\n"
- " (typically, portrait for a phone, landscape for a tablet).\n"
- " Any --max-size value is computed on the cropped size.\n"
- "\n"
- " --disable-screensaver\n"
- " Disable screensaver while scrcpy is running.\n"
- "\n"
- " --display id\n"
- " Specify the display id to mirror.\n"
- "\n"
- " The list of possible display ids can be listed by:\n"
- " adb shell dumpsys display\n"
- " (search \"mDisplayId=\" in the output)\n"
- "\n"
- " Default is 0.\n"
- "\n"
- " --encoder name\n"
- " Use a specific MediaCodec encoder (must be a H.264 encoder).\n"
- "\n"
- " --force-adb-forward\n"
- " Do not attempt to use \"adb reverse\" to connect to the\n"
- " the device.\n"
- "\n"
- " --forward-all-clicks\n"
- " By default, right-click triggers BACK (or POWER on) and\n"
- " middle-click triggers HOME. This option disables these\n"
- " shortcuts and forward the clicks to the device instead.\n"
- "\n"
- " -f, --fullscreen\n"
- " Start in fullscreen.\n"
- "\n"
- " -h, --help\n"
- " Print this help.\n"
- "\n"
- " --legacy-paste\n"
- " Inject computer clipboard text as a sequence of key events\n"
- " on Ctrl+v (like MOD+Shift+v).\n"
- " This is a workaround for some devices not behaving as\n"
- " expected when setting the device clipboard programmatically.\n"
- "\n"
- " --lock-video-orientation [value]\n"
- " Lock video orientation to value.\n"
- " Possible values are \"unlocked\", \"initial\" (locked to the\n"
- " initial orientation), 0, 1, 2 and 3.\n"
- " Natural device orientation is 0, and each increment adds a\n"
- " 90 degrees rotation counterclockwise.\n"
- " Default is \"unlocked\".\n"
- " Passing the option without argument is equivalent to passing\n"
- " \"initial\".\n"
- "\n"
- " --max-fps value\n"
- " Limit the frame rate of screen capture (officially supported\n"
- " since Android 10, but may work on earlier versions).\n"
- "\n"
- " -m, --max-size value\n"
- " Limit both the width and height of the video to value. The\n"
- " other dimension is computed so that the device aspect-ratio\n"
- " is preserved.\n"
- " Default is 0 (unlimited).\n"
- "\n"
- " -n, --no-control\n"
- " Disable device control (mirror the device in read-only).\n"
- "\n"
- " -N, --no-display\n"
- " Do not display device (only when screen recording is\n"
- " enabled).\n"
- "\n"
- " --no-key-repeat\n"
- " Do not forward repeated key events when a key is held down.\n"
- "\n"
- " --no-mipmaps\n"
- " If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n"
- " mipmaps are automatically generated to improve downscaling\n"
- " quality. This option disables the generation of mipmaps.\n"
- "\n"
- " -p, --port port[:port]\n"
- " Set the TCP port (range) used by the client to listen.\n"
- " Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":"
- STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".\n"
- "\n"
- " --prefer-text\n"
- " Inject alpha characters and space as text events instead of\n"
- " key events.\n"
- " This avoids issues when combining multiple keys to enter a\n"
- " special character, but breaks the expected behavior of alpha\n"
- " keys in games (typically WASD).\n"
- "\n"
- " --push-target path\n"
- " Set the target directory for pushing files to the device by\n"
- " drag & drop. It is passed as-is to \"adb push\".\n"
- " Default is \"/sdcard/Download/\".\n"
- "\n"
- " -r, --record file.mp4\n"
- " Record screen to file.\n"
- " The format is determined by the --record-format option if\n"
- " set, or by the file extension (.mp4 or .mkv).\n"
- "\n"
- " --record-format format\n"
- " Force recording format (either mp4 or mkv).\n"
- "\n"
- " --render-driver name\n"
- " Request SDL to use the given render driver (this is just a\n"
- " hint).\n"
- " Supported names are currently \"direct3d\", \"opengl\",\n"
- " \"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
- " \n"
- "\n"
- " --rotation value\n"
- " Set the initial display rotation.\n"
- " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n"
- " degrees rotation counterclockwise.\n"
- "\n"
- " -s, --serial serial\n"
- " The device serial number. Mandatory only if several devices\n"
- " are connected to adb.\n"
- "\n"
- " --shortcut-mod key[+...]][,...]\n"
- " Specify the modifiers to use for scrcpy shortcuts.\n"
- " Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n"
- " \"lsuper\" and \"rsuper\".\n"
- "\n"
- " A shortcut can consist in several keys, separated by '+'.\n"
- " Several shortcuts can be specified, separated by ','.\n"
- "\n"
- " For example, to use either LCtrl+LAlt or LSuper for scrcpy\n"
- " shortcuts, pass \"lctrl+lalt,lsuper\".\n"
- "\n"
- " Default is \"lalt,lsuper\" (left-Alt or left-Super).\n"
- "\n"
- " -S, --turn-screen-off\n"
- " Turn the device screen off immediately.\n"
- "\n"
- " -t, --show-touches\n"
- " Enable \"show touches\" on start, restore the initial value\n"
- " on exit.\n"
- " It only shows physical touches (not clicks from scrcpy).\n"
- "\n"
-#ifdef HAVE_V4L2
- " --v4l2-sink /dev/videoN\n"
- " Output to v4l2loopback device.\n"
- " It requires to lock the video orientation (see\n"
- " --lock-video-orientation).\n"
- "\n"
-#endif
- " -V, --verbosity value\n"
- " Set the log level (verbose, debug, info, warn or error).\n"
-#ifndef NDEBUG
- " Default is debug.\n"
-#else
- " Default is info.\n"
-#endif
- "\n"
- " -v, --version\n"
- " Print the version of scrcpy.\n"
- "\n"
- " -w, --stay-awake\n"
- " Keep the device on while scrcpy is running, when the device\n"
- " is plugged in.\n"
- "\n"
- " --window-borderless\n"
- " Disable window decorations (display borderless window).\n"
- "\n"
- " --window-title text\n"
- " Set a custom window title.\n"
- "\n"
- " --window-x value\n"
- " Set the initial window horizontal position.\n"
- " Default is \"auto\".\n"
- "\n"
- " --window-y value\n"
- " Set the initial window vertical position.\n"
- " Default is \"auto\".\n"
- "\n"
- " --window-width value\n"
- " Set the initial window width.\n"
- " Default is 0 (automatic).\n"
- "\n"
- " --window-height value\n"
- " Set the initial window height.\n"
- " Default is 0 (automatic).\n"
- "\n"
- "Shortcuts:\n"
- "\n"
- " In the following list, MOD is the shortcut modifier. By default,\n"
- " it's (left) Alt or (left) Super, but it can be configured by\n"
- " --shortcut-mod (see above).\n"
- "\n"
- " MOD+f\n"
- " Switch fullscreen mode\n"
- "\n"
- " MOD+Left\n"
- " Rotate display left\n"
- "\n"
- " MOD+Right\n"
- " Rotate display right\n"
- "\n"
- " MOD+g\n"
- " Resize window to 1:1 (pixel-perfect)\n"
- "\n"
- " MOD+w\n"
- " Double-click on black borders\n"
- " Resize window to remove black borders\n"
- "\n"
- " MOD+h\n"
- " Middle-click\n"
- " Click on HOME\n"
- "\n"
- " MOD+b\n"
- " MOD+Backspace\n"
- " Right-click (when screen is on)\n"
- " Click on BACK\n"
- "\n"
- " MOD+s\n"
- " Click on APP_SWITCH\n"
- "\n"
- " MOD+m\n"
- " Click on MENU\n"
- "\n"
- " MOD+Up\n"
- " Click on VOLUME_UP\n"
- "\n"
- " MOD+Down\n"
- " Click on VOLUME_DOWN\n"
- "\n"
- " MOD+p\n"
- " Click on POWER (turn screen on/off)\n"
- "\n"
- " Right-click (when screen is off)\n"
- " Power on\n"
- "\n"
- " MOD+o\n"
- " Turn device screen off (keep mirroring)\n"
- "\n"
- " MOD+Shift+o\n"
- " Turn device screen on\n"
- "\n"
- " MOD+r\n"
- " Rotate device screen\n"
- "\n"
- " MOD+n\n"
- " Expand notification panel\n"
- "\n"
- " MOD+Shift+n\n"
- " Collapse notification panel\n"
- "\n"
- " MOD+c\n"
- " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n"
- "\n"
- " MOD+x\n"
- " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n"
- "\n"
- " MOD+v\n"
- " Copy computer clipboard to device, then paste (inject PASTE\n"
- " keycode, Android >= 7 only)\n"
- "\n"
- " MOD+Shift+v\n"
- " Inject computer clipboard text as a sequence of key events\n"
- "\n"
- " MOD+i\n"
- " Enable/disable FPS counter (print frames/second in logs)\n"
- "\n"
- " Ctrl+click-and-move\n"
- " Pinch-to-zoom from the center of the screen\n"
- "\n"
- " Drag & drop APK file\n"
- " Install APK from computer\n"
- "\n", arg0);
+#define SC_TERM_COLS_DEFAULT 80
+ unsigned cols;
+
+ if (!isatty(STDERR_FILENO)) {
+ // Not a tty
+ cols = SC_TERM_COLS_DEFAULT;
+ } else {
+ bool ok = sc_term_get_size(NULL, &cols);
+ if (!ok) {
+ // Could not get the terminal size
+ cols = SC_TERM_COLS_DEFAULT;
+ }
+ if (cols < 20) {
+ // Do not accept a too small value
+ cols = 20;
+ }
+ }
+
+ fprintf(stderr, "Usage: %s [options]\n\n"
+ "Options:\n", arg0);
+ for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
+ print_option_usage(&options[i], cols);
+ }
+
+ // Print shortcuts section
+ fprintf(stderr, "\nShortcuts:\n\n");
+ print_shortcuts_intro(cols);
+ for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) {
+ print_shortcut(&shortcuts[i], cols);
+ }
}
static bool
@@ -314,9 +779,9 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
long value;
bool ok;
if (accept_suffix) {
- ok = parse_integer_with_suffix(s, &value);
+ ok = sc_str_parse_integer_with_suffix(s, &value);
} else {
- ok = parse_integer(s, &value);
+ ok = sc_str_parse_integer(s, &value);
}
if (!ok) {
LOGE("Could not parse %s: %s", name, s);
@@ -336,7 +801,7 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
static size_t
parse_integers_arg(const char *s, size_t max_items, long *out, long min,
long max, const char *name) {
- size_t count = parse_integers(s, ':', max_items, out);
+ size_t count = sc_str_parse_integers(s, ':', max_items, out);
if (!count) {
LOGE("Could not parse %s: %s", name, s);
return 0;
@@ -392,6 +857,19 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
return true;
}
+static bool
+parse_buffering_time(const char *s, sc_tick *tick) {
+ long value;
+ bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF,
+ "buffering time");
+ if (!ok) {
+ return false;
+ }
+
+ *tick = SC_TICK_FROM_MS(value);
+ return true;
+}
+
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
@@ -661,108 +1139,21 @@ guess_record_format(const char *filename) {
return 0;
}
-#define OPT_RENDER_EXPIRED_FRAMES 1000
-#define OPT_WINDOW_TITLE 1001
-#define OPT_PUSH_TARGET 1002
-#define OPT_ALWAYS_ON_TOP 1003
-#define OPT_CROP 1004
-#define OPT_RECORD_FORMAT 1005
-#define OPT_PREFER_TEXT 1006
-#define OPT_WINDOW_X 1007
-#define OPT_WINDOW_Y 1008
-#define OPT_WINDOW_WIDTH 1009
-#define OPT_WINDOW_HEIGHT 1010
-#define OPT_WINDOW_BORDERLESS 1011
-#define OPT_MAX_FPS 1012
-#define OPT_LOCK_VIDEO_ORIENTATION 1013
-#define OPT_DISPLAY_ID 1014
-#define OPT_ROTATION 1015
-#define OPT_RENDER_DRIVER 1016
-#define OPT_NO_MIPMAPS 1017
-#define OPT_CODEC_OPTIONS 1018
-#define OPT_FORCE_ADB_FORWARD 1019
-#define OPT_DISABLE_SCREENSAVER 1020
-#define OPT_SHORTCUT_MOD 1021
-#define OPT_NO_KEY_REPEAT 1022
-#define OPT_FORWARD_ALL_CLICKS 1023
-#define OPT_LEGACY_PASTE 1024
-#define OPT_ENCODER_NAME 1025
-#define OPT_POWER_OFF_ON_CLOSE 1026
-#define OPT_V4L2_SINK 1027
-
-bool
-scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
- static const struct option long_options[] = {
- {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP},
- {"bit-rate", required_argument, NULL, 'b'},
- {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS},
- {"crop", required_argument, NULL, OPT_CROP},
- {"disable-screensaver", no_argument, NULL,
- OPT_DISABLE_SCREENSAVER},
- {"display", required_argument, NULL, OPT_DISPLAY_ID},
- {"encoder", required_argument, NULL, OPT_ENCODER_NAME},
- {"force-adb-forward", no_argument, NULL,
- OPT_FORCE_ADB_FORWARD},
- {"forward-all-clicks", no_argument, NULL,
- OPT_FORWARD_ALL_CLICKS},
- {"fullscreen", no_argument, NULL, 'f'},
- {"help", no_argument, NULL, 'h'},
- {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
- {"lock-video-orientation", optional_argument, NULL,
- OPT_LOCK_VIDEO_ORIENTATION},
- {"max-fps", required_argument, NULL, OPT_MAX_FPS},
- {"max-size", required_argument, NULL, 'm'},
- {"no-control", no_argument, NULL, 'n'},
- {"no-display", no_argument, NULL, 'N'},
- {"no-key-repeat", no_argument, NULL, OPT_NO_KEY_REPEAT},
- {"no-mipmaps", no_argument, NULL, OPT_NO_MIPMAPS},
- {"port", required_argument, NULL, 'p'},
- {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT},
- {"push-target", required_argument, NULL, OPT_PUSH_TARGET},
- {"record", required_argument, NULL, 'r'},
- {"record-format", required_argument, NULL, OPT_RECORD_FORMAT},
- {"render-driver", required_argument, NULL, OPT_RENDER_DRIVER},
- {"render-expired-frames", no_argument, NULL,
- OPT_RENDER_EXPIRED_FRAMES},
- {"rotation", required_argument, NULL, OPT_ROTATION},
- {"serial", required_argument, NULL, 's'},
- {"shortcut-mod", required_argument, NULL, OPT_SHORTCUT_MOD},
- {"show-touches", no_argument, NULL, 't'},
- {"stay-awake", no_argument, NULL, 'w'},
- {"turn-screen-off", no_argument, NULL, 'S'},
-#ifdef HAVE_V4L2
- {"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK},
-#endif
- {"verbosity", required_argument, NULL, 'V'},
- {"version", no_argument, NULL, 'v'},
- {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
- {"window-x", required_argument, NULL, OPT_WINDOW_X},
- {"window-y", required_argument, NULL, OPT_WINDOW_Y},
- {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH},
- {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
- {"window-borderless", no_argument, NULL,
- OPT_WINDOW_BORDERLESS},
- {"power-off-on-close", no_argument, NULL,
- OPT_POWER_OFF_ON_CLOSE},
- {NULL, 0, NULL, 0 },
- };
-
+static bool
+parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
+ const char *optstring, const struct option *longopts) {
struct scrcpy_options *opts = &args->opts;
optind = 0; // reset to start from the first argument in tests
int c;
- while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w",
- long_options, NULL)) != -1) {
+ while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
return false;
}
break;
- case 'c':
- LOGW("Deprecated option -c. Use --crop instead.");
- // fall through
case OPT_CROP:
opts->crop = optarg;
break;
@@ -785,6 +1176,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case 'h':
args->help = true;
break;
+ case 'K':
+ opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID;
+ break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
@@ -824,9 +1218,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case 't':
opts->show_touches = true;
break;
- case 'T':
- LOGW("Deprecated option -T. Use --always-on-top instead.");
- // fall through
case OPT_ALWAYS_ON_TOP:
opts->always_on_top = true;
break;
@@ -917,10 +1308,20 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_POWER_OFF_ON_CLOSE:
opts->power_off_on_close = true;
break;
+ case OPT_DISPLAY_BUFFER:
+ if (!parse_buffering_time(optarg, &opts->display_buffer)) {
+ return false;
+ }
+ break;
#ifdef HAVE_V4L2
case OPT_V4L2_SINK:
opts->v4l2_device = optarg;
break;
+ case OPT_V4L2_BUFFER:
+ if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) {
+ return false;
+ }
+ break;
#endif
default:
// getopt prints the error message on stderr
@@ -941,6 +1342,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
+
+ if (opts->v4l2_buffer && !opts->v4l2_device) {
+ LOGE("V4L2 buffer value without V4L2 sink\n");
+ return false;
+ }
#else
if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
@@ -981,3 +1387,19 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
return true;
}
+
+bool
+scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
+ struct sc_getopt_adapter adapter;
+ if (!sc_getopt_adapter_init(&adapter)) {
+ LOGW("Could not create getopt adapter");
+ return false;
+ }
+
+ bool ret = parse_args_with_getopt(args, argc, argv, adapter.optstring,
+ adapter.longopts);
+
+ sc_getopt_adapter_destroy(&adapter);
+
+ return ret;
+}
diff --git a/app/src/cli.h b/app/src/cli.h
index 419f156f..b9361a9c 100644
--- a/app/src/cli.h
+++ b/app/src/cli.h
@@ -5,7 +5,7 @@
#include
-#include "scrcpy.h"
+#include "options.h"
struct scrcpy_cli_args {
struct scrcpy_options opts;
diff --git a/app/src/clock.c b/app/src/clock.c
new file mode 100644
index 00000000..fe072f01
--- /dev/null
+++ b/app/src/clock.c
@@ -0,0 +1,111 @@
+#include "clock.h"
+
+#include "util/log.h"
+
+#define SC_CLOCK_NDEBUG // comment to debug
+
+void
+sc_clock_init(struct sc_clock *clock) {
+ clock->count = 0;
+ clock->head = 0;
+ clock->left_sum.system = 0;
+ clock->left_sum.stream = 0;
+ clock->right_sum.system = 0;
+ clock->right_sum.stream = 0;
+}
+
+// Estimate the affine function f(stream) = slope * stream + offset
+static void
+sc_clock_estimate(struct sc_clock *clock,
+ double *out_slope, sc_tick *out_offset) {
+ assert(clock->count > 1); // two points are necessary
+
+ struct sc_clock_point left_avg = {
+ .system = clock->left_sum.system / (clock->count / 2),
+ .stream = clock->left_sum.stream / (clock->count / 2),
+ };
+ struct sc_clock_point right_avg = {
+ .system = clock->right_sum.system / ((clock->count + 1) / 2),
+ .stream = clock->right_sum.stream / ((clock->count + 1) / 2),
+ };
+
+ double slope = (double) (right_avg.system - left_avg.system)
+ / (right_avg.stream - left_avg.stream);
+
+ if (clock->count < SC_CLOCK_RANGE) {
+ /* The first frames are typically received and decoded with more delay
+ * than the others, causing a wrong slope estimation on start. To
+ * compensate, assume an initial slope of 1, then progressively use the
+ * estimated slope. */
+ slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
+ / SC_CLOCK_RANGE;
+ }
+
+ struct sc_clock_point global_avg = {
+ .system = (clock->left_sum.system + clock->right_sum.system)
+ / clock->count,
+ .stream = (clock->left_sum.stream + clock->right_sum.stream)
+ / clock->count,
+ };
+
+ sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);
+
+ *out_slope = slope;
+ *out_offset = offset;
+}
+
+void
+sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
+ struct sc_clock_point *point = &clock->points[clock->head];
+
+ if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
+ // One point passes from the right sum to the left sum
+
+ unsigned mid;
+ if (clock->count == SC_CLOCK_RANGE) {
+ mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
+ } else {
+ // Only for the first frames
+ mid = clock->count / 2;
+ }
+
+ struct sc_clock_point *mid_point = &clock->points[mid];
+ clock->left_sum.system += mid_point->system;
+ clock->left_sum.stream += mid_point->stream;
+ clock->right_sum.system -= mid_point->system;
+ clock->right_sum.stream -= mid_point->stream;
+ }
+
+ if (clock->count == SC_CLOCK_RANGE) {
+ // The current point overwrites the previous value in the circular
+ // array, update the left sum accordingly
+ clock->left_sum.system -= point->system;
+ clock->left_sum.stream -= point->stream;
+ } else {
+ ++clock->count;
+ }
+
+ point->system = system;
+ point->stream = stream;
+
+ clock->right_sum.system += system;
+ clock->right_sum.stream += stream;
+
+ clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
+
+ if (clock->count > 1) {
+ // Update estimation
+ sc_clock_estimate(clock, &clock->slope, &clock->offset);
+
+#ifndef SC_CLOCK_NDEBUG
+ LOGD("Clock estimation: %g * pts + %" PRItick,
+ clock->slope, clock->offset);
+#endif
+ }
+}
+
+sc_tick
+sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
+ assert(clock->count > 1); // sc_clock_update() must have been called
+ return (sc_tick) (stream * clock->slope) + clock->offset;
+}
diff --git a/app/src/clock.h b/app/src/clock.h
new file mode 100644
index 00000000..886d1f4d
--- /dev/null
+++ b/app/src/clock.h
@@ -0,0 +1,70 @@
+#ifndef SC_CLOCK_H
+#define SC_CLOCK_H
+
+#include "common.h"
+
+#include
+
+#include "util/tick.h"
+
+#define SC_CLOCK_RANGE 32
+static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
+
+struct sc_clock_point {
+ sc_tick system;
+ sc_tick stream;
+};
+
+/**
+ * The clock aims to estimate the affine relation between the stream (device)
+ * time and the system time:
+ *
+ * f(stream) = slope * stream + offset
+ *
+ * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
+ * of a frame expressed both in stream time and system time) in a circular
+ * array.
+ *
+ * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
+ * sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
+ * point"). The slope of the estimated affine function is that of the line
+ * passing through these two points.
+ *
+ * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE
+ * points. The resulting affine function passes by this centroid.
+ *
+ * With a circular array, the rolling sums (and average) are quick to compute.
+ * In practice, the estimation is stable and the evolution is smooth.
+ */
+struct sc_clock {
+ // Circular array
+ struct sc_clock_point points[SC_CLOCK_RANGE];
+
+ // Number of points in the array (count <= SC_CLOCK_RANGE)
+ unsigned count;
+
+ // Index of the next point to write
+ unsigned head;
+
+ // Sum of the first count/2 points
+ struct sc_clock_point left_sum;
+
+ // Sum of the last (count+1)/2 points
+ struct sc_clock_point right_sum;
+
+ // Estimated slope and offset
+ // (computed on sc_clock_update(), used by sc_clock_to_system_time())
+ double slope;
+ sc_tick offset;
+};
+
+void
+sc_clock_init(struct sc_clock *clock);
+
+void
+sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream);
+
+sc_tick
+sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream);
+
+#endif
diff --git a/app/src/compat.h b/app/src/compat.h
index 8e2d18f4..9f66ce95 100644
--- a/app/src/compat.h
+++ b/app/src/compat.h
@@ -43,6 +43,11 @@
# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
#endif
+#if SDL_VERSION_ATLEAST(2, 0, 6)
+//
+# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
+#endif
+
#if SDL_VERSION_ATLEAST(2, 0, 8)
//
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
diff --git a/app/src/control_msg.c b/app/src/control_msg.c
index 1257010e..74e3315c 100644
--- a/app/src/control_msg.c
+++ b/app/src/control_msg.c
@@ -7,7 +7,7 @@
#include "util/buffer_util.h"
#include "util/log.h"
-#include "util/str_util.h"
+#include "util/str.h"
/**
* Map an enum value to a string based on an array, without crashing on an
@@ -56,7 +56,7 @@ static const char *const screen_power_mode_labels[] = {
};
static void
-write_position(uint8_t *buf, const struct position *position) {
+write_position(uint8_t *buf, const struct sc_position *position) {
buffer_write32be(&buf[0], position->point.x);
buffer_write32be(&buf[4], position->point.y);
buffer_write16be(&buf[8], position->screen_size.width);
@@ -66,7 +66,7 @@ write_position(uint8_t *buf, const struct position *position) {
// write length (2 bytes) + string (non nul-terminated)
static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) {
- size_t len = utf8_truncation_index(utf8, max_len);
+ size_t len = sc_str_utf8_truncation_index(utf8, max_len);
buffer_write32be(buf, len);
memcpy(&buf[4], utf8, len);
return 4 + len;
diff --git a/app/src/control_msg.h b/app/src/control_msg.h
index 920a493a..16492849 100644
--- a/app/src/control_msg.h
+++ b/app/src/control_msg.h
@@ -57,11 +57,11 @@ struct control_msg {
enum android_motionevent_action action;
enum android_motionevent_buttons buttons;
uint64_t pointer_id;
- struct position position;
+ struct sc_position position;
float pressure;
} inject_touch_event;
struct {
- struct position position;
+ struct sc_position position;
int32_t hscroll;
int32_t vscroll;
} inject_scroll_event;
diff --git a/app/src/controller.c b/app/src/controller.c
index 3a428aa8..e486ea72 100644
--- a/app/src/controller.c
+++ b/app/src/controller.c
@@ -5,7 +5,7 @@
#include "util/log.h"
bool
-controller_init(struct controller *controller, socket_t control_socket) {
+controller_init(struct controller *controller, sc_socket control_socket) {
cbuf_init(&controller->queue);
bool ok = receiver_init(&controller->receiver, control_socket);
@@ -47,7 +47,7 @@ controller_destroy(struct controller *controller) {
bool
controller_push_msg(struct controller *controller,
- const struct control_msg *msg) {
+ const struct control_msg *msg) {
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
control_msg_log(msg);
}
@@ -63,14 +63,14 @@ controller_push_msg(struct controller *controller,
}
static bool
-process_msg(struct controller *controller,
- const struct control_msg *msg) {
+process_msg(struct controller *controller, const struct control_msg *msg) {
static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE];
size_t length = control_msg_serialize(msg, serialized_msg);
if (!length) {
return false;
}
- int w = net_send_all(controller->control_socket, serialized_msg, length);
+ ssize_t w =
+ net_send_all(controller->control_socket, serialized_msg, length);
return (size_t) w == length;
}
diff --git a/app/src/controller.h b/app/src/controller.h
index c53d0a61..e7004131 100644
--- a/app/src/controller.h
+++ b/app/src/controller.h
@@ -14,7 +14,7 @@
struct control_msg_queue CBUF(struct control_msg, 64);
struct controller {
- socket_t control_socket;
+ sc_socket control_socket;
sc_thread thread;
sc_mutex mutex;
sc_cond msg_cond;
@@ -24,7 +24,7 @@ struct controller {
};
bool
-controller_init(struct controller *controller, socket_t control_socket);
+controller_init(struct controller *controller, sc_socket control_socket);
void
controller_destroy(struct controller *controller);
diff --git a/app/src/coords.h b/app/src/coords.h
index 7be6836d..cdabb782 100644
--- a/app/src/coords.h
+++ b/app/src/coords.h
@@ -3,22 +3,22 @@
#include
-struct size {
+struct sc_size {
uint16_t width;
uint16_t height;
};
-struct point {
+struct sc_point {
int32_t x;
int32_t y;
};
-struct position {
+struct sc_position {
// The video screen size may be different from the real device screen size,
// so store to which size the absolute position apply, to scale it
// accordingly.
- struct size screen_size;
- struct point point;
+ struct sc_size screen_size;
+ struct sc_point point;
};
#endif
diff --git a/app/src/decoder.c b/app/src/decoder.c
index aa5018b3..7c67e836 100644
--- a/app/src/decoder.c
+++ b/app/src/decoder.c
@@ -1,5 +1,6 @@
#include "decoder.h"
+#include
#include
#include "events.h"
diff --git a/app/src/event_converter.h b/app/src/event_converter.h
deleted file mode 100644
index d28e9fdc..00000000
--- a/app/src/event_converter.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef CONVERT_H
-#define CONVERT_H
-
-#include "common.h"
-
-#include
-#include
-
-#include "control_msg.h"
-
-bool
-convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to);
-
-enum android_metastate
-convert_meta_state(SDL_Keymod mod);
-
-bool
-convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
- bool prefer_text);
-
-enum android_motionevent_buttons
-convert_mouse_buttons(uint32_t state);
-
-bool
-convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to);
-
-bool
-convert_touch_action(SDL_EventType from, enum android_motionevent_action *to);
-
-#endif
diff --git a/app/src/events.h b/app/src/events.h
index a4d6f3df..abe1a72c 100644
--- a/app/src/events.h
+++ b/app/src/events.h
@@ -1,2 +1,4 @@
-#define EVENT_NEW_FRAME SDL_USEREVENT
-#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
+#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)
diff --git a/app/src/file_handler.c b/app/src/file_handler.c
index 27fe6fa3..fe0ab857 100644
--- a/app/src/file_handler.c
+++ b/app/src/file_handler.c
@@ -46,7 +46,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
file_handler->initialized = false;
file_handler->stopped = false;
- file_handler->current_process = PROCESS_NONE;
+ file_handler->current_process = SC_PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
@@ -65,12 +65,12 @@ file_handler_destroy(struct file_handler *file_handler) {
}
}
-static process_t
+static sc_pid
install_apk(const char *serial, const char *file) {
return adb_install(serial, file);
}
-static process_t
+static sc_pid
push_file(const char *serial, const char *file, const char *push_target) {
return adb_push(serial, file, push_target);
}
@@ -109,7 +109,7 @@ run_file_handler(void *data) {
for (;;) {
sc_mutex_lock(&file_handler->mutex);
- file_handler->current_process = PROCESS_NONE;
+ file_handler->current_process = SC_PROCESS_NONE;
while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) {
sc_cond_wait(&file_handler->event_cond, &file_handler->mutex);
}
@@ -123,26 +123,26 @@ run_file_handler(void *data) {
assert(non_empty);
(void) non_empty;
- process_t process;
+ sc_pid pid;
if (req.action == ACTION_INSTALL_APK) {
LOGI("Installing %s...", req.file);
- process = install_apk(file_handler->serial, req.file);
+ pid = install_apk(file_handler->serial, req.file);
} else {
LOGI("Pushing %s...", req.file);
- process = push_file(file_handler->serial, req.file,
- file_handler->push_target);
+ pid = push_file(file_handler->serial, req.file,
+ file_handler->push_target);
}
- file_handler->current_process = process;
+ file_handler->current_process = pid;
sc_mutex_unlock(&file_handler->mutex);
if (req.action == ACTION_INSTALL_APK) {
- if (process_check_success(process, "adb install", false)) {
+ if (sc_process_check_success(pid, "adb install", false)) {
LOGI("%s successfully installed", req.file);
} else {
LOGE("Failed to install %s", req.file);
}
} else {
- if (process_check_success(process, "adb push", false)) {
+ if (sc_process_check_success(pid, "adb push", false)) {
LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target);
} else {
@@ -152,11 +152,11 @@ run_file_handler(void *data) {
}
sc_mutex_lock(&file_handler->mutex);
- // Close the process (it is necessary already terminated)
+ // Close the process (it is necessarily already terminated)
// Execute this call with mutex locked to avoid race conditions with
// file_handler_stop()
- process_close(file_handler->current_process);
- file_handler->current_process = PROCESS_NONE;
+ sc_process_close(file_handler->current_process);
+ file_handler->current_process = SC_PROCESS_NONE;
sc_mutex_unlock(&file_handler->mutex);
file_handler_request_destroy(&req);
@@ -183,8 +183,8 @@ file_handler_stop(struct file_handler *file_handler) {
sc_mutex_lock(&file_handler->mutex);
file_handler->stopped = true;
sc_cond_signal(&file_handler->event_cond);
- if (file_handler->current_process != PROCESS_NONE) {
- if (!process_terminate(file_handler->current_process)) {
+ if (file_handler->current_process != SC_PROCESS_NONE) {
+ if (!sc_process_terminate(file_handler->current_process)) {
LOGW("Could not terminate push/install process");
}
}
diff --git a/app/src/file_handler.h b/app/src/file_handler.h
index fe1d1804..e2067533 100644
--- a/app/src/file_handler.h
+++ b/app/src/file_handler.h
@@ -29,7 +29,7 @@ struct file_handler {
sc_cond event_cond;
bool stopped;
bool initialized;
- process_t current_process;
+ sc_pid current_process;
struct file_handler_request_queue queue;
};
diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c
index bbf71887..c92d4140 100644
--- a/app/src/fps_counter.c
+++ b/app/src/fps_counter.c
@@ -1,11 +1,10 @@
#include "fps_counter.h"
#include
-#include
#include "util/log.h"
-#define FPS_COUNTER_INTERVAL_MS 1000
+#define FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1)
bool
fps_counter_init(struct fps_counter *counter) {
@@ -47,7 +46,7 @@ set_started(struct fps_counter *counter, bool started) {
static void
display_fps(struct fps_counter *counter) {
unsigned rendered_per_second =
- counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS;
+ counter->nr_rendered * SC_TICK_FREQ / FPS_COUNTER_INTERVAL;
if (counter->nr_skipped) {
LOGI("%u fps (+%u frames skipped)", rendered_per_second,
counter->nr_skipped);
@@ -68,8 +67,8 @@ check_interval_expired(struct fps_counter *counter, uint32_t now) {
counter->nr_skipped = 0;
// add a multiple of the interval
uint32_t elapsed_slices =
- (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1;
- counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices;
+ (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL + 1;
+ counter->next_timestamp += FPS_COUNTER_INTERVAL * elapsed_slices;
}
static int
@@ -82,14 +81,12 @@ run_fps_counter(void *data) {
sc_cond_wait(&counter->state_cond, &counter->mutex);
}
while (!counter->interrupted && is_started(counter)) {
- uint32_t now = SDL_GetTicks();
+ sc_tick now = sc_tick_now();
check_interval_expired(counter, now);
- assert(counter->next_timestamp > now);
- uint32_t remaining = counter->next_timestamp - now;
-
// ignore the reason (timeout or signaled), we just loop anyway
- sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining);
+ sc_cond_timedwait(&counter->state_cond, &counter->mutex,
+ counter->next_timestamp);
}
}
sc_mutex_unlock(&counter->mutex);
@@ -99,7 +96,7 @@ run_fps_counter(void *data) {
bool
fps_counter_start(struct fps_counter *counter) {
sc_mutex_lock(&counter->mutex);
- counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS;
+ counter->next_timestamp = sc_tick_now() + FPS_COUNTER_INTERVAL;
counter->nr_rendered = 0;
counter->nr_skipped = 0;
sc_mutex_unlock(&counter->mutex);
@@ -165,7 +162,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) {
}
sc_mutex_lock(&counter->mutex);
- uint32_t now = SDL_GetTicks();
+ sc_tick now = sc_tick_now();
check_interval_expired(counter, now);
++counter->nr_rendered;
sc_mutex_unlock(&counter->mutex);
@@ -178,7 +175,7 @@ fps_counter_add_skipped_frame(struct fps_counter *counter) {
}
sc_mutex_lock(&counter->mutex);
- uint32_t now = SDL_GetTicks();
+ sc_tick now = sc_tick_now();
check_interval_expired(counter, now);
++counter->nr_skipped;
sc_mutex_unlock(&counter->mutex);
diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h
index de252586..9609c814 100644
--- a/app/src/fps_counter.h
+++ b/app/src/fps_counter.h
@@ -24,7 +24,7 @@ struct fps_counter {
bool interrupted;
unsigned nr_rendered;
unsigned nr_skipped;
- uint32_t next_timestamp;
+ sc_tick next_timestamp;
};
bool
diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c
new file mode 100644
index 00000000..33ca6227
--- /dev/null
+++ b/app/src/frame_buffer.c
@@ -0,0 +1,88 @@
+#include "frame_buffer.h"
+
+#include
+#include
+#include
+
+#include "util/log.h"
+
+bool
+sc_frame_buffer_init(struct sc_frame_buffer *fb) {
+ fb->pending_frame = av_frame_alloc();
+ if (!fb->pending_frame) {
+ return false;
+ }
+
+ fb->tmp_frame = av_frame_alloc();
+ if (!fb->tmp_frame) {
+ av_frame_free(&fb->pending_frame);
+ return false;
+ }
+
+ bool ok = sc_mutex_init(&fb->mutex);
+ if (!ok) {
+ av_frame_free(&fb->pending_frame);
+ av_frame_free(&fb->tmp_frame);
+ return false;
+ }
+
+ // there is initially no frame, so consider it has already been consumed
+ fb->pending_frame_consumed = true;
+
+ return true;
+}
+
+void
+sc_frame_buffer_destroy(struct sc_frame_buffer *fb) {
+ sc_mutex_destroy(&fb->mutex);
+ av_frame_free(&fb->pending_frame);
+ av_frame_free(&fb->tmp_frame);
+}
+
+static inline void
+swap_frames(AVFrame **lhs, AVFrame **rhs) {
+ AVFrame *tmp = *lhs;
+ *lhs = *rhs;
+ *rhs = tmp;
+}
+
+bool
+sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame,
+ bool *previous_frame_skipped) {
+ sc_mutex_lock(&fb->mutex);
+
+ // Use a temporary frame to preserve pending_frame in case of error.
+ // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand.
+ int r = av_frame_ref(fb->tmp_frame, frame);
+ if (r) {
+ LOGE("Could not ref frame: %d", r);
+ return false;
+ }
+
+ // Now that av_frame_ref() succeeded, we can replace the previous
+ // pending_frame
+ swap_frames(&fb->pending_frame, &fb->tmp_frame);
+ av_frame_unref(fb->tmp_frame);
+
+ if (previous_frame_skipped) {
+ *previous_frame_skipped = !fb->pending_frame_consumed;
+ }
+ fb->pending_frame_consumed = false;
+
+ sc_mutex_unlock(&fb->mutex);
+
+ return true;
+}
+
+void
+sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst) {
+ sc_mutex_lock(&fb->mutex);
+ assert(!fb->pending_frame_consumed);
+ fb->pending_frame_consumed = true;
+
+ av_frame_move_ref(dst, fb->pending_frame);
+ // av_frame_move_ref() resets its source frame, so no need to call
+ // av_frame_unref()
+
+ sc_mutex_unlock(&fb->mutex);
+}
diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h
new file mode 100644
index 00000000..f97261cd
--- /dev/null
+++ b/app/src/frame_buffer.h
@@ -0,0 +1,44 @@
+#ifndef SC_FRAME_BUFFER_H
+#define SC_FRAME_BUFFER_H
+
+#include "common.h"
+
+#include
+
+#include "util/thread.h"
+
+// forward declarations
+typedef struct AVFrame AVFrame;
+
+/**
+ * A frame buffer holds 1 pending frame, which is the last frame received from
+ * the producer (typically, the decoder).
+ *
+ * If a pending frame has not been consumed when the producer pushes a new
+ * frame, then it is lost. The intent is to always provide access to the very
+ * last frame to minimize latency.
+ */
+
+struct sc_frame_buffer {
+ AVFrame *pending_frame;
+ AVFrame *tmp_frame; // To preserve the pending frame on error
+
+ sc_mutex mutex;
+
+ bool pending_frame_consumed;
+};
+
+bool
+sc_frame_buffer_init(struct sc_frame_buffer *fb);
+
+void
+sc_frame_buffer_destroy(struct sc_frame_buffer *fb);
+
+bool
+sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame,
+ bool *skipped);
+
+void
+sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst);
+
+#endif
diff --git a/app/src/hid_keyboard.c b/app/src/hid_keyboard.c
new file mode 100644
index 00000000..3ac1a441
--- /dev/null
+++ b/app/src/hid_keyboard.c
@@ -0,0 +1,363 @@
+#include "hid_keyboard.h"
+
+#include
+#include
+
+#include "util/log.h"
+
+/** Downcast key processor to hid_keyboard */
+#define DOWNCAST(KP) container_of(KP, struct sc_hid_keyboard, key_processor)
+
+#define HID_KEYBOARD_ACCESSORY_ID 1
+
+#define HID_MODIFIER_NONE 0x00
+#define HID_MODIFIER_LEFT_CONTROL (1 << 0)
+#define HID_MODIFIER_LEFT_SHIFT (1 << 1)
+#define HID_MODIFIER_LEFT_ALT (1 << 2)
+#define HID_MODIFIER_LEFT_GUI (1 << 3)
+#define HID_MODIFIER_RIGHT_CONTROL (1 << 4)
+#define HID_MODIFIER_RIGHT_SHIFT (1 << 5)
+#define HID_MODIFIER_RIGHT_ALT (1 << 6)
+#define HID_MODIFIER_RIGHT_GUI (1 << 7)
+
+#define HID_KEYBOARD_INDEX_MODIFIER 0
+#define HID_KEYBOARD_INDEX_KEYS 2
+
+// USB HID protocol says 6 keys in an event is the requirement for BIOS
+// keyboard support, though OS could support more keys via modifying the report
+// desc. 6 should be enough for scrcpy.
+#define HID_KEYBOARD_MAX_KEYS 6
+#define HID_KEYBOARD_EVENT_SIZE (2 + HID_KEYBOARD_MAX_KEYS)
+
+#define HID_RESERVED 0x00
+#define HID_ERROR_ROLL_OVER 0x01
+
+/**
+ * For HID over AOAv2, only report descriptor is needed.
+ *
+ * The specification is available here:
+ *
+ *
+ * In particular, read:
+ * - 6.2.2 Report Descriptor
+ * - Appendix B.1 Protocol 1 (Keyboard)
+ * - Appendix C: Keyboard Implementation
+ *
+ * Normally a basic HID keyboard uses 8 bytes:
+ * Modifier Reserved Key Key Key Key Key Key
+ *
+ * You can dump your device's report descriptor with:
+ *
+ * sudo usbhid-dump -m vid:pid -e descriptor
+ *
+ * (change vid:pid' to your device's vendor ID and product ID).
+ */
+static const unsigned char keyboard_report_desc[] = {
+ // Usage Page (Generic Desktop)
+ 0x05, 0x01,
+ // Usage (Keyboard)
+ 0x09, 0x06,
+
+ // Collection (Application)
+ 0xA1, 0x01,
+
+ // Usage Page (Key Codes)
+ 0x05, 0x07,
+ // Usage Minimum (224)
+ 0x19, 0xE0,
+ // Usage Maximum (231)
+ 0x29, 0xE7,
+ // Logical Minimum (0)
+ 0x15, 0x00,
+ // Logical Maximum (1)
+ 0x25, 0x01,
+ // Report Size (1)
+ 0x75, 0x01,
+ // Report Count (8)
+ 0x95, 0x08,
+ // Input (Data, Variable, Absolute): Modifier byte
+ 0x81, 0x02,
+
+ // Report Size (8)
+ 0x75, 0x08,
+ // Report Count (1)
+ 0x95, 0x01,
+ // Input (Constant): Reserved byte
+ 0x81, 0x01,
+
+ // Usage Page (LEDs)
+ 0x05, 0x08,
+ // Usage Minimum (1)
+ 0x19, 0x01,
+ // Usage Maximum (5)
+ 0x29, 0x05,
+ // Report Size (1)
+ 0x75, 0x01,
+ // Report Count (5)
+ 0x95, 0x05,
+ // Output (Data, Variable, Absolute): LED report
+ 0x91, 0x02,
+
+ // Report Size (3)
+ 0x75, 0x03,
+ // Report Count (1)
+ 0x95, 0x01,
+ // Output (Constant): LED report padding
+ 0x91, 0x01,
+
+ // Usage Page (Key Codes)
+ 0x05, 0x07,
+ // Usage Minimum (0)
+ 0x19, 0x00,
+ // Usage Maximum (101)
+ 0x29, SC_HID_KEYBOARD_KEYS - 1,
+ // Logical Minimum (0)
+ 0x15, 0x00,
+ // Logical Maximum(101)
+ 0x25, SC_HID_KEYBOARD_KEYS - 1,
+ // Report Size (8)
+ 0x75, 0x08,
+ // Report Count (6)
+ 0x95, HID_KEYBOARD_MAX_KEYS,
+ // Input (Data, Array): Keys
+ 0x81, 0x00,
+
+ // End Collection
+ 0xC0
+};
+
+static unsigned char
+sdl_keymod_to_hid_modifiers(SDL_Keymod mod) {
+ unsigned char modifiers = HID_MODIFIER_NONE;
+ if (mod & KMOD_LCTRL) {
+ modifiers |= HID_MODIFIER_LEFT_CONTROL;
+ }
+ if (mod & KMOD_LSHIFT) {
+ modifiers |= HID_MODIFIER_LEFT_SHIFT;
+ }
+ if (mod & KMOD_LALT) {
+ modifiers |= HID_MODIFIER_LEFT_ALT;
+ }
+ if (mod & KMOD_LGUI) {
+ modifiers |= HID_MODIFIER_LEFT_GUI;
+ }
+ if (mod & KMOD_RCTRL) {
+ modifiers |= HID_MODIFIER_RIGHT_CONTROL;
+ }
+ if (mod & KMOD_RSHIFT) {
+ modifiers |= HID_MODIFIER_RIGHT_SHIFT;
+ }
+ if (mod & KMOD_RALT) {
+ modifiers |= HID_MODIFIER_RIGHT_ALT;
+ }
+ if (mod & KMOD_RGUI) {
+ modifiers |= HID_MODIFIER_RIGHT_GUI;
+ }
+ return modifiers;
+}
+
+static bool
+sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) {
+ unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE);
+ if (!buffer) {
+ return false;
+ }
+
+ buffer[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE;
+ buffer[1] = HID_RESERVED;
+ memset(&buffer[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS);
+
+ sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, buffer,
+ HID_KEYBOARD_EVENT_SIZE);
+ return true;
+}
+
+static inline bool
+scancode_is_modifier(SDL_Scancode scancode) {
+ return scancode >= SDL_SCANCODE_LCTRL && scancode <= SDL_SCANCODE_RGUI;
+}
+
+static bool
+convert_hid_keyboard_event(struct sc_hid_keyboard *kb,
+ struct sc_hid_event *hid_event,
+ const SDL_KeyboardEvent *event) {
+ SDL_Scancode scancode = event->keysym.scancode;
+ assert(scancode >= 0);
+
+ // SDL also generates events when only modifiers are pressed, we cannot
+ // ignore them totally, for example press 'a' first then press 'Control',
+ // if we ignore 'Control' event, only 'a' is sent.
+ if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) {
+ // Scancode to ignore
+ return false;
+ }
+
+ if (!sc_hid_keyboard_event_init(hid_event)) {
+ LOGW("Could not initialize HID keyboard event");
+ return false;
+ }
+
+ unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->keysym.mod);
+
+ if (scancode < SC_HID_KEYBOARD_KEYS) {
+ // Pressed is true and released is false
+ kb->keys[scancode] = (event->type == SDL_KEYDOWN);
+ LOGV("keys[%02x] = %s", scancode,
+ kb->keys[scancode] ? "true" : "false");
+ }
+
+ hid_event->buffer[HID_KEYBOARD_INDEX_MODIFIER] = modifiers;
+
+ unsigned char *keys_buffer = &hid_event->buffer[HID_KEYBOARD_INDEX_KEYS];
+ // Re-calculate pressed keys every time
+ int keys_pressed_count = 0;
+ for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) {
+ if (kb->keys[i]) {
+ // USB HID protocol says that if keys exceeds report count, a
+ // phantom state should be reported
+ if (keys_pressed_count >= HID_KEYBOARD_MAX_KEYS) {
+ // Pantom state:
+ // - Modifiers
+ // - Reserved
+ // - ErrorRollOver * HID_MAX_KEYS
+ memset(keys_buffer, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS);
+ goto end;
+ }
+
+ keys_buffer[keys_pressed_count] = i;
+ ++keys_pressed_count;
+ }
+ }
+
+end:
+ LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x",
+ event->type == SDL_KEYDOWN ? "down" : "up", event->keysym.scancode,
+ event->keysym.scancode, modifiers);
+
+ return true;
+}
+
+
+static bool
+push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) {
+ bool capslock = sdl_mod & KMOD_CAPS;
+ bool numlock = sdl_mod & KMOD_NUM;
+ if (!capslock && !numlock) {
+ // Nothing to do
+ return true;
+ }
+
+ struct sc_hid_event hid_event;
+ if (!sc_hid_keyboard_event_init(&hid_event)) {
+ LOGW("Could not initialize HID keyboard event");
+ return false;
+ }
+
+#define SC_SCANCODE_CAPSLOCK SDL_SCANCODE_CAPSLOCK
+#define SC_SCANCODE_NUMLOCK SDL_SCANCODE_NUMLOCKCLEAR
+ unsigned i = 0;
+ if (capslock) {
+ hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK;
+ ++i;
+ }
+ if (numlock) {
+ hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK;
+ ++i;
+ }
+
+ if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) {
+ sc_hid_event_destroy(&hid_event);
+ LOGW("Could request HID event");
+ return false;
+ }
+
+ LOGD("HID keyboard state synchronized");
+
+ return true;
+}
+
+static void
+sc_key_processor_process_key(struct sc_key_processor *kp,
+ const SDL_KeyboardEvent *event) {
+ if (event->repeat) {
+ // In USB HID protocol, key repeat is handled by the host (Android), so
+ // just ignore key repeat here.
+ return;
+ }
+
+ struct sc_hid_keyboard *kb = DOWNCAST(kp);
+
+ struct sc_hid_event hid_event;
+ // Not all keys are supported, just ignore unsupported keys
+ if (convert_hid_keyboard_event(kb, &hid_event, event)) {
+ if (!kb->mod_lock_synchronized) {
+ // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize
+ // keyboard state
+ if (push_mod_lock_state(kb, event->keysym.mod)) {
+ kb->mod_lock_synchronized = true;
+ }
+ }
+
+ SDL_Keycode keycode = event->keysym.sym;
+ bool down = event->type == SDL_KEYDOWN;
+ bool ctrl = event->keysym.mod & KMOD_CTRL;
+ bool shift = event->keysym.mod & KMOD_SHIFT;
+ if (ctrl && !shift && keycode == SDLK_v && down) {
+ // Ctrl+v is pressed, so clipboard synchronization has been
+ // requested. Wait a bit so that the clipboard is set before
+ // injecting Ctrl+v via HID, otherwise it would paste the old
+ // clipboard content.
+ hid_event.delay = SC_TICK_FROM_MS(5);
+ }
+
+ if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) {
+ sc_hid_event_destroy(&hid_event);
+ LOGW("Could request HID event");
+ }
+ }
+}
+
+static void
+sc_key_processor_process_text(struct sc_key_processor *kp,
+ const SDL_TextInputEvent *event) {
+ (void) kp;
+ (void) event;
+
+ // Never forward text input via HID (all the keys are injected separately)
+}
+
+bool
+sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) {
+ kb->aoa = aoa;
+
+ bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID,
+ keyboard_report_desc,
+ ARRAY_LEN(keyboard_report_desc));
+ if (!ok) {
+ LOGW("Register HID keyboard failed");
+ return false;
+ }
+
+ // Reset all states
+ memset(kb->keys, false, SC_HID_KEYBOARD_KEYS);
+
+ kb->mod_lock_synchronized = false;
+
+ static const struct sc_key_processor_ops ops = {
+ .process_key = sc_key_processor_process_key,
+ .process_text = sc_key_processor_process_text,
+ };
+
+ kb->key_processor.ops = &ops;
+
+ return true;
+}
+
+void
+sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb) {
+ // Unregister HID keyboard so the soft keyboard shows again on Android
+ bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID);
+ if (!ok) {
+ LOGW("Could not unregister HID keyboard");
+ }
+}
diff --git a/app/src/hid_keyboard.h b/app/src/hid_keyboard.h
new file mode 100644
index 00000000..7173a898
--- /dev/null
+++ b/app/src/hid_keyboard.h
@@ -0,0 +1,44 @@
+#ifndef SC_HID_KEYBOARD_H
+#define SC_HID_KEYBOARD_H
+
+#include "common.h"
+
+#include
+
+#include "aoa_hid.h"
+#include "trait/key_processor.h"
+
+// See "SDL2/SDL_scancode.h".
+// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
+// HID protocol.
+// 0x65 is Application, typically AT-101 Keyboard ends here.
+#define SC_HID_KEYBOARD_KEYS 0x66
+
+/**
+ * HID keyboard events are sequence-based, every time keyboard state changes
+ * it sends an array of currently pressed keys, the host is responsible for
+ * compare events and determine which key becomes pressed and which key becomes
+ * released. In order to convert SDL_KeyboardEvent to HID events, we first use
+ * an array of keys to save each keys' state. And when a SDL_KeyboardEvent was
+ * emitted, we updated our state, and then we use a loop to generate HID
+ * events. The sequence of array elements is unimportant and when too much keys
+ * pressed at the same time (more than report count), we should generate
+ * phantom state. Don't forget that modifiers should be updated too, even for
+ * phantom state.
+ */
+struct sc_hid_keyboard {
+ struct sc_key_processor key_processor; // key processor trait
+
+ struct sc_aoa *aoa;
+ bool keys[SC_HID_KEYBOARD_KEYS];
+
+ bool mod_lock_synchronized;
+};
+
+bool
+sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa);
+
+void
+sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb);
+
+#endif
diff --git a/app/src/icon.c b/app/src/icon.c
new file mode 100644
index 00000000..2616007e
--- /dev/null
+++ b/app/src/icon.c
@@ -0,0 +1,290 @@
+#include "icon.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "config.h"
+#include "compat.h"
+#include "util/file.h"
+#include "util/log.h"
+#include "util/str.h"
+
+#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png"
+#define SCRCPY_DEFAULT_ICON_PATH \
+ PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png"
+
+static char *
+get_icon_path(void) {
+#ifdef __WINDOWS__
+ const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH");
+#else
+ const char *icon_path_env = getenv("SCRCPY_ICON_PATH");
+#endif
+ if (icon_path_env) {
+ // if the envvar is set, use it
+#ifdef __WINDOWS__
+ char *icon_path = sc_str_from_wchars(icon_path_env);
+#else
+ char *icon_path = strdup(icon_path_env);
+#endif
+ if (!icon_path) {
+ LOGE("Could not allocate memory");
+ return NULL;
+ }
+ LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
+ return icon_path;
+ }
+
+#ifndef PORTABLE
+ LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
+ char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
+ if (!icon_path) {
+ LOGE("Could not allocate memory");
+ return NULL;
+ }
+#else
+ char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
+ if (!icon_path) {
+ LOGE("Could not get icon path");
+ return NULL;
+ }
+ LOGD("Using icon (portable): %s", icon_path);
+#endif
+
+ return icon_path;
+}
+
+static AVFrame *
+decode_image(const char *path) {
+ AVFrame *result = NULL;
+
+ AVFormatContext *ctx = avformat_alloc_context();
+ if (!ctx) {
+ LOGE("Could not allocate image decoder context");
+ return NULL;
+ }
+
+ if (avformat_open_input(&ctx, path, NULL, NULL) < 0) {
+ LOGE("Could not open image codec: %s", path);
+ goto free_ctx;
+ }
+
+ if (avformat_find_stream_info(ctx, NULL) < 0) {
+ LOGE("Could not find image stream info");
+ goto close_input;
+ }
+
+ int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
+ if (stream < 0 ) {
+ LOGE("Could not find best image stream");
+ goto close_input;
+ }
+
+ AVCodecParameters *params = ctx->streams[stream]->codecpar;
+
+ AVCodec *codec = avcodec_find_decoder(params->codec_id);
+ if (!codec) {
+ LOGE("Could not find image decoder");
+ goto close_input;
+ }
+
+ AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
+ if (!codec_ctx) {
+ LOGE("Could not allocate codec context");
+ goto close_input;
+ }
+
+ if (avcodec_parameters_to_context(codec_ctx, params) < 0) {
+ LOGE("Could not fill codec context");
+ goto free_codec_ctx;
+ }
+
+ if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
+ LOGE("Could not open image codec");
+ goto free_codec_ctx;
+ }
+
+ AVFrame *frame = av_frame_alloc();
+ if (!frame) {
+ LOGE("Could not allocate frame");
+ goto close_codec;
+ }
+
+ AVPacket *packet = av_packet_alloc();
+ if (!packet) {
+ LOGE("Could not allocate packet");
+ av_frame_free(&frame);
+ goto close_codec;
+ }
+
+ if (av_read_frame(ctx, packet) < 0) {
+ LOGE("Could not read frame");
+ av_packet_free(&packet);
+ av_frame_free(&frame);
+ goto close_codec;
+ }
+
+ int ret;
+ if ((ret = avcodec_send_packet(codec_ctx, packet)) < 0) {
+ LOGE("Could not send icon packet: %d", ret);
+ av_packet_free(&packet);
+ av_frame_free(&frame);
+ goto close_codec;
+ }
+
+ if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) {
+ LOGE("Could not receive icon frame: %d", ret);
+ av_packet_free(&packet);
+ av_frame_free(&frame);
+ goto close_codec;
+ }
+
+ av_packet_free(&packet);
+
+ result = frame;
+
+close_codec:
+ avcodec_close(codec_ctx);
+free_codec_ctx:
+ avcodec_free_context(&codec_ctx);
+close_input:
+ avformat_close_input(&ctx);
+free_ctx:
+ avformat_free_context(ctx);
+
+ return result;
+}
+
+#if !SDL_VERSION_ATLEAST(2, 0, 10)
+// SDL_PixelFormatEnum has been introduced in SDL 2.0.10. Use int for older SDL
+// versions.
+typedef int SDL_PixelFormatEnum;
+#endif
+
+static SDL_PixelFormatEnum
+to_sdl_pixel_format(enum AVPixelFormat fmt) {
+ switch (fmt) {
+ case AV_PIX_FMT_RGB24: return SDL_PIXELFORMAT_RGB24;
+ case AV_PIX_FMT_BGR24: return SDL_PIXELFORMAT_BGR24;
+ case AV_PIX_FMT_ARGB: return SDL_PIXELFORMAT_ARGB32;
+ case AV_PIX_FMT_RGBA: return SDL_PIXELFORMAT_RGBA32;
+ case AV_PIX_FMT_ABGR: return SDL_PIXELFORMAT_ABGR32;
+ case AV_PIX_FMT_BGRA: return SDL_PIXELFORMAT_BGRA32;
+ case AV_PIX_FMT_RGB565BE: return SDL_PIXELFORMAT_RGB565;
+ case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_RGB555;
+ case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565;
+ case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555;
+ case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444;
+#if SDL_VERSION_ATLEAST(2, 0, 12)
+ case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444;
+#endif
+ case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8;
+ default: return SDL_PIXELFORMAT_UNKNOWN;
+ }
+}
+
+static SDL_Surface *
+load_from_path(const char *path) {
+ AVFrame *frame = decode_image(path);
+ if (!frame) {
+ return NULL;
+ }
+
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format);
+ if (!desc) {
+ LOGE("Could not get icon format descriptor");
+ goto error;
+ }
+
+ bool is_packed = !(desc->flags & AV_PIX_FMT_FLAG_PLANAR);
+ if (!is_packed) {
+ LOGE("Could not load non-packed icon");
+ goto error;
+ }
+
+ SDL_PixelFormatEnum format = to_sdl_pixel_format(frame->format);
+ if (format == SDL_PIXELFORMAT_UNKNOWN) {
+ LOGE("Unsupported icon pixel format: %s (%d)", desc->name,
+ frame->format);
+ goto error;
+ }
+
+ int bits_per_pixel = av_get_bits_per_pixel(desc);
+ SDL_Surface *surface =
+ SDL_CreateRGBSurfaceWithFormatFrom(frame->data[0],
+ frame->width, frame->height,
+ bits_per_pixel,
+ frame->linesize[0],
+ format);
+
+ if (!surface) {
+ LOGE("Could not create icon surface");
+ goto error;
+ }
+
+ if (frame->format == AV_PIX_FMT_PAL8) {
+ // Initialize the SDL palette
+ uint8_t *data = frame->data[1];
+ SDL_Color colors[256];
+ for (int i = 0; i < 256; ++i) {
+ SDL_Color *color = &colors[i];
+
+ // The palette is transported in AVFrame.data[1], is 1024 bytes
+ // long (256 4-byte entries) and is formatted the same as in
+ // AV_PIX_FMT_RGB32 described above (i.e., it is also
+ // endian-specific).
+ //
+#if SDL_BYTEORDER == SDL_BIG_ENDIAN
+ color->a = data[i * 4];
+ color->r = data[i * 4 + 1];
+ color->g = data[i * 4 + 2];
+ color->b = data[i * 4 + 3];
+#else
+ color->a = data[i * 4 + 3];
+ color->r = data[i * 4 + 2];
+ color->g = data[i * 4 + 1];
+ color->b = data[i * 4];
+#endif
+ }
+
+ SDL_Palette *palette = surface->format->palette;
+ assert(palette);
+ int ret = SDL_SetPaletteColors(palette, colors, 0, 256);
+ if (ret) {
+ LOGE("Could not set palette colors");
+ SDL_FreeSurface(surface);
+ goto error;
+ }
+ }
+
+ surface->userdata = frame; // frame owns the data
+
+ return surface;
+
+error:
+ av_frame_free(&frame);
+ return NULL;
+}
+
+SDL_Surface *
+scrcpy_icon_load() {
+ char *icon_path = get_icon_path();
+ if (!icon_path) {
+ return NULL;
+ }
+
+ SDL_Surface *icon = load_from_path(icon_path);
+ free(icon_path);
+ return icon;
+}
+
+void
+scrcpy_icon_destroy(SDL_Surface *icon) {
+ AVFrame *frame = icon->userdata;
+ assert(frame);
+ av_frame_free(&frame);
+ SDL_FreeSurface(icon);
+}
diff --git a/app/src/icon.h b/app/src/icon.h
new file mode 100644
index 00000000..8df53671
--- /dev/null
+++ b/app/src/icon.h
@@ -0,0 +1,16 @@
+#ifndef ICON_H
+#define ICON_H
+
+#include "common.h"
+
+#include
+#include
+#include
+
+SDL_Surface *
+scrcpy_icon_load(void);
+
+void
+scrcpy_icon_destroy(SDL_Surface *icon);
+
+#endif
diff --git a/app/src/icon.xpm b/app/src/icon.xpm
deleted file mode 100644
index 73b29da9..00000000
--- a/app/src/icon.xpm
+++ /dev/null
@@ -1,53 +0,0 @@
-/* XPM */
-static char * icon_xpm[] = {
-"48 48 2 1",
-" c None",
-". c #96C13E",
-" .. .. ",
-" ... ... ",
-" ... ...... ... ",
-" ................ ",
-" .............. ",
-" ................ ",
-" .................. ",
-" .................... ",
-" ..... ........ ..... ",
-" ..... ........ ..... ",
-" ...................... ",
-" ........................ ",
-" ........................ ",
-" ........................ ",
-" ",
-" ",
-" .... ........................ .... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" ...... ........................ ...... ",
-" .... ........................ .... ",
-" ........................ ",
-" ...................... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" ...... ...... ",
-" .... .... "};
diff --git a/app/src/input_manager.c b/app/src/input_manager.c
index a5d0ad07..b84f3bea 100644
--- a/app/src/input_manager.c
+++ b/app/src/input_manager.c
@@ -3,7 +3,6 @@
#include
#include
-#include "event_converter.h"
#include "util/log.h"
static const int ACTION_DOWN = 1;
@@ -53,15 +52,18 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) {
void
input_manager_init(struct input_manager *im, struct controller *controller,
- struct screen *screen,
+ struct screen *screen, struct sc_key_processor *kp,
+ struct sc_mouse_processor *mp,
const struct scrcpy_options *options) {
+ assert(!options->control || (kp && kp->ops));
+ assert(!options->control || (mp && mp->ops));
+
im->controller = controller;
im->screen = screen;
- im->repeat = 0;
+ im->kp = kp;
+ im->mp = mp;
im->control = options->control;
- im->forward_key_repeat = options->forward_key_repeat;
- im->prefer_text = options->prefer_text;
im->forward_all_clicks = options->forward_all_clicks;
im->legacy_paste = options->legacy_paste;
@@ -323,32 +325,14 @@ input_manager_process_text_input(struct input_manager *im,
// A shortcut must never generate text events
return;
}
- if (!im->prefer_text) {
- char c = event->text[0];
- if (isalpha(c) || c == ' ') {
- assert(event->text[1] == '\0');
- // letters and space are handled as raw key event
- return;
- }
- }
- struct control_msg msg;
- msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
- msg.inject_text.text = strdup(event->text);
- if (!msg.inject_text.text) {
- LOGW("Could not strdup input text");
- return;
- }
- if (!controller_push_msg(im->controller, &msg)) {
- free(msg.inject_text.text);
- LOGW("Could not request 'inject text'");
- }
+ im->kp->ops->process_text(im->kp, event);
}
static bool
simulate_virtual_finger(struct input_manager *im,
enum android_motionevent_action action,
- struct point point) {
+ struct sc_point point) {
bool up = action == AMOTION_EVENT_ACTION_UP;
struct control_msg msg;
@@ -368,34 +352,13 @@ simulate_virtual_finger(struct input_manager *im,
return true;
}
-static struct point
-inverse_point(struct point point, struct size size) {
+static struct sc_point
+inverse_point(struct sc_point point, struct sc_size size) {
point.x = size.width - point.x;
point.y = size.height - point.y;
return point;
}
-static bool
-convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
- bool prefer_text, uint32_t repeat) {
- to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
-
- if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
- return false;
- }
-
- uint16_t mod = from->keysym.mod;
- if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
- prefer_text)) {
- return false;
- }
-
- to->inject_keycode.repeat = repeat;
- to->inject_keycode.metastate = convert_meta_state(mod);
-
- return true;
-}
-
static void
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event) {
@@ -549,15 +512,6 @@ input_manager_process_key(struct input_manager *im,
return;
}
- if (event->repeat) {
- if (!im->forward_key_repeat) {
- return;
- }
- ++im->repeat;
- } else {
- im->repeat = 0;
- }
-
if (ctrl && !shift && keycode == SDLK_v && down && !repeat) {
if (im->legacy_paste) {
// inject the text as input events
@@ -569,27 +523,7 @@ input_manager_process_key(struct input_manager *im,
set_device_clipboard(controller, false);
}
- struct control_msg msg;
- if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) {
- if (!controller_push_msg(controller, &msg)) {
- LOGW("Could not request 'inject keycode'");
- }
- }
-}
-
-static bool
-convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
- struct control_msg *to) {
- to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
- to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
- to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
- to->inject_touch_event.position.screen_size = screen->frame_size;
- to->inject_touch_event.position.point =
- screen_convert_window_to_frame_coords(screen, from->x, from->y);
- to->inject_touch_event.pressure = 1.f;
- to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
-
- return true;
+ im->kp->ops->process_key(im->kp, event);
}
static void
@@ -607,79 +541,22 @@ input_manager_process_mouse_motion(struct input_manager *im,
// simulated from touch events, so it's a duplicate
return;
}
- struct control_msg msg;
- if (!convert_mouse_motion(event, im->screen, &msg)) {
- return;
- }
- if (!controller_push_msg(im->controller, &msg)) {
- LOGW("Could not request 'inject mouse motion event'");
- }
+ im->mp->ops->process_mouse_motion(im->mp, event);
if (im->vfinger_down) {
- struct point mouse = msg.inject_touch_event.position.point;
- struct point vfinger = inverse_point(mouse, im->screen->frame_size);
+ struct sc_point mouse =
+ screen_convert_window_to_frame_coords(im->screen, event->x,
+ event->y);
+ struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
}
}
-static bool
-convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
- struct control_msg *to) {
- to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
-
- if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
- return false;
- }
-
- to->inject_touch_event.pointer_id = from->fingerId;
- to->inject_touch_event.position.screen_size = screen->frame_size;
-
- int dw;
- int dh;
- SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
-
- // SDL touch event coordinates are normalized in the range [0; 1]
- int32_t x = from->x * dw;
- int32_t y = from->y * dh;
- to->inject_touch_event.position.point =
- screen_convert_drawable_to_frame_coords(screen, x, y);
-
- to->inject_touch_event.pressure = from->pressure;
- to->inject_touch_event.buttons = 0;
- return true;
-}
-
static void
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event) {
- struct control_msg msg;
- if (convert_touch(event, im->screen, &msg)) {
- if (!controller_push_msg(im->controller, &msg)) {
- LOGW("Could not request 'inject touch event'");
- }
- }
-}
-
-static bool
-convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
- struct control_msg *to) {
- to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
-
- if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
- return false;
- }
-
- to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
- to->inject_touch_event.position.screen_size = screen->frame_size;
- to->inject_touch_event.position.point =
- screen_convert_window_to_frame_coords(screen, from->x, from->y);
- to->inject_touch_event.pressure =
- from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f;
- to->inject_touch_event.buttons =
- convert_mouse_buttons(SDL_BUTTON(from->button));
-
- return true;
+ im->mp->ops->process_touch(im->mp, event);
}
static void
@@ -739,15 +616,7 @@ input_manager_process_mouse_button(struct input_manager *im,
return;
}
- struct control_msg msg;
- if (!convert_mouse_button(event, im->screen, &msg)) {
- return;
- }
-
- if (!controller_push_msg(im->controller, &msg)) {
- LOGW("Could not request 'inject mouse button event'");
- return;
- }
+ im->mp->ops->process_mouse_button(im->mp, event);
// Pinch-to-zoom simulation.
//
@@ -761,8 +630,10 @@ input_manager_process_mouse_button(struct input_manager *im,
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL))
if ((down && !im->vfinger_down && CTRL_PRESSED)
|| (!down && im->vfinger_down)) {
- struct point mouse = msg.inject_touch_event.position.point;
- struct point vfinger = inverse_point(mouse, im->screen->frame_size);
+ struct sc_point mouse =
+ screen_convert_window_to_frame_coords(im->screen, event->x,
+ event->y);
+ struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size);
enum android_motionevent_action action = down
? AMOTION_EVENT_ACTION_DOWN
: AMOTION_EVENT_ACTION_UP;
@@ -773,39 +644,10 @@ input_manager_process_mouse_button(struct input_manager *im,
}
}
-static bool
-convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
- struct control_msg *to) {
-
- // mouse_x and mouse_y are expressed in pixels relative to the window
- int mouse_x;
- int mouse_y;
- SDL_GetMouseState(&mouse_x, &mouse_y);
-
- struct position position = {
- .screen_size = screen->frame_size,
- .point = screen_convert_window_to_frame_coords(screen,
- mouse_x, mouse_y),
- };
-
- to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
-
- to->inject_scroll_event.position = position;
- to->inject_scroll_event.hscroll = from->x;
- to->inject_scroll_event.vscroll = from->y;
-
- return true;
-}
-
static void
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event) {
- struct control_msg msg;
- if (convert_mouse_wheel(event, im->screen, &msg)) {
- if (!controller_push_msg(im->controller, &msg)) {
- LOGW("Could not request 'inject mouse wheel event'");
- }
- }
+ im->mp->ops->process_mouse_wheel(im->mp, event);
}
bool
diff --git a/app/src/input_manager.h b/app/src/input_manager.h
index 1dd7825f..f018f98a 100644
--- a/app/src/input_manager.h
+++ b/app/src/input_manager.h
@@ -9,20 +9,19 @@
#include "controller.h"
#include "fps_counter.h"
-#include "scrcpy.h"
+#include "options.h"
#include "screen.h"
+#include "trait/key_processor.h"
+#include "trait/mouse_processor.h"
struct input_manager {
struct controller *controller;
struct screen *screen;
- // SDL reports repeated events as a boolean, but Android expects the actual
- // number of repetitions. This variable keeps track of the count.
- unsigned repeat;
+ struct sc_key_processor *kp;
+ struct sc_mouse_processor *mp;
bool control;
- bool forward_key_repeat;
- bool prefer_text;
bool forward_all_clicks;
bool legacy_paste;
@@ -43,7 +42,9 @@ struct input_manager {
void
input_manager_init(struct input_manager *im, struct controller *controller,
- struct screen *screen, const struct scrcpy_options *options);
+ struct screen *screen, struct sc_key_processor *kp,
+ struct sc_mouse_processor *mp,
+ const struct scrcpy_options *options);
bool
input_manager_handle_event(struct input_manager *im, SDL_Event *event);
diff --git a/app/src/event_converter.c b/app/src/keyboard_inject.c
similarity index 65%
rename from app/src/event_converter.c
rename to app/src/keyboard_inject.c
index a3c2da89..bcc85da8 100644
--- a/app/src/event_converter.c
+++ b/app/src/keyboard_inject.c
@@ -1,9 +1,20 @@
-#include "event_converter.h"
+#include "keyboard_inject.h"
+
+#include
+#include
+
+#include "android/input.h"
+#include "control_msg.h"
+#include "controller.h"
+#include "util/log.h"
+
+/** Downcast key processor to sc_keyboard_inject */
+#define DOWNCAST(KP) \
+ container_of(KP, struct sc_keyboard_inject, key_processor)
#define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false
-
-bool
+static bool
convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
switch (from) {
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
@@ -12,67 +23,7 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
}
}
-static enum android_metastate
-autocomplete_metastate(enum android_metastate metastate) {
- // fill dependent flags
- if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
- metastate |= AMETA_SHIFT_ON;
- }
- if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) {
- metastate |= AMETA_CTRL_ON;
- }
- if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) {
- metastate |= AMETA_ALT_ON;
- }
- if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) {
- metastate |= AMETA_META_ON;
- }
-
- return metastate;
-}
-
-enum android_metastate
-convert_meta_state(SDL_Keymod mod) {
- enum android_metastate metastate = 0;
- if (mod & KMOD_LSHIFT) {
- metastate |= AMETA_SHIFT_LEFT_ON;
- }
- if (mod & KMOD_RSHIFT) {
- metastate |= AMETA_SHIFT_RIGHT_ON;
- }
- if (mod & KMOD_LCTRL) {
- metastate |= AMETA_CTRL_LEFT_ON;
- }
- if (mod & KMOD_RCTRL) {
- metastate |= AMETA_CTRL_RIGHT_ON;
- }
- if (mod & KMOD_LALT) {
- metastate |= AMETA_ALT_LEFT_ON;
- }
- if (mod & KMOD_RALT) {
- metastate |= AMETA_ALT_RIGHT_ON;
- }
- if (mod & KMOD_LGUI) { // Windows key
- metastate |= AMETA_META_LEFT_ON;
- }
- if (mod & KMOD_RGUI) { // Windows key
- metastate |= AMETA_META_RIGHT_ON;
- }
- if (mod & KMOD_NUM) {
- metastate |= AMETA_NUM_LOCK_ON;
- }
- if (mod & KMOD_CAPS) {
- metastate |= AMETA_CAPS_LOCK_ON;
- }
- if (mod & KMOD_MODE) { // Alt Gr
- // no mapping?
- }
-
- // fill the dependent fields
- return autocomplete_metastate(metastate);
-}
-
-bool
+static bool
convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
bool prefer_text) {
switch (from) {
@@ -154,42 +105,150 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod,
}
}
-enum android_motionevent_buttons
-convert_mouse_buttons(uint32_t state) {
- enum android_motionevent_buttons buttons = 0;
- if (state & SDL_BUTTON_LMASK) {
- buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
+static enum android_metastate
+autocomplete_metastate(enum android_metastate metastate) {
+ // fill dependent flags
+ if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
+ metastate |= AMETA_SHIFT_ON;
}
- if (state & SDL_BUTTON_RMASK) {
- buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
+ if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) {
+ metastate |= AMETA_CTRL_ON;
}
- if (state & SDL_BUTTON_MMASK) {
- buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
+ if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) {
+ metastate |= AMETA_ALT_ON;
}
- if (state & SDL_BUTTON_X1MASK) {
- buttons |= AMOTION_EVENT_BUTTON_BACK;
+ if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) {
+ metastate |= AMETA_META_ON;
}
- if (state & SDL_BUTTON_X2MASK) {
- buttons |= AMOTION_EVENT_BUTTON_FORWARD;
- }
- return buttons;
+
+ return metastate;
}
-bool
-convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
- switch (from) {
- MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
- MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
- FAIL;
+static enum android_metastate
+convert_meta_state(SDL_Keymod mod) {
+ enum android_metastate metastate = 0;
+ if (mod & KMOD_LSHIFT) {
+ metastate |= AMETA_SHIFT_LEFT_ON;
+ }
+ if (mod & KMOD_RSHIFT) {
+ metastate |= AMETA_SHIFT_RIGHT_ON;
+ }
+ if (mod & KMOD_LCTRL) {
+ metastate |= AMETA_CTRL_LEFT_ON;
+ }
+ if (mod & KMOD_RCTRL) {
+ metastate |= AMETA_CTRL_RIGHT_ON;
+ }
+ if (mod & KMOD_LALT) {
+ metastate |= AMETA_ALT_LEFT_ON;
+ }
+ if (mod & KMOD_RALT) {
+ metastate |= AMETA_ALT_RIGHT_ON;
+ }
+ if (mod & KMOD_LGUI) { // Windows key
+ metastate |= AMETA_META_LEFT_ON;
+ }
+ if (mod & KMOD_RGUI) { // Windows key
+ metastate |= AMETA_META_RIGHT_ON;
+ }
+ if (mod & KMOD_NUM) {
+ metastate |= AMETA_NUM_LOCK_ON;
+ }
+ if (mod & KMOD_CAPS) {
+ metastate |= AMETA_CAPS_LOCK_ON;
+ }
+ if (mod & KMOD_MODE) { // Alt Gr
+ // no mapping?
+ }
+
+ // fill the dependent fields
+ return autocomplete_metastate(metastate);
+}
+
+static bool
+convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
+ bool prefer_text, uint32_t repeat) {
+ to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE;
+
+ if (!convert_keycode_action(from->type, &to->inject_keycode.action)) {
+ return false;
+ }
+
+ uint16_t mod = from->keysym.mod;
+ if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod,
+ prefer_text)) {
+ return false;
+ }
+
+ to->inject_keycode.repeat = repeat;
+ to->inject_keycode.metastate = convert_meta_state(mod);
+
+ return true;
+}
+
+static void
+sc_key_processor_process_key(struct sc_key_processor *kp,
+ const SDL_KeyboardEvent *event) {
+ struct sc_keyboard_inject *ki = DOWNCAST(kp);
+
+ if (event->repeat) {
+ if (!ki->forward_key_repeat) {
+ return;
+ }
+ ++ki->repeat;
+ } else {
+ ki->repeat = 0;
+ }
+
+ struct control_msg msg;
+ if (convert_input_key(event, &msg, ki->prefer_text, ki->repeat)) {
+ if (!controller_push_msg(ki->controller, &msg)) {
+ LOGW("Could not request 'inject keycode'");
+ }
}
}
-bool
-convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
- switch (from) {
- MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
- MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN);
- MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP);
- FAIL;
+static void
+sc_key_processor_process_text(struct sc_key_processor *kp,
+ const SDL_TextInputEvent *event) {
+ struct sc_keyboard_inject *ki = DOWNCAST(kp);
+
+ if (!ki->prefer_text) {
+ char c = event->text[0];
+ if (isalpha(c) || c == ' ') {
+ assert(event->text[1] == '\0');
+ // letters and space are handled as raw key event
+ return;
+ }
+ }
+
+ struct control_msg msg;
+ msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
+ msg.inject_text.text = strdup(event->text);
+ if (!msg.inject_text.text) {
+ LOGW("Could not strdup input text");
+ return;
+ }
+ if (!controller_push_msg(ki->controller, &msg)) {
+ free(msg.inject_text.text);
+ LOGW("Could not request 'inject text'");
}
}
+
+void
+sc_keyboard_inject_init(struct sc_keyboard_inject *ki,
+ struct controller *controller,
+ const struct scrcpy_options *options) {
+ ki->controller = controller;
+ ki->prefer_text = options->prefer_text;
+ ki->forward_key_repeat = options->forward_key_repeat;
+
+ ki->repeat = 0;
+
+ static const struct sc_key_processor_ops ops = {
+ .process_key = sc_key_processor_process_key,
+ .process_text = sc_key_processor_process_text,
+ };
+
+ ki->key_processor.ops = &ops;
+}
diff --git a/app/src/keyboard_inject.h b/app/src/keyboard_inject.h
new file mode 100644
index 00000000..f4ebe40e
--- /dev/null
+++ b/app/src/keyboard_inject.h
@@ -0,0 +1,30 @@
+#ifndef SC_KEYBOARD_INJECT_H
+#define SC_KEYBOARD_INJECT_H
+
+#include "common.h"
+
+#include
+
+#include "controller.h"
+#include "options.h"
+#include "trait/key_processor.h"
+
+struct sc_keyboard_inject {
+ struct sc_key_processor key_processor; // key processor trait
+
+ struct controller *controller;
+
+ // SDL reports repeated events as a boolean, but Android expects the actual
+ // number of repetitions. This variable keeps track of the count.
+ unsigned repeat;
+
+ bool prefer_text;
+ bool forward_key_repeat;
+};
+
+void
+sc_keyboard_inject_init(struct sc_keyboard_inject *ki,
+ struct controller *controller,
+ const struct scrcpy_options *options);
+
+#endif
diff --git a/app/src/main.c b/app/src/main.c
index 2afa3c4e..831b98fa 100644
--- a/app/src/main.c
+++ b/app/src/main.c
@@ -1,5 +1,3 @@
-#include "scrcpy.h"
-
#include "common.h"
#include
@@ -13,6 +11,8 @@
#include
#include "cli.h"
+#include "options.h"
+#include "scrcpy.h"
#include "util/log.h"
static void
@@ -48,7 +48,7 @@ main(int argc, char *argv[]) {
#endif
struct scrcpy_cli_args args = {
- .opts = SCRCPY_OPTIONS_DEFAULT,
+ .opts = scrcpy_options_default,
.help = false,
.version = false,
};
diff --git a/app/src/mouse_inject.c b/app/src/mouse_inject.c
new file mode 100644
index 00000000..1d5fe230
--- /dev/null
+++ b/app/src/mouse_inject.c
@@ -0,0 +1,211 @@
+#include "mouse_inject.h"
+
+#include
+#include
+
+#include "android/input.h"
+#include "control_msg.h"
+#include "controller.h"
+#include "util/log.h"
+
+/** Downcast mouse processor to sc_mouse_inject */
+#define DOWNCAST(MP) container_of(MP, struct sc_mouse_inject, mouse_processor)
+
+static enum android_motionevent_buttons
+convert_mouse_buttons(uint32_t state) {
+ enum android_motionevent_buttons buttons = 0;
+ if (state & SDL_BUTTON_LMASK) {
+ buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
+ }
+ if (state & SDL_BUTTON_RMASK) {
+ buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
+ }
+ if (state & SDL_BUTTON_MMASK) {
+ buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
+ }
+ if (state & SDL_BUTTON_X1MASK) {
+ buttons |= AMOTION_EVENT_BUTTON_BACK;
+ }
+ if (state & SDL_BUTTON_X2MASK) {
+ buttons |= AMOTION_EVENT_BUTTON_FORWARD;
+ }
+ return buttons;
+}
+
+#define MAP(FROM, TO) case FROM: *to = TO; return true
+#define FAIL default: return false
+static bool
+convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
+ switch (from) {
+ MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
+ MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
+ FAIL;
+ }
+}
+
+static bool
+convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) {
+ switch (from) {
+ MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE);
+ MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN);
+ MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP);
+ FAIL;
+ }
+}
+
+static bool
+convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
+ struct control_msg *to) {
+ to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
+ to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE;
+ to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
+ to->inject_touch_event.position.screen_size = screen->frame_size;
+ to->inject_touch_event.position.point =
+ screen_convert_window_to_frame_coords(screen, from->x, from->y);
+ to->inject_touch_event.pressure = 1.f;
+ to->inject_touch_event.buttons = convert_mouse_buttons(from->state);
+
+ return true;
+}
+
+static bool
+convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
+ struct control_msg *to) {
+ to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
+
+ if (!convert_touch_action(from->type, &to->inject_touch_event.action)) {
+ return false;
+ }
+
+ to->inject_touch_event.pointer_id = from->fingerId;
+ to->inject_touch_event.position.screen_size = screen->frame_size;
+
+ int dw;
+ int dh;
+ SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
+
+ // SDL touch event coordinates are normalized in the range [0; 1]
+ int32_t x = from->x * dw;
+ int32_t y = from->y * dh;
+ to->inject_touch_event.position.point =
+ screen_convert_drawable_to_frame_coords(screen, x, y);
+
+ to->inject_touch_event.pressure = from->pressure;
+ to->inject_touch_event.buttons = 0;
+ return true;
+}
+
+static bool
+convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
+ struct control_msg *to) {
+ to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;
+
+ if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) {
+ return false;
+ }
+
+ to->inject_touch_event.pointer_id = POINTER_ID_MOUSE;
+ to->inject_touch_event.position.screen_size = screen->frame_size;
+ to->inject_touch_event.position.point =
+ screen_convert_window_to_frame_coords(screen, from->x, from->y);
+ to->inject_touch_event.pressure =
+ from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f;
+ to->inject_touch_event.buttons =
+ convert_mouse_buttons(SDL_BUTTON(from->button));
+
+ return true;
+}
+
+static bool
+convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
+ struct control_msg *to) {
+
+ // mouse_x and mouse_y are expressed in pixels relative to the window
+ int mouse_x;
+ int mouse_y;
+ SDL_GetMouseState(&mouse_x, &mouse_y);
+
+ struct sc_position position = {
+ .screen_size = screen->frame_size,
+ .point = screen_convert_window_to_frame_coords(screen,
+ mouse_x, mouse_y),
+ };
+
+ to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT;
+
+ to->inject_scroll_event.position = position;
+ to->inject_scroll_event.hscroll = from->x;
+ to->inject_scroll_event.vscroll = from->y;
+
+ return true;
+}
+
+static void
+sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
+ const SDL_MouseMotionEvent *event) {
+ struct sc_mouse_inject *mi = DOWNCAST(mp);
+
+ struct control_msg msg;
+ if (!convert_mouse_motion(event, mi->screen, &msg)) {
+ return;
+ }
+
+ if (!controller_push_msg(mi->controller, &msg)) {
+ LOGW("Could not request 'inject mouse motion event'");
+ }
+}
+
+static void
+sc_mouse_processor_process_touch(struct sc_mouse_processor *mp,
+ const SDL_TouchFingerEvent *event) {
+ struct sc_mouse_inject *mi = DOWNCAST(mp);
+
+ struct control_msg msg;
+ if (convert_touch(event, mi->screen, &msg)) {
+ if (!controller_push_msg(mi->controller, &msg)) {
+ LOGW("Could not request 'inject touch event'");
+ }
+ }
+}
+
+static void
+sc_mouse_processor_process_mouse_button(struct sc_mouse_processor *mp,
+ const SDL_MouseButtonEvent *event) {
+ struct sc_mouse_inject *mi = DOWNCAST(mp);
+
+ struct control_msg msg;
+ if (convert_mouse_button(event, mi->screen, &msg)) {
+ if (!controller_push_msg(mi->controller, &msg)) {
+ LOGW("Could not request 'inject mouse button event'");
+ }
+ }
+}
+
+static void
+sc_mouse_processor_process_mouse_wheel(struct sc_mouse_processor *mp,
+ const SDL_MouseWheelEvent *event) {
+ struct sc_mouse_inject *mi = DOWNCAST(mp);
+
+ struct control_msg msg;
+ if (convert_mouse_wheel(event, mi->screen, &msg)) {
+ if (!controller_push_msg(mi->controller, &msg)) {
+ LOGW("Could not request 'inject mouse wheel event'");
+ }
+ }
+}
+
+void
+sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller,
+ struct screen *screen) {
+ mi->controller = controller;
+ mi->screen = screen;
+
+ static const struct sc_mouse_processor_ops ops = {
+ .process_mouse_motion = sc_mouse_processor_process_mouse_motion,
+ .process_touch = sc_mouse_processor_process_touch,
+ .process_mouse_button = sc_mouse_processor_process_mouse_button,
+ .process_mouse_wheel = sc_mouse_processor_process_mouse_wheel,
+ };
+
+ mi->mouse_processor.ops = &ops;
+}
diff --git a/app/src/mouse_inject.h b/app/src/mouse_inject.h
new file mode 100644
index 00000000..7dcf7e83
--- /dev/null
+++ b/app/src/mouse_inject.h
@@ -0,0 +1,23 @@
+#ifndef SC_MOUSE_INJECT_H
+#define SC_MOUSE_INJECT_H
+
+#include "common.h"
+
+#include
+
+#include "controller.h"
+#include "screen.h"
+#include "trait/mouse_processor.h"
+
+struct sc_mouse_inject {
+ struct sc_mouse_processor mouse_processor; // mouse processor trait
+
+ struct controller *controller;
+ struct screen *screen;
+};
+
+void
+sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller,
+ struct screen *screen);
+
+#endif
diff --git a/app/src/options.c b/app/src/options.c
new file mode 100644
index 00000000..82f25342
--- /dev/null
+++ b/app/src/options.c
@@ -0,0 +1,54 @@
+#include "options.h"
+
+const struct scrcpy_options scrcpy_options_default = {
+ .serial = NULL,
+ .crop = NULL,
+ .record_filename = NULL,
+ .window_title = NULL,
+ .push_target = NULL,
+ .render_driver = NULL,
+ .codec_options = NULL,
+ .encoder_name = NULL,
+#ifdef HAVE_V4L2
+ .v4l2_device = NULL,
+#endif
+ .log_level = SC_LOG_LEVEL_INFO,
+ .record_format = SC_RECORD_FORMAT_AUTO,
+ .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
+ .port_range = {
+ .first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
+ .last = DEFAULT_LOCAL_PORT_RANGE_LAST,
+ },
+ .shortcut_mods = {
+ .data = {SC_MOD_LALT, SC_MOD_LSUPER},
+ .count = 2,
+ },
+ .max_size = 0,
+ .bit_rate = DEFAULT_BIT_RATE,
+ .max_fps = 0,
+ .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
+ .rotation = 0,
+ .window_x = SC_WINDOW_POSITION_UNDEFINED,
+ .window_y = SC_WINDOW_POSITION_UNDEFINED,
+ .window_width = 0,
+ .window_height = 0,
+ .display_id = 0,
+ .display_buffer = 0,
+ .v4l2_buffer = 0,
+ .show_touches = false,
+ .fullscreen = false,
+ .always_on_top = false,
+ .control = true,
+ .display = true,
+ .turn_screen_off = false,
+ .prefer_text = false,
+ .window_borderless = false,
+ .mipmaps = true,
+ .stay_awake = false,
+ .force_adb_forward = false,
+ .disable_screensaver = false,
+ .forward_key_repeat = true,
+ .forward_all_clicks = false,
+ .legacy_paste = false,
+ .power_off_on_close = false,
+};
diff --git a/app/src/options.h b/app/src/options.h
new file mode 100644
index 00000000..434225b9
--- /dev/null
+++ b/app/src/options.h
@@ -0,0 +1,113 @@
+#ifndef SCRCPY_OPTIONS_H
+#define SCRCPY_OPTIONS_H
+
+#include "common.h"
+
+#include
+#include
+#include
+
+#include "util/tick.h"
+
+enum sc_log_level {
+ SC_LOG_LEVEL_VERBOSE,
+ SC_LOG_LEVEL_DEBUG,
+ SC_LOG_LEVEL_INFO,
+ SC_LOG_LEVEL_WARN,
+ SC_LOG_LEVEL_ERROR,
+};
+
+enum sc_record_format {
+ SC_RECORD_FORMAT_AUTO,
+ SC_RECORD_FORMAT_MP4,
+ SC_RECORD_FORMAT_MKV,
+};
+
+enum sc_lock_video_orientation {
+ SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
+ // lock the current orientation when scrcpy starts
+ SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
+ SC_LOCK_VIDEO_ORIENTATION_0 = 0,
+ SC_LOCK_VIDEO_ORIENTATION_1,
+ SC_LOCK_VIDEO_ORIENTATION_2,
+ SC_LOCK_VIDEO_ORIENTATION_3,
+};
+
+enum sc_keyboard_input_mode {
+ SC_KEYBOARD_INPUT_MODE_INJECT,
+ SC_KEYBOARD_INPUT_MODE_HID,
+};
+
+#define SC_MAX_SHORTCUT_MODS 8
+
+enum sc_shortcut_mod {
+ SC_MOD_LCTRL = 1 << 0,
+ SC_MOD_RCTRL = 1 << 1,
+ SC_MOD_LALT = 1 << 2,
+ SC_MOD_RALT = 1 << 3,
+ SC_MOD_LSUPER = 1 << 4,
+ SC_MOD_RSUPER = 1 << 5,
+};
+
+struct sc_shortcut_mods {
+ unsigned data[SC_MAX_SHORTCUT_MODS];
+ unsigned count;
+};
+
+struct sc_port_range {
+ uint16_t first;
+ uint16_t last;
+};
+
+#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
+
+struct scrcpy_options {
+ const char *serial;
+ const char *crop;
+ const char *record_filename;
+ const char *window_title;
+ const char *push_target;
+ const char *render_driver;
+ const char *codec_options;
+ const char *encoder_name;
+#ifdef HAVE_V4L2
+ const char *v4l2_device;
+#endif
+ enum sc_log_level log_level;
+ enum sc_record_format record_format;
+ enum sc_keyboard_input_mode keyboard_input_mode;
+ struct sc_port_range port_range;
+ struct sc_shortcut_mods shortcut_mods;
+ uint16_t max_size;
+ uint32_t bit_rate;
+ uint16_t max_fps;
+ enum sc_lock_video_orientation lock_video_orientation;
+ uint8_t rotation;
+ int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
+ int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
+ uint16_t window_width;
+ uint16_t window_height;
+ uint32_t display_id;
+ sc_tick display_buffer;
+ sc_tick v4l2_buffer;
+ bool show_touches;
+ bool fullscreen;
+ bool always_on_top;
+ bool control;
+ bool display;
+ bool turn_screen_off;
+ bool prefer_text;
+ bool window_borderless;
+ bool mipmaps;
+ bool stay_awake;
+ bool force_adb_forward;
+ bool disable_screensaver;
+ bool forward_key_repeat;
+ bool forward_all_clicks;
+ bool legacy_paste;
+ bool power_off_on_close;
+};
+
+extern const struct scrcpy_options scrcpy_options_default;
+
+#endif
diff --git a/app/src/receiver.c b/app/src/receiver.c
index 337d2a17..b5cf9b39 100644
--- a/app/src/receiver.c
+++ b/app/src/receiver.c
@@ -7,7 +7,7 @@
#include "util/log.h"
bool
-receiver_init(struct receiver *receiver, socket_t control_socket) {
+receiver_init(struct receiver *receiver, sc_socket control_socket) {
bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) {
return false;
diff --git a/app/src/receiver.h b/app/src/receiver.h
index 36523b62..99f128a4 100644
--- a/app/src/receiver.h
+++ b/app/src/receiver.h
@@ -11,13 +11,13 @@
// receive events from the device
// managed by the controller
struct receiver {
- socket_t control_socket;
+ sc_socket control_socket;
sc_thread thread;
sc_mutex mutex;
};
bool
-receiver_init(struct receiver *receiver, socket_t control_socket);
+receiver_init(struct receiver *receiver, sc_socket control_socket);
void
receiver_destroy(struct receiver *receiver);
diff --git a/app/src/recorder.c b/app/src/recorder.c
index 85570324..b9c585f4 100644
--- a/app/src/recorder.c
+++ b/app/src/recorder.c
@@ -1,10 +1,12 @@
#include "recorder.h"
#include
+#include
+#include
#include
#include "util/log.h"
-#include "util/str_util.h"
+#include "util/str.h"
/** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink)
@@ -24,7 +26,7 @@ find_muxer(const char *name) {
oformat = av_oformat_next(oformat);
#endif
// until null or containing the requested name
- } while (oformat && !strlist_contains(oformat->name, ',', name));
+ } while (oformat && !sc_str_list_contains(oformat->name, ',', name));
return oformat;
}
@@ -51,16 +53,15 @@ record_packet_new(const AVPacket *packet) {
static void
record_packet_delete(struct record_packet *rec) {
- av_packet_unref(rec->packet);
av_packet_free(&rec->packet);
free(rec);
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
- while (!queue_is_empty(queue)) {
+ while (!sc_queue_is_empty(queue)) {
struct record_packet *rec;
- queue_take(queue, next, &rec);
+ sc_queue_take(queue, next, &rec);
record_packet_delete(rec);
}
}
@@ -136,14 +137,14 @@ run_recorder(void *data) {
for (;;) {
sc_mutex_lock(&recorder->mutex);
- while (!recorder->stopped && queue_is_empty(&recorder->queue)) {
+ 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 && queue_is_empty(&recorder->queue)) {
+ if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
sc_mutex_unlock(&recorder->mutex);
struct record_packet *last = recorder->previous;
if (last) {
@@ -162,7 +163,7 @@ run_recorder(void *data) {
}
struct record_packet *rec;
- queue_take(&recorder->queue, next, &rec);
+ sc_queue_take(&recorder->queue, next, &rec);
sc_mutex_unlock(&recorder->mutex);
@@ -214,7 +215,8 @@ run_recorder(void *data) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
- LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
+ LOGI("Recording complete to %s file: %s", format_name,
+ recorder->filename);
}
LOGD("Recorder thread ended");
@@ -236,7 +238,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
goto error_mutex_destroy;
}
- queue_init(&recorder->queue);
+ sc_queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->header_written = false;
@@ -341,7 +343,7 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) {
return false;
}
- queue_push(&recorder->queue, next, rec);
+ sc_queue_push(&recorder->queue, next, rec);
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
@@ -370,7 +372,7 @@ bool
recorder_init(struct recorder *recorder,
const char *filename,
enum sc_record_format format,
- struct size declared_frame_size) {
+ struct sc_size declared_frame_size) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
diff --git a/app/src/recorder.h b/app/src/recorder.h
index 0c376cd1..27ea5526 100644
--- a/app/src/recorder.h
+++ b/app/src/recorder.h
@@ -7,7 +7,7 @@
#include
#include "coords.h"
-#include "scrcpy.h"
+#include "options.h"
#include "trait/packet_sink.h"
#include "util/queue.h"
#include "util/thread.h"
@@ -17,7 +17,7 @@ struct record_packet {
struct record_packet *next;
};
-struct recorder_queue QUEUE(struct record_packet);
+struct recorder_queue SC_QUEUE(struct record_packet);
struct recorder {
struct sc_packet_sink packet_sink; // packet sink trait
@@ -25,7 +25,7 @@ struct recorder {
char *filename;
enum sc_record_format format;
AVFormatContext *ctx;
- struct size declared_frame_size;
+ struct sc_size declared_frame_size;
bool header_written;
sc_thread thread;
@@ -44,7 +44,7 @@ struct recorder {
bool
recorder_init(struct recorder *recorder, const char *filename,
- enum sc_record_format format, struct size declared_frame_size);
+ enum sc_record_format format, struct sc_size declared_frame_size);
void
recorder_destroy(struct recorder *recorder);
diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c
index 4dcb412f..9643b04e 100644
--- a/app/src/scrcpy.c
+++ b/app/src/scrcpy.c
@@ -18,11 +18,15 @@
#include "events.h"
#include "file_handler.h"
#include "input_manager.h"
+#ifdef HAVE_AOA_HID
+# include "hid_keyboard.h"
+#endif
+#include "keyboard_inject.h"
+#include "mouse_inject.h"
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "stream.h"
-#include "tiny_xpm.h"
#include "util/log.h"
#include "util/net.h"
#ifdef HAVE_V4L2
@@ -30,7 +34,7 @@
#endif
struct scrcpy {
- struct server server;
+ struct sc_server server;
struct screen screen;
struct stream stream;
struct decoder decoder;
@@ -40,44 +44,43 @@ struct scrcpy {
#endif
struct controller controller;
struct file_handler file_handler;
+#ifdef HAVE_AOA_HID
+ struct sc_aoa aoa;
+#endif
+ union {
+ struct sc_keyboard_inject keyboard_inject;
+#ifdef HAVE_AOA_HID
+ struct sc_hid_keyboard keyboard_hid;
+#endif
+ };
+ struct sc_mouse_inject mouse_inject;
struct input_manager input_manager;
};
+static inline void
+push_event(uint32_t type, const char *name) {
+ SDL_Event event;
+ event.type = type;
+ int ret = SDL_PushEvent(&event);
+ if (ret < 0) {
+ LOGE("Could not post %s event: %s", name, SDL_GetError());
+ // What could we do?
+ }
+}
+#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE)
+
#ifdef _WIN32
BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
if (ctrl_type == CTRL_C_EVENT) {
- SDL_Event event;
- event.type = SDL_QUIT;
- SDL_PushEvent(&event);
+ PUSH_EVENT(SDL_QUIT);
return TRUE;
}
return FALSE;
}
#endif // _WIN32
-// init SDL and set appropriate hints
-static bool
-sdl_init_and_configure(bool display, const char *render_driver,
- bool disable_screensaver) {
- uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS;
- if (SDL_Init(flags)) {
- LOGC("Could not initialize SDL: %s", SDL_GetError());
- return false;
- }
-
- atexit(SDL_Quit);
-
-#ifdef _WIN32
- // Clean up properly on Ctrl+C on Windows
- bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
- if (!ok) {
- LOGW("Could not set Ctrl+C handler");
- }
-#endif // _WIN32
-
- if (!display) {
- return true;
- }
+static void
+sdl_set_hints(const char *render_driver) {
if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) {
LOGW("Could not set render driver");
@@ -95,6 +98,15 @@ sdl_init_and_configure(bool display, const char *render_driver,
}
#endif
+#ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
+ // Disable synthetic mouse events from touch events
+ // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is
+ // better not to generate them in the first place.
+ if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) {
+ LOGW("Could not disable synthetic mouse events");
+ }
+#endif
+
#ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
// Disable compositor bypassing on X11
if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) {
@@ -106,6 +118,21 @@ sdl_init_and_configure(bool display, const char *render_driver,
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
LOGW("Could not disable minimize on focus loss");
}
+}
+
+static void
+sdl_configure(bool display, bool disable_screensaver) {
+#ifdef _WIN32
+ // Clean up properly on Ctrl+C on Windows
+ bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
+ if (!ok) {
+ LOGW("Could not set Ctrl+C handler");
+ }
+#endif // _WIN32
+
+ if (!display) {
+ return;
+ }
if (disable_screensaver) {
LOGD("Screensaver disabled");
@@ -114,8 +141,6 @@ sdl_init_and_configure(bool display, const char *render_driver,
LOGD("Screensaver enabled");
SDL_EnableScreenSaver();
}
-
- return true;
}
static bool
@@ -192,6 +217,29 @@ event_loop(struct scrcpy *s, const struct scrcpy_options *options) {
return false;
}
+static bool
+await_for_server(void) {
+ SDL_Event event;
+ while (SDL_WaitEvent(&event)) {
+ switch (event.type) {
+ case SDL_QUIT:
+ LOGD("User requested to quit");
+ return false;
+ case EVENT_SERVER_CONNECTION_FAILED:
+ LOGE("Server connection failed");
+ return false;
+ case EVENT_SERVER_CONNECTED:
+ LOGD("Server connected");
+ return true;
+ default:
+ break;
+ }
+ }
+
+ LOGE("SDL_WaitEvent() error: %s", SDL_GetError());
+ return false;
+}
+
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
@@ -234,20 +282,48 @@ stream_on_eos(struct stream *stream, void *userdata) {
(void) stream;
(void) userdata;
- SDL_Event stop_event;
- stop_event.type = EVENT_STREAM_STOPPED;
- SDL_PushEvent(&stop_event);
+ PUSH_EVENT(EVENT_STREAM_STOPPED);
+}
+
+static void
+sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
+ (void) server;
+ (void) userdata;
+
+ PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
+}
+
+static void
+sc_server_on_connected(struct sc_server *server, void *userdata) {
+ (void) server;
+ (void) userdata;
+
+ PUSH_EVENT(EVENT_SERVER_CONNECTED);
+}
+
+static void
+sc_server_on_disconnected(struct sc_server *server, void *userdata) {
+ (void) server;
+ (void) userdata;
+
+ LOGD("Server disconnected");
+ // Do nothing, the disconnection will be handled by the "stream stopped"
+ // event
}
bool
-scrcpy(const struct scrcpy_options *options) {
+scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy;
struct scrcpy *s = &scrcpy;
- if (!server_init(&s->server)) {
+ // Minimal SDL initialization
+ if (SDL_Init(SDL_INIT_EVENTS)) {
+ LOGC("Could not initialize SDL: %s", SDL_GetError());
return false;
}
+ atexit(SDL_Quit);
+
bool ret = false;
bool server_started = false;
@@ -257,12 +333,14 @@ scrcpy(const struct scrcpy_options *options) {
bool v4l2_sink_initialized = false;
#endif
bool stream_started = false;
+#ifdef HAVE_AOA_HID
+ bool aoa_hid_initialized = false;
+#endif
bool controller_initialized = false;
bool controller_started = false;
bool screen_initialized = false;
- bool record = !!options->record_filename;
- struct server_params params = {
+ struct sc_server_params params = {
.serial = options->serial,
.log_level = options->log_level,
.crop = options->crop,
@@ -280,26 +358,44 @@ scrcpy(const struct scrcpy_options *options) {
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
};
- if (!server_start(&s->server, ¶ms)) {
+
+ static const struct sc_server_callbacks cbs = {
+ .on_connection_failed = sc_server_on_connection_failed,
+ .on_connected = sc_server_on_connected,
+ .on_disconnected = sc_server_on_disconnected,
+ };
+ if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) {
+ return false;
+ }
+
+ if (!sc_server_start(&s->server)) {
goto end;
}
server_started = true;
- if (!sdl_init_and_configure(options->display, options->render_driver,
- options->disable_screensaver)) {
+ if (options->display) {
+ sdl_set_hints(options->render_driver);
+ }
+
+ // Initialize SDL video in addition if display is enabled
+ if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
+ LOGC("Could not initialize SDL: %s", SDL_GetError());
goto end;
}
- char device_name[DEVICE_NAME_FIELD_LENGTH];
- struct size frame_size;
+ sdl_configure(options->display, options->disable_screensaver);
- if (!server_connect_to(&s->server, device_name, &frame_size)) {
+ // Await for server without blocking Ctrl+C handling
+ if (!await_for_server()) {
goto end;
}
+ // It is necessarily initialized here, since the device is connected
+ struct sc_server_info *info = &s->server.info;
+
if (options->display && options->control) {
- if (!file_handler_init(&s->file_handler, s->server.serial,
+ if (!file_handler_init(&s->file_handler, options->serial,
options->push_target)) {
goto end;
}
@@ -317,11 +413,11 @@ scrcpy(const struct scrcpy_options *options) {
}
struct recorder *rec = NULL;
- if (record) {
+ if (options->record_filename) {
if (!recorder_init(&s->recorder,
options->record_filename,
options->record_format,
- frame_size)) {
+ info->frame_size)) {
goto end;
}
rec = &s->recorder;
@@ -330,7 +426,7 @@ scrcpy(const struct scrcpy_options *options) {
av_log_set_callback(av_log_callback);
- const struct stream_callbacks stream_cbs = {
+ static const struct stream_callbacks stream_cbs = {
.on_eos = stream_on_eos,
};
stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL);
@@ -343,42 +439,16 @@ scrcpy(const struct scrcpy_options *options) {
stream_add_sink(&s->stream, &rec->packet_sink);
}
- if (options->display) {
- if (options->control) {
- if (!controller_init(&s->controller, s->server.control_socket)) {
- goto end;
- }
- controller_initialized = true;
-
- if (!controller_start(&s->controller)) {
- goto end;
- }
- controller_started = true;
- }
-
- const char *window_title =
- options->window_title ? options->window_title : device_name;
-
- struct screen_params screen_params = {
- .window_title = window_title,
- .frame_size = frame_size,
- .always_on_top = options->always_on_top,
- .window_x = options->window_x,
- .window_y = options->window_y,
- .window_width = options->window_width,
- .window_height = options->window_height,
- .window_borderless = options->window_borderless,
- .rotation = options->rotation,
- .mipmaps = options->mipmaps,
- .fullscreen = options->fullscreen,
- };
-
- if (!screen_init(&s->screen, &screen_params)) {
+ if (options->control) {
+ if (!controller_init(&s->controller, s->server.control_socket)) {
goto end;
}
- screen_initialized = true;
+ controller_initialized = true;
- decoder_add_sink(&s->decoder, &s->screen.frame_sink);
+ if (!controller_start(&s->controller)) {
+ goto end;
+ }
+ controller_started = true;
if (options->turn_screen_off) {
struct control_msg msg;
@@ -391,9 +461,37 @@ scrcpy(const struct scrcpy_options *options) {
}
}
+ if (options->display) {
+ const char *window_title =
+ options->window_title ? options->window_title : info->device_name;
+
+ struct screen_params screen_params = {
+ .window_title = window_title,
+ .frame_size = info->frame_size,
+ .always_on_top = options->always_on_top,
+ .window_x = options->window_x,
+ .window_y = options->window_y,
+ .window_width = options->window_width,
+ .window_height = options->window_height,
+ .window_borderless = options->window_borderless,
+ .rotation = options->rotation,
+ .mipmaps = options->mipmaps,
+ .fullscreen = options->fullscreen,
+ .buffering_time = options->display_buffer,
+ };
+
+ if (!screen_init(&s->screen, &screen_params)) {
+ goto end;
+ }
+ screen_initialized = true;
+
+ decoder_add_sink(&s->decoder, &s->screen.frame_sink);
+ }
+
#ifdef HAVE_V4L2
if (options->v4l2_device) {
- if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size)) {
+ if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
+ info->frame_size, options->v4l2_buffer)) {
goto end;
}
@@ -410,7 +508,77 @@ scrcpy(const struct scrcpy_options *options) {
}
stream_started = true;
- input_manager_init(&s->input_manager, &s->controller, &s->screen, options);
+ struct sc_key_processor *kp = NULL;
+ struct sc_mouse_processor *mp = NULL;
+
+ if (options->control) {
+ if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) {
+#ifdef HAVE_AOA_HID
+ bool aoa_hid_ok = false;
+
+ char *serialno = NULL;
+
+ const char *serial = options->serial;
+ if (!serial) {
+ serialno = adb_get_serialno();
+ if (!serialno) {
+ LOGE("Could not get device serial");
+ goto aoa_hid_end;
+ }
+ serial = serialno;
+ LOGI("Device serial: %s", serial);
+ }
+
+ bool ok = sc_aoa_init(&s->aoa, serial);
+ free(serialno);
+ if (!ok) {
+ goto aoa_hid_end;
+ }
+
+ if (!sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) {
+ sc_aoa_destroy(&s->aoa);
+ goto aoa_hid_end;
+ }
+
+ if (!sc_aoa_start(&s->aoa)) {
+ sc_hid_keyboard_destroy(&s->keyboard_hid);
+ sc_aoa_destroy(&s->aoa);
+ goto aoa_hid_end;
+ }
+
+ aoa_hid_ok = true;
+ kp = &s->keyboard_hid.key_processor;
+
+ aoa_hid_initialized = true;
+
+aoa_hid_end:
+ if (!aoa_hid_ok) {
+ LOGE("Failed to enable HID over AOA, "
+ "fallback to default keyboard injection method "
+ "(-K/--hid-keyboard ignored)");
+ options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT;
+ }
+#else
+ LOGE("HID over AOA is not supported on this platform, "
+ "fallback to default keyboard injection method "
+ "(-K/--hid-keyboard ignored)");
+ options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT;
+#endif
+ }
+
+ // keyboard_input_mode may have been reset if HID mode failed
+ if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) {
+ sc_keyboard_inject_init(&s->keyboard_inject, &s->controller,
+ options);
+ kp = &s->keyboard_inject.key_processor;
+ }
+
+ sc_mouse_inject_init(&s->mouse_inject, &s->controller, &s->screen);
+ mp = &s->mouse_inject.mouse_processor;
+ }
+
+ input_manager_init(&s->input_manager, &s->controller, &s->screen, kp, mp,
+ options);
ret = event_loop(s, options);
LOGD("quit...");
@@ -422,6 +590,12 @@ scrcpy(const struct scrcpy_options *options) {
end:
// The stream is not stopped explicitly, because it will stop by itself on
// end-of-stream
+#ifdef HAVE_AOA_HID
+ if (aoa_hid_initialized) {
+ sc_hid_keyboard_destroy(&s->keyboard_hid);
+ sc_aoa_stop(&s->aoa);
+ }
+#endif
if (controller_started) {
controller_stop(&s->controller);
}
@@ -434,7 +608,7 @@ end:
if (server_started) {
// shutdown the sockets and kill the server
- server_stop(&s->server);
+ sc_server_stop(&s->server);
}
// now that the sockets are shutdown, the stream and controller are
@@ -449,6 +623,13 @@ end:
}
#endif
+#ifdef HAVE_AOA_HID
+ if (aoa_hid_initialized) {
+ sc_aoa_join(&s->aoa);
+ sc_aoa_destroy(&s->aoa);
+ }
+#endif
+
// Destroy the screen only after the stream is guaranteed to be finished,
// because otherwise the screen could receive new frames after destruction
if (screen_initialized) {
@@ -472,7 +653,7 @@ end:
file_handler_destroy(&s->file_handler);
}
- server_destroy(&s->server);
+ sc_server_destroy(&s->server);
return ret;
}
diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h
index 0a2deb71..cdcecda7 100644
--- a/app/src/scrcpy.h
+++ b/app/src/scrcpy.h
@@ -4,147 +4,9 @@
#include "common.h"
#include
-#include
-#include
-
-enum sc_log_level {
- SC_LOG_LEVEL_VERBOSE,
- SC_LOG_LEVEL_DEBUG,
- SC_LOG_LEVEL_INFO,
- SC_LOG_LEVEL_WARN,
- SC_LOG_LEVEL_ERROR,
-};
-
-enum sc_record_format {
- SC_RECORD_FORMAT_AUTO,
- SC_RECORD_FORMAT_MP4,
- SC_RECORD_FORMAT_MKV,
-};
-
-enum sc_lock_video_orientation {
- SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
- // lock the current orientation when scrcpy starts
- SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
- SC_LOCK_VIDEO_ORIENTATION_0 = 0,
- SC_LOCK_VIDEO_ORIENTATION_1,
- SC_LOCK_VIDEO_ORIENTATION_2,
- SC_LOCK_VIDEO_ORIENTATION_3,
-};
-
-#define SC_MAX_SHORTCUT_MODS 8
-
-enum sc_shortcut_mod {
- SC_MOD_LCTRL = 1 << 0,
- SC_MOD_RCTRL = 1 << 1,
- SC_MOD_LALT = 1 << 2,
- SC_MOD_RALT = 1 << 3,
- SC_MOD_LSUPER = 1 << 4,
- SC_MOD_RSUPER = 1 << 5,
-};
-
-struct sc_shortcut_mods {
- unsigned data[SC_MAX_SHORTCUT_MODS];
- unsigned count;
-};
-
-struct sc_port_range {
- uint16_t first;
- uint16_t last;
-};
-
-#define SC_WINDOW_POSITION_UNDEFINED (-0x8000)
-
-struct scrcpy_options {
- const char *serial;
- const char *crop;
- const char *record_filename;
- const char *window_title;
- const char *push_target;
- const char *render_driver;
- const char *codec_options;
- const char *encoder_name;
- const char *v4l2_device;
- enum sc_log_level log_level;
- enum sc_record_format record_format;
- struct sc_port_range port_range;
- struct sc_shortcut_mods shortcut_mods;
- uint16_t max_size;
- uint32_t bit_rate;
- uint16_t max_fps;
- enum sc_lock_video_orientation lock_video_orientation;
- uint8_t rotation;
- int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
- int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
- uint16_t window_width;
- uint16_t window_height;
- uint32_t display_id;
- bool show_touches;
- bool fullscreen;
- bool always_on_top;
- bool control;
- bool display;
- bool turn_screen_off;
- bool prefer_text;
- bool window_borderless;
- bool mipmaps;
- bool stay_awake;
- bool force_adb_forward;
- bool disable_screensaver;
- bool forward_key_repeat;
- bool forward_all_clicks;
- bool legacy_paste;
- bool power_off_on_close;
-};
-
-#define SCRCPY_OPTIONS_DEFAULT { \
- .serial = NULL, \
- .crop = NULL, \
- .record_filename = NULL, \
- .window_title = NULL, \
- .push_target = NULL, \
- .render_driver = NULL, \
- .codec_options = NULL, \
- .encoder_name = NULL, \
- .v4l2_device = NULL, \
- .log_level = SC_LOG_LEVEL_INFO, \
- .record_format = SC_RECORD_FORMAT_AUTO, \
- .port_range = { \
- .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \
- .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \
- }, \
- .shortcut_mods = { \
- .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
- .count = 2, \
- }, \
- .max_size = 0, \
- .bit_rate = DEFAULT_BIT_RATE, \
- .max_fps = 0, \
- .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \
- .rotation = 0, \
- .window_x = SC_WINDOW_POSITION_UNDEFINED, \
- .window_y = SC_WINDOW_POSITION_UNDEFINED, \
- .window_width = 0, \
- .window_height = 0, \
- .display_id = 0, \
- .show_touches = false, \
- .fullscreen = false, \
- .always_on_top = false, \
- .control = true, \
- .display = true, \
- .turn_screen_off = false, \
- .prefer_text = false, \
- .window_borderless = false, \
- .mipmaps = true, \
- .stay_awake = false, \
- .force_adb_forward = false, \
- .disable_screensaver = false, \
- .forward_key_repeat = true, \
- .forward_all_clicks = false, \
- .legacy_paste = false, \
- .power_off_on_close = false, \
-}
+#include "options.h"
bool
-scrcpy(const struct scrcpy_options *options);
+scrcpy(struct scrcpy_options *options);
#endif
diff --git a/app/src/screen.c b/app/src/screen.c
index 99327b3b..d402b402 100644
--- a/app/src/screen.c
+++ b/app/src/screen.c
@@ -5,9 +5,8 @@
#include
#include "events.h"
-#include "icon.xpm"
-#include "scrcpy.h"
-#include "tiny_xpm.h"
+#include "icon.h"
+#include "options.h"
#include "video_buffer.h"
#include "util/log.h"
@@ -15,9 +14,9 @@
#define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink)
-static inline struct size
-get_rotated_size(struct size size, int rotation) {
- struct size rotated_size;
+static inline struct sc_size
+get_rotated_size(struct sc_size size, int rotation) {
+ struct sc_size rotated_size;
if (rotation & 1) {
rotated_size.width = size.height;
rotated_size.height = size.width;
@@ -28,26 +27,26 @@ get_rotated_size(struct size size, int rotation) {
return rotated_size;
}
-// get the window size in a struct size
-static struct size
+// get the window size in a struct sc_size
+static struct sc_size
get_window_size(const struct screen *screen) {
int width;
int height;
SDL_GetWindowSize(screen->window, &width, &height);
- struct size size;
+ struct sc_size size;
size.width = width;
size.height = height;
return size;
}
-static struct point
+static struct sc_point
get_window_position(const struct screen *screen) {
int x;
int y;
SDL_GetWindowPosition(screen->window, &x, &y);
- struct point point;
+ struct sc_point point;
point.x = x;
point.y = y;
return point;
@@ -55,7 +54,7 @@ get_window_position(const struct screen *screen) {
// set the window size to be applied when fullscreen is disabled
static void
-set_window_size(struct screen *screen, struct size new_size) {
+set_window_size(struct screen *screen, struct sc_size new_size) {
assert(!screen->fullscreen);
assert(!screen->maximized);
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
@@ -63,7 +62,7 @@ set_window_size(struct screen *screen, struct size new_size) {
// get the preferred display bounds (i.e. the screen bounds with some margins)
static bool
-get_preferred_display_bounds(struct size *bounds) {
+get_preferred_display_bounds(struct sc_size *bounds) {
SDL_Rect rect;
#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS
# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r))
@@ -81,7 +80,7 @@ get_preferred_display_bounds(struct size *bounds) {
}
static bool
-is_optimal_size(struct size current_size, struct size content_size) {
+is_optimal_size(struct sc_size current_size, struct sc_size content_size) {
// The size is optimal if we can recompute one dimension of the current
// size from the other
return current_size.height == current_size.width * content_size.height
@@ -95,16 +94,16 @@ is_optimal_size(struct size current_size, struct size content_size) {
// crops the black borders)
// - it keeps the aspect ratio
// - it scales down to make it fit in the display_size
-static struct size
-get_optimal_size(struct size current_size, struct size content_size) {
+static struct sc_size
+get_optimal_size(struct sc_size current_size, struct sc_size content_size) {
if (content_size.width == 0 || content_size.height == 0) {
// avoid division by 0
return current_size;
}
- struct size window_size;
+ struct sc_size window_size;
- struct size display_size;
+ struct sc_size display_size;
if (!get_preferred_display_bounds(&display_size)) {
// could not get display bounds, do not constraint the size
window_size.width = current_size.width;
@@ -136,10 +135,10 @@ get_optimal_size(struct size current_size, struct size content_size) {
// initially, there is no current size, so use the frame size as current size
// req_width and req_height, if not 0, are the sizes requested by the user
-static inline struct size
-get_initial_optimal_size(struct size content_size, uint16_t req_width,
+static inline struct sc_size
+get_initial_optimal_size(struct sc_size content_size, uint16_t req_width,
uint16_t req_height) {
- struct size window_size;
+ struct sc_size window_size;
if (!req_width && !req_height) {
window_size = get_optimal_size(content_size, content_size);
} else {
@@ -167,9 +166,9 @@ screen_update_content_rect(struct screen *screen) {
int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
- struct size content_size = screen->content_size;
+ struct sc_size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
- struct size drawable_size = {dw, dh};
+ struct sc_size drawable_size = {dw, dh};
SDL_Rect *rect = &screen->rect;
@@ -201,7 +200,7 @@ screen_update_content_rect(struct screen *screen) {
static inline SDL_Texture *
create_texture(struct screen *screen) {
SDL_Renderer *renderer = screen->renderer;
- struct size size = screen->frame_size;
+ struct sc_size size = screen->frame_size;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
@@ -225,6 +224,45 @@ create_texture(struct screen *screen) {
return texture;
}
+// render the texture to the renderer
+//
+// Set the update_content_rect flag if the window or content size may have
+// changed, so that the content rectangle is recomputed
+static void
+screen_render(struct screen *screen, bool update_content_rect) {
+ if (update_content_rect) {
+ screen_update_content_rect(screen);
+ }
+
+ SDL_RenderClear(screen->renderer);
+ if (screen->rotation == 0) {
+ SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
+ } else {
+ // rotation in RenderCopyEx() is clockwise, while screen->rotation is
+ // counterclockwise (to be consistent with --lock-video-orientation)
+ int cw_rotation = (4 - screen->rotation) % 4;
+ double angle = 90 * cw_rotation;
+
+ SDL_Rect *dstrect = NULL;
+ SDL_Rect rect;
+ if (screen->rotation & 1) {
+ rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
+ rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
+ rect.w = screen->rect.h;
+ rect.h = screen->rect.w;
+ dstrect = ▭
+ } else {
+ assert(screen->rotation == 2);
+ dstrect = &screen->rect;
+ }
+
+ SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
+ angle, NULL, 0);
+ }
+ SDL_RenderPresent(screen->renderer);
+}
+
+
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@@ -274,27 +312,43 @@ screen_frame_sink_close(struct sc_frame_sink *sink) {
static bool
screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct screen *screen = DOWNCAST(sink);
+ return sc_video_buffer_push(&screen->vb, frame);
+}
- bool previous_frame_skipped;
- bool ok = video_buffer_push(&screen->vb, frame, &previous_frame_skipped);
- if (!ok) {
- return false;
- }
+static void
+sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
+ void *userdata) {
+ (void) vb;
+ struct screen *screen = userdata;
- if (previous_frame_skipped) {
+ // event_failed implies previous_skipped (the previous frame may not have
+ // been consumed if the event was not sent)
+ assert(!screen->event_failed || previous_skipped);
+
+ bool need_new_event;
+ if (previous_skipped) {
fps_counter_add_skipped_frame(&screen->fps_counter);
// The EVENT_NEW_FRAME triggered for the previous frame will consume
- // this new frame instead
+ // this new frame instead, unless the previous event failed
+ need_new_event = screen->event_failed;
} else {
+ need_new_event = true;
+ }
+
+ if (need_new_event) {
static SDL_Event new_frame_event = {
.type = EVENT_NEW_FRAME,
};
// Post the event on the UI thread
- SDL_PushEvent(&new_frame_event);
+ int ret = SDL_PushEvent(&new_frame_event);
+ if (ret < 0) {
+ LOGW("Could not post new frame event: %s", SDL_GetError());
+ screen->event_failed = true;
+ } else {
+ screen->event_failed = false;
+ }
}
-
- return true;
}
bool
@@ -303,16 +357,28 @@ screen_init(struct screen *screen, const struct screen_params *params) {
screen->has_frame = false;
screen->fullscreen = false;
screen->maximized = false;
+ screen->event_failed = false;
- bool ok = video_buffer_init(&screen->vb);
+ static const struct sc_video_buffer_callbacks cbs = {
+ .on_new_frame = sc_video_buffer_on_new_frame,
+ };
+
+ bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs,
+ screen);
if (!ok) {
LOGE("Could not initialize video buffer");
return false;
}
+ ok = sc_video_buffer_start(&screen->vb);
+ if (!ok) {
+ LOGE("Could not start video_buffer");
+ goto error_destroy_video_buffer;
+ }
+
if (!fps_counter_init(&screen->fps_counter)) {
LOGE("Could not initialize FPS counter");
- goto error_destroy_video_buffer;
+ goto error_stop_and_join_video_buffer;
}
screen->frame_size = params->frame_size;
@@ -320,13 +386,13 @@ screen_init(struct screen *screen, const struct screen_params *params) {
if (screen->rotation) {
LOGI("Initial display rotation set to %u", screen->rotation);
}
- struct size content_size =
+ struct sc_size content_size =
get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size;
- struct size window_size = get_initial_optimal_size(content_size,
- params->window_width,
- params->window_height);
+ struct sc_size window_size =
+ get_initial_optimal_size(content_size,params->window_width,
+ params->window_height);
uint32_t window_flags = SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_ALLOW_HIGHDPI;
@@ -394,10 +460,10 @@ screen_init(struct screen *screen, const struct screen_params *params) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
- SDL_Surface *icon = read_xpm(icon_xpm);
+ SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
- SDL_FreeSurface(icon);
+ scrcpy_icon_destroy(icon);
} else {
LOGW("Could not load icon");
}
@@ -453,8 +519,11 @@ error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
fps_counter_destroy(&screen->fps_counter);
+error_stop_and_join_video_buffer:
+ sc_video_buffer_stop(&screen->vb);
+ sc_video_buffer_join(&screen->vb);
error_destroy_video_buffer:
- video_buffer_destroy(&screen->vb);
+ sc_video_buffer_destroy(&screen->vb);
return false;
}
@@ -471,11 +540,13 @@ screen_hide_window(struct screen *screen) {
void
screen_interrupt(struct screen *screen) {
+ sc_video_buffer_stop(&screen->vb);
fps_counter_interrupt(&screen->fps_counter);
}
void
screen_join(struct screen *screen) {
+ sc_video_buffer_join(&screen->vb);
fps_counter_join(&screen->fps_counter);
}
@@ -489,14 +560,14 @@ screen_destroy(struct screen *screen) {
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
fps_counter_destroy(&screen->fps_counter);
- video_buffer_destroy(&screen->vb);
+ sc_video_buffer_destroy(&screen->vb);
}
static void
-resize_for_content(struct screen *screen, struct size old_content_size,
- struct size new_content_size) {
- struct size window_size = get_window_size(screen);
- struct size target_size = {
+resize_for_content(struct screen *screen, struct sc_size old_content_size,
+ struct sc_size new_content_size) {
+ struct sc_size window_size = get_window_size(screen);
+ struct sc_size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width
/ old_content_size.width,
.height = (uint32_t) window_size.height * new_content_size.height
@@ -507,7 +578,7 @@ resize_for_content(struct screen *screen, struct size old_content_size,
}
static void
-set_content_size(struct screen *screen, struct size new_content_size) {
+set_content_size(struct screen *screen, struct sc_size new_content_size) {
if (!screen->fullscreen && !screen->maximized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
@@ -538,7 +609,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) {
return;
}
- struct size new_content_size =
+ struct sc_size new_content_size =
get_rotated_size(screen->frame_size, rotation);
set_content_size(screen, new_content_size);
@@ -551,7 +622,7 @@ screen_set_rotation(struct screen *screen, unsigned rotation) {
// recreate the texture and resize the window if the frame size has changed
static bool
-prepare_for_frame(struct screen *screen, struct size new_frame_size) {
+prepare_for_frame(struct screen *screen, struct sc_size new_frame_size) {
if (screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed, destroy texture
@@ -559,7 +630,7 @@ prepare_for_frame(struct screen *screen, struct size new_frame_size) {
screen->frame_size = new_frame_size;
- struct size new_content_size =
+ struct sc_size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size);
@@ -595,12 +666,12 @@ update_texture(struct screen *screen, const AVFrame *frame) {
static bool
screen_update_frame(struct screen *screen) {
av_frame_unref(screen->frame);
- video_buffer_consume(&screen->vb, screen->frame);
+ sc_video_buffer_consume(&screen->vb, screen->frame);
AVFrame *frame = screen->frame;
fps_counter_add_rendered_frame(&screen->fps_counter);
- struct size new_frame_size = {frame->width, frame->height};
+ struct sc_size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) {
return false;
}
@@ -610,40 +681,6 @@ screen_update_frame(struct screen *screen) {
return true;
}
-void
-screen_render(struct screen *screen, bool update_content_rect) {
- if (update_content_rect) {
- screen_update_content_rect(screen);
- }
-
- SDL_RenderClear(screen->renderer);
- if (screen->rotation == 0) {
- SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
- } else {
- // rotation in RenderCopyEx() is clockwise, while screen->rotation is
- // counterclockwise (to be consistent with --lock-video-orientation)
- int cw_rotation = (4 - screen->rotation) % 4;
- double angle = 90 * cw_rotation;
-
- SDL_Rect *dstrect = NULL;
- SDL_Rect rect;
- if (screen->rotation & 1) {
- rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
- rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
- rect.w = screen->rect.h;
- rect.h = screen->rect.w;
- dstrect = ▭
- } else {
- assert(screen->rotation == 2);
- dstrect = &screen->rect;
- }
-
- SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
- angle, NULL, 0);
- }
- SDL_RenderPresent(screen->renderer);
-}
-
void
screen_switch_fullscreen(struct screen *screen) {
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
@@ -667,10 +704,10 @@ screen_resize_to_fit(struct screen *screen) {
return;
}
- struct point point = get_window_position(screen);
- struct size window_size = get_window_size(screen);
+ struct sc_point point = get_window_position(screen);
+ struct sc_size window_size = get_window_size(screen);
- struct size optimal_size =
+ struct sc_size optimal_size =
get_optimal_size(window_size, screen->content_size);
// Center the window related to the device screen
@@ -696,7 +733,7 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
screen->maximized = false;
}
- struct size content_size = screen->content_size;
+ struct sc_size content_size = screen->content_size;
SDL_SetWindowSize(screen->window, content_size.width, content_size.height);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
content_size.height);
@@ -751,7 +788,7 @@ screen_handle_event(struct screen *screen, SDL_Event *event) {
return false;
}
-struct point
+struct sc_point
screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
unsigned rotation = screen->rotation;
@@ -765,7 +802,7 @@ screen_convert_drawable_to_frame_coords(struct screen *screen,
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
// rotate
- struct point result;
+ struct sc_point result;
switch (rotation) {
case 0:
result.x = x;
@@ -788,7 +825,7 @@ screen_convert_drawable_to_frame_coords(struct screen *screen,
return result;
}
-struct point
+struct sc_point
screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y) {
screen_hidpi_scale_coords(screen, &x, &y);
diff --git a/app/src/screen.h b/app/src/screen.h
index e2a43da7..b82bf631 100644
--- a/app/src/screen.h
+++ b/app/src/screen.h
@@ -8,6 +8,7 @@
#include
#include "coords.h"
+#include "fps_counter.h"
#include "opengl.h"
#include "trait/frame_sink.h"
#include "video_buffer.h"
@@ -19,20 +20,20 @@ struct screen {
bool open; // track the open/close state to assert correct behavior
#endif
- struct video_buffer vb;
+ struct sc_video_buffer vb;
struct fps_counter fps_counter;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_opengl gl;
- struct size frame_size;
- struct size content_size; // rotated frame_size
+ struct sc_size frame_size;
+ struct sc_size content_size; // rotated frame_size
bool resize_pending; // resize requested while fullscreen or maximized
// The content size the last time the window was not maximized or
// fullscreen (meaningful only when resize_pending is true)
- struct size windowed_content_size;
+ struct sc_size windowed_content_size;
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise)
unsigned rotation;
@@ -43,12 +44,14 @@ struct screen {
bool maximized;
bool mipmaps;
+ bool event_failed; // in case SDL_PushEvent() returned an error
+
AVFrame *frame;
};
struct screen_params {
const char *window_title;
- struct size frame_size;
+ struct sc_size frame_size;
bool always_on_top;
int16_t window_x;
@@ -62,6 +65,8 @@ struct screen_params {
bool mipmaps;
bool fullscreen;
+
+ sc_tick buffering_time;
};
// initialize screen, create window, renderer and texture (window is hidden)
@@ -88,13 +93,6 @@ screen_destroy(struct screen *screen);
void
screen_hide_window(struct screen *screen);
-// render the texture to the renderer
-//
-// Set the update_content_rect flag if the window or content size may have
-// changed, so that the content rectangle is recomputed
-void
-screen_render(struct screen *screen, bool update_content_rect);
-
// switch the fullscreen mode
void
screen_switch_fullscreen(struct screen *screen);
@@ -117,13 +115,13 @@ screen_handle_event(struct screen *screen, SDL_Event *event);
// convert point from window coordinates to frame coordinates
// x and y are expressed in pixels
-struct point
+struct sc_point
screen_convert_window_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
// convert point from drawable coordinates to frame coordinates
// x and y are expressed in pixels
-struct point
+struct sc_point
screen_convert_drawable_to_frame_coords(struct screen *screen,
int32_t x, int32_t y);
diff --git a/app/src/server.c b/app/src/server.c
index a4cdb0c9..58247a72 100644
--- a/app/src/server.c
+++ b/app/src/server.c
@@ -3,21 +3,21 @@
#include
#include
#include
-#include
#include
#include
#include
#include "adb.h"
+#include "util/file.h"
#include "util/log.h"
-#include "util/net.h"
-#include "util/str_util.h"
+#include "util/net_intr.h"
+#include "util/process_intr.h"
+#include "util/str.h"
-#define SOCKET_NAME "scrcpy"
-#define SERVER_FILENAME "scrcpy-server"
+#define SC_SERVER_FILENAME "scrcpy-server"
-#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
-#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
+#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
+#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
static char *
get_server_path(void) {
@@ -29,7 +29,7 @@ get_server_path(void) {
if (server_path_env) {
// if the envvar is set, use it
#ifdef __WINDOWS__
- char *server_path = utf8_from_wide_char(server_path_env);
+ char *server_path = sc_str_from_wchars(server_path_env);
#else
char *server_path = strdup(server_path_env);
#endif
@@ -42,194 +42,79 @@ get_server_path(void) {
}
#ifndef PORTABLE
- LOGD("Using server: " DEFAULT_SERVER_PATH);
- char *server_path = strdup(DEFAULT_SERVER_PATH);
+ LOGD("Using server: " SC_SERVER_PATH_DEFAULT);
+ char *server_path = strdup(SC_SERVER_PATH_DEFAULT);
if (!server_path) {
LOGE("Could not allocate memory");
return NULL;
}
- // the absolute path is hardcoded
- return server_path;
#else
-
- // use scrcpy-server in the same directory as the executable
- char *executable_path = get_executable_path();
- if (!executable_path) {
- LOGE("Could not get executable path, "
- "using " SERVER_FILENAME " from current directory");
- // not found, use current directory
- return strdup(SERVER_FILENAME);
- }
- char *dir = dirname(executable_path);
- size_t dirlen = strlen(dir);
-
- // sizeof(SERVER_FILENAME) gives statically the size including the null byte
- size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
- char *server_path = malloc(len);
+ char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME);
if (!server_path) {
- LOGE("Could not alloc server path string, "
- "using " SERVER_FILENAME " from current directory");
- free(executable_path);
- return strdup(SERVER_FILENAME);
+ LOGE("Could not get local file path, "
+ "using " SC_SERVER_FILENAME " from current directory");
+ return strdup(SC_SERVER_FILENAME);
}
- memcpy(server_path, dir, dirlen);
- server_path[dirlen] = PATH_SEPARATOR;
- memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME));
- // the final null byte has been copied with SERVER_FILENAME
-
- free(executable_path);
-
LOGD("Using server (portable): %s", server_path);
- return server_path;
#endif
+
+ return server_path;
+}
+
+static void
+sc_server_params_destroy(struct sc_server_params *params) {
+ // The server stores a copy of the params provided by the user
+ free((char *) params->serial);
+ free((char *) params->crop);
+ free((char *) params->codec_options);
+ free((char *) params->encoder_name);
}
static bool
-push_server(const char *serial) {
+sc_server_params_copy(struct sc_server_params *dst,
+ const struct sc_server_params *src) {
+ *dst = *src;
+
+ // The params reference user-allocated memory, so we must copy them to
+ // handle them from another thread
+
+#define COPY(FIELD) \
+ dst->FIELD = NULL; \
+ if (src->FIELD) { \
+ dst->FIELD = strdup(src->FIELD); \
+ if (!dst->FIELD) { \
+ goto error; \
+ } \
+ }
+
+ COPY(serial);
+ COPY(crop);
+ COPY(codec_options);
+ COPY(encoder_name);
+#undef COPY
+
+ return true;
+
+error:
+ sc_server_params_destroy(dst);
+ return false;
+}
+
+static bool
+push_server(struct sc_intr *intr, const char *serial) {
char *server_path = get_server_path();
if (!server_path) {
return false;
}
- if (!is_regular_file(server_path)) {
+ if (!sc_file_is_regular(server_path)) {
LOGE("'%s' does not exist or is not a regular file\n", server_path);
free(server_path);
return false;
}
- process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH);
+ sc_pid pid = adb_push(serial, server_path, SC_DEVICE_SERVER_PATH);
free(server_path);
- return process_check_success(process, "adb push", true);
-}
-
-static bool
-enable_tunnel_reverse(const char *serial, uint16_t local_port) {
- process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
- return process_check_success(process, "adb reverse", true);
-}
-
-static bool
-disable_tunnel_reverse(const char *serial) {
- process_t process = adb_reverse_remove(serial, SOCKET_NAME);
- return process_check_success(process, "adb reverse --remove", true);
-}
-
-static bool
-enable_tunnel_forward(const char *serial, uint16_t local_port) {
- process_t process = adb_forward(serial, local_port, SOCKET_NAME);
- return process_check_success(process, "adb forward", true);
-}
-
-static bool
-disable_tunnel_forward(const char *serial, uint16_t local_port) {
- process_t process = adb_forward_remove(serial, local_port);
- return process_check_success(process, "adb forward --remove", true);
-}
-
-static bool
-disable_tunnel(struct server *server) {
- if (server->tunnel_forward) {
- return disable_tunnel_forward(server->serial, server->local_port);
- }
- return disable_tunnel_reverse(server->serial);
-}
-
-static socket_t
-listen_on_port(uint16_t port) {
-#define IPV4_LOCALHOST 0x7F000001
- return net_listen(IPV4_LOCALHOST, port, 1);
-}
-
-static bool
-enable_tunnel_reverse_any_port(struct server *server,
- struct sc_port_range port_range) {
- uint16_t port = port_range.first;
- for (;;) {
- if (!enable_tunnel_reverse(server->serial, port)) {
- // the command itself failed, it will fail on any port
- return false;
- }
-
- // At the application level, the device part is "the server" because it
- // serves video stream and control. However, at the network level, the
- // client listens and the server connects to the client. That way, the
- // client can listen before starting the server app, so there is no
- // need to try to connect until the server socket is listening on the
- // device.
- server->server_socket = listen_on_port(port);
- if (server->server_socket != INVALID_SOCKET) {
- // success
- server->local_port = port;
- return true;
- }
-
- // failure, disable tunnel and try another port
- if (!disable_tunnel_reverse(server->serial)) {
- LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
- }
-
- // check before incrementing to avoid overflow on port 65535
- if (port < port_range.last) {
- LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
- port, (uint16_t) (port + 1));
- port++;
- continue;
- }
-
- if (port_range.first == port_range.last) {
- LOGE("Could not listen on port %" PRIu16, port_range.first);
- } else {
- LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
- port_range.first, port_range.last);
- }
- return false;
- }
-}
-
-static bool
-enable_tunnel_forward_any_port(struct server *server,
- struct sc_port_range port_range) {
- server->tunnel_forward = true;
- uint16_t port = port_range.first;
- for (;;) {
- if (enable_tunnel_forward(server->serial, port)) {
- // success
- server->local_port = port;
- return true;
- }
-
- if (port < port_range.last) {
- LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
- port, (uint16_t) (port + 1));
- port++;
- continue;
- }
-
- if (port_range.first == port_range.last) {
- LOGE("Could not forward port %" PRIu16, port_range.first);
- } else {
- LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
- port_range.first, port_range.last);
- }
- return false;
- }
-}
-
-static bool
-enable_tunnel_any_port(struct server *server, struct sc_port_range port_range,
- bool force_adb_forward) {
- if (!force_adb_forward) {
- // Attempt to use "adb reverse"
- if (enable_tunnel_reverse_any_port(server, port_range)) {
- return true;
- }
-
- // if "adb reverse" does not work (e.g. over "adb connect"), it
- // fallbacks to "adb forward", so the app socket is the client
-
- LOGW("'adb reverse' failed, fallback to 'adb forward'");
- }
-
- return enable_tunnel_forward_any_port(server, port_range);
+ return sc_process_check_success_intr(intr, pid, "adb push");
}
static const char *
@@ -251,8 +136,11 @@ log_level_to_server_string(enum sc_log_level level) {
}
}
-static process_t
-execute_server(struct server *server, const struct server_params *params) {
+static sc_pid
+execute_server(struct sc_server *server,
+ const struct sc_server_params *params) {
+ const char *serial = server->params.serial;
+
char max_size_string[6];
char bit_rate_string[11];
char max_fps_string[6];
@@ -261,17 +149,19 @@ execute_server(struct server *server, const struct server_params *params) {
sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
- sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
+ sprintf(lock_video_orientation_string, "%"PRIi8,
+ params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu32, params->display_id);
const char *const cmd[] = {
"shell",
- "CLASSPATH=" DEVICE_SERVER_PATH,
+ "CLASSPATH=" SC_DEVICE_SERVER_PATH,
"app_process",
#ifdef SERVER_DEBUGGER
# define SERVER_DEBUGGER_PORT "5005"
# ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
- "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,server=y,address="
+ "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
+ "server=y,address="
# else
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
@@ -286,7 +176,7 @@ execute_server(struct server *server, const struct server_params *params) {
bit_rate_string,
max_fps_string,
lock_video_orientation_string,
- server->tunnel_forward ? "true" : "false",
+ server->tunnel.forward ? "true" : "false",
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->control ? "true" : "false",
@@ -308,276 +198,334 @@ execute_server(struct server *server, const struct server_params *params) {
// Port: 5005
// Then click on "Debug"
#endif
- return adb_execute(server->serial, cmd, ARRAY_LEN(cmd));
+ return adb_execute(serial, cmd, ARRAY_LEN(cmd));
}
-static socket_t
-connect_and_read_byte(uint16_t port) {
- socket_t socket = net_connect(IPV4_LOCALHOST, port);
- if (socket == INVALID_SOCKET) {
- return INVALID_SOCKET;
+static bool
+connect_and_read_byte(struct sc_intr *intr, sc_socket socket, uint16_t port) {
+ bool ok = net_connect_intr(intr, socket, IPV4_LOCALHOST, port);
+ if (!ok) {
+ return false;
}
char byte;
// the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection
- if (net_recv(socket, &byte, 1) != 1) {
+ if (net_recv_intr(intr, socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
- net_close(socket);
- return INVALID_SOCKET;
+ return false;
}
- return socket;
+
+ return true;
}
-static socket_t
-connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) {
+static sc_socket
+connect_to_server(struct sc_server *server, uint32_t attempts, sc_tick delay) {
+ uint16_t port = server->tunnel.local_port;
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
- socket_t socket = connect_and_read_byte(port);
- if (socket != INVALID_SOCKET) {
- // it worked!
- return socket;
+ sc_socket socket = net_socket();
+ if (socket != SC_SOCKET_NONE) {
+ bool ok = connect_and_read_byte(&server->intr, socket, port);
+ if (ok) {
+ // it worked!
+ return socket;
+ }
+
+ net_close(socket);
}
if (attempts) {
- SDL_Delay(delay);
+ sc_mutex_lock(&server->mutex);
+ sc_tick deadline = sc_tick_now() + delay;
+ bool timed_out = false;
+ while (!server->stopped && !timed_out) {
+ timed_out = !sc_cond_timedwait(&server->cond_stopped,
+ &server->mutex, deadline);
+ }
+ bool stopped = server->stopped;
+ sc_mutex_unlock(&server->mutex);
+
+ if (stopped) {
+ LOGI("Connection attempt stopped");
+ break;
+ }
}
} while (--attempts > 0);
- return INVALID_SOCKET;
-}
-
-static void
-close_socket(socket_t socket) {
- assert(socket != INVALID_SOCKET);
- net_shutdown(socket, SHUT_RDWR);
- if (!net_close(socket)) {
- LOGW("Could not close socket");
- }
+ return SC_SOCKET_NONE;
}
bool
-server_init(struct server *server) {
- server->serial = NULL;
- server->process = PROCESS_NONE;
- atomic_flag_clear_explicit(&server->server_socket_closed,
- memory_order_relaxed);
-
- bool ok = sc_mutex_init(&server->mutex);
+sc_server_init(struct sc_server *server, const struct sc_server_params *params,
+ const struct sc_server_callbacks *cbs, void *cbs_userdata) {
+ bool ok = sc_server_params_copy(&server->params, params);
if (!ok) {
+ LOGE("Could not copy server params");
return false;
}
- ok = sc_cond_init(&server->process_terminated_cond);
+ ok = sc_mutex_init(&server->mutex);
if (!ok) {
+ LOGE("Could not create server mutex");
+ sc_server_params_destroy(&server->params);
+ return false;
+ }
+
+ ok = sc_cond_init(&server->cond_stopped);
+ if (!ok) {
+ LOGE("Could not create server cond_stopped");
sc_mutex_destroy(&server->mutex);
+ sc_server_params_destroy(&server->params);
return false;
}
- server->process_terminated = false;
-
- server->server_socket = INVALID_SOCKET;
- server->video_socket = INVALID_SOCKET;
- server->control_socket = INVALID_SOCKET;
-
- server->local_port = 0;
-
- server->tunnel_enabled = false;
- server->tunnel_forward = false;
-
- return true;
-}
-
-static int
-run_wait_server(void *data) {
- struct server *server = data;
- process_wait(server->process, false); // ignore exit code
-
- sc_mutex_lock(&server->mutex);
- server->process_terminated = true;
- sc_cond_signal(&server->process_terminated_cond);
- sc_mutex_unlock(&server->mutex);
-
- // no need for synchronization, server_socket is initialized before this
- // thread was created
- if (server->server_socket != INVALID_SOCKET
- && !atomic_flag_test_and_set(&server->server_socket_closed)) {
- // On Linux, accept() is unblocked by shutdown(), but on Windows, it is
- // unblocked by closesocket(). Therefore, call both (close_socket()).
- close_socket(server->server_socket);
- }
- LOGD("Server terminated");
- return 0;
-}
-
-bool
-server_start(struct server *server, const struct server_params *params) {
- if (params->serial) {
- server->serial = strdup(params->serial);
- if (!server->serial) {
- return false;
- }
- }
-
- if (!push_server(params->serial)) {
- /* server->serial will be freed on server_destroy() */
- return false;
- }
-
- if (!enable_tunnel_any_port(server, params->port_range,
- params->force_adb_forward)) {
- return false;
- }
-
- // server will connect to our server socket
- server->process = execute_server(server, params);
- if (server->process == PROCESS_NONE) {
- goto error;
- }
-
- // If the server process dies before connecting to the server socket, then
- // the client will be stuck forever on accept(). To avoid the problem, we
- // must be able to wake up the accept() call when the server dies. To keep
- // things simple and multiplatform, just spawn a new thread waiting for the
- // server process and calling shutdown()/close() on the server socket if
- // necessary to wake up any accept() blocking call.
- bool ok = sc_thread_create(&server->wait_server_thread, run_wait_server,
- "wait-server", server);
+ ok = sc_intr_init(&server->intr);
if (!ok) {
- process_terminate(server->process);
- process_wait(server->process, true); // ignore exit code
- goto error;
+ LOGE("Could not create intr");
+ sc_cond_destroy(&server->cond_stopped);
+ sc_mutex_destroy(&server->mutex);
+ sc_server_params_destroy(&server->params);
+ return false;
}
- server->tunnel_enabled = true;
+ server->stopped = false;
+
+ server->video_socket = SC_SOCKET_NONE;
+ server->control_socket = SC_SOCKET_NONE;
+
+ sc_adb_tunnel_init(&server->tunnel);
+
+ assert(cbs);
+ assert(cbs->on_connection_failed);
+ assert(cbs->on_connected);
+ assert(cbs->on_disconnected);
+
+ server->cbs = cbs;
+ server->cbs_userdata = cbs_userdata;
return true;
-
-error:
- if (!server->tunnel_forward) {
- bool was_closed =
- atomic_flag_test_and_set(&server->server_socket_closed);
- // the thread is not started, the flag could not be already set
- assert(!was_closed);
- (void) was_closed;
- close_socket(server->server_socket);
- }
- disable_tunnel(server);
-
- return false;
}
static bool
-device_read_info(socket_t device_socket, char *device_name, struct size *size) {
- unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4];
- int r = net_recv_all(device_socket, buf, sizeof(buf));
- if (r < DEVICE_NAME_FIELD_LENGTH + 4) {
+device_read_info(struct sc_intr *intr, sc_socket device_socket,
+ struct sc_server_info *info) {
+ unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH + 4];
+ ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
+ if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) {
LOGE("Could not retrieve device information");
return false;
}
// in case the client sends garbage
- buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
- // strcpy is safe here, since name contains at least
- // DEVICE_NAME_FIELD_LENGTH bytes and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
- strcpy(device_name, (char *) buf);
- size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8)
- | buf[DEVICE_NAME_FIELD_LENGTH + 1];
- size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8)
- | buf[DEVICE_NAME_FIELD_LENGTH + 3];
+ buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
+ memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
+
+ 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;
}
-bool
-server_connect_to(struct server *server, char *device_name, struct size *size) {
- if (!server->tunnel_forward) {
- server->video_socket = net_accept(server->server_socket);
- if (server->video_socket == INVALID_SOCKET) {
- return false;
+static bool
+sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
+ struct sc_adb_tunnel *tunnel = &server->tunnel;
+
+ assert(tunnel->enabled);
+
+ const char *serial = server->params.serial;
+
+ sc_socket video_socket = SC_SOCKET_NONE;
+ sc_socket control_socket = SC_SOCKET_NONE;
+ if (!tunnel->forward) {
+ video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
+ if (video_socket == SC_SOCKET_NONE) {
+ goto fail;
}
- server->control_socket = net_accept(server->server_socket);
- if (server->control_socket == INVALID_SOCKET) {
- // the video_socket will be cleaned up on destroy
- return false;
- }
-
- // we don't need the server socket anymore
- if (!atomic_flag_test_and_set(&server->server_socket_closed)) {
- // close it from here
- close_socket(server->server_socket);
- // otherwise, it is closed by run_wait_server()
+ control_socket = net_accept_intr(&server->intr, tunnel->server_socket);
+ if (control_socket == SC_SOCKET_NONE) {
+ goto fail;
}
} else {
uint32_t attempts = 100;
- uint32_t delay = 100; // ms
- server->video_socket =
- connect_to_server(server->local_port, attempts, delay);
- if (server->video_socket == INVALID_SOCKET) {
- return false;
+ sc_tick delay = SC_TICK_FROM_MS(100);
+ video_socket = connect_to_server(server, attempts, delay);
+ if (video_socket == SC_SOCKET_NONE) {
+ goto fail;
}
// we know that the device is listening, we don't need several attempts
- server->control_socket =
- net_connect(IPV4_LOCALHOST, server->local_port);
- if (server->control_socket == INVALID_SOCKET) {
- return false;
+ control_socket = net_socket();
+ if (control_socket == SC_SOCKET_NONE) {
+ goto fail;
+ }
+ bool ok = net_connect_intr(&server->intr, control_socket,
+ IPV4_LOCALHOST, tunnel->local_port);
+ if (!ok) {
+ goto fail;
}
}
// we don't need the adb tunnel anymore
- disable_tunnel(server); // ignore failure
- server->tunnel_enabled = false;
+ sc_adb_tunnel_close(tunnel, &server->intr, serial);
// The sockets will be closed on stop if device_read_info() fails
- return device_read_info(server->video_socket, device_name, size);
+ bool ok = device_read_info(&server->intr, video_socket, info);
+ if (!ok) {
+ goto fail;
+ }
+
+ assert(video_socket != SC_SOCKET_NONE);
+ assert(control_socket != SC_SOCKET_NONE);
+
+ server->video_socket = video_socket;
+ server->control_socket = control_socket;
+
+ return true;
+
+fail:
+ if (video_socket != SC_SOCKET_NONE) {
+ if (!net_close(video_socket)) {
+ LOGW("Could not close video socket");
+ }
+ }
+
+ if (control_socket != SC_SOCKET_NONE) {
+ if (!net_close(control_socket)) {
+ LOGW("Could not close control socket");
+ }
+ }
+
+ // Always leave this function with tunnel disabled
+ sc_adb_tunnel_close(tunnel, &server->intr, serial);
+
+ return false;
}
-void
-server_stop(struct server *server) {
- if (server->server_socket != INVALID_SOCKET
- && !atomic_flag_test_and_set(&server->server_socket_closed)) {
- close_socket(server->server_socket);
- }
- if (server->video_socket != INVALID_SOCKET) {
- close_socket(server->video_socket);
- }
- if (server->control_socket != INVALID_SOCKET) {
- close_socket(server->control_socket);
+static void
+sc_server_on_terminated(void *userdata) {
+ struct sc_server *server = userdata;
+
+ // If the server process dies before connecting to the server socket,
+ // then the client will be stuck forever on accept(). To avoid the problem,
+ // wake up the accept() call (or any other) when the server dies, like on
+ // stop() (it is safe to call interrupt() twice).
+ sc_intr_interrupt(&server->intr);
+
+ server->cbs->on_disconnected(server, server->cbs_userdata);
+
+ LOGD("Server terminated");
+}
+
+static int
+run_server(void *data) {
+ struct sc_server *server = data;
+
+ const struct sc_server_params *params = &server->params;
+
+ bool ok = push_server(&server->intr, params->serial);
+ if (!ok) {
+ goto error_connection_failed;
}
- assert(server->process != PROCESS_NONE);
-
- if (server->tunnel_enabled) {
- // ignore failure
- disable_tunnel(server);
+ ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, params->serial,
+ params->port_range, params->force_adb_forward);
+ if (!ok) {
+ goto error_connection_failed;
}
- // Give some delay for the server to terminate properly
+ // 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, params->serial);
+ goto error_connection_failed;
+ }
+
+ static const struct sc_process_listener listener = {
+ .on_terminated = sc_server_on_terminated,
+ };
+ struct sc_process_observer observer;
+ ok = sc_process_observer_init(&observer, pid, &listener, server);
+ if (!ok) {
+ sc_process_terminate(pid);
+ sc_process_wait(pid, true); // ignore exit code
+ sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial);
+ goto error_connection_failed;
+ }
+
+ ok = sc_server_connect_to(server, &server->info);
+ // The tunnel is always closed by server_connect_to()
+ if (!ok) {
+ sc_process_terminate(pid);
+ sc_process_wait(pid, true); // ignore exit code
+ sc_process_observer_join(&observer);
+ sc_process_observer_destroy(&observer);
+ goto error_connection_failed;
+ }
+
+ // Now connected
+ server->cbs->on_connected(server, server->cbs_userdata);
+
+ // Wait for server_stop()
sc_mutex_lock(&server->mutex);
- bool signaled = false;
- if (!server->process_terminated) {
-#define WATCHDOG_DELAY_MS 1000
- signaled = sc_cond_timedwait(&server->process_terminated_cond,
- &server->mutex,
- WATCHDOG_DELAY_MS);
+ while (!server->stopped) {
+ sc_cond_wait(&server->cond_stopped, &server->mutex);
}
sc_mutex_unlock(&server->mutex);
+ // Give some delay for the server to terminate properly
+#define WATCHDOG_DELAY SC_TICK_FROM_SEC(1)
+ sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY;
+ bool terminated = sc_process_observer_timedwait(&observer, deadline);
+
// After this delay, kill the server if it's not dead already.
// On some devices, closing the sockets is not sufficient to wake up the
// blocking calls while the device is asleep.
- if (!signaled) {
- // The process is terminated, but not reaped (closed) yet, so its PID
- // is still valid.
+ if (!terminated) {
+ // The process may have terminated since the check, but it is not
+ // reaped (closed) yet, so its PID is still valid, and it is ok to call
+ // sc_process_terminate() even in that case.
LOGW("Killing the server...");
- process_terminate(server->process);
+ sc_process_terminate(pid);
}
- sc_thread_join(&server->wait_server_thread, NULL);
- process_close(server->process);
+ sc_process_observer_join(&observer);
+ sc_process_observer_destroy(&observer);
+
+ sc_process_close(pid);
+
+ return 0;
+
+error_connection_failed:
+ server->cbs->on_connection_failed(server, server->cbs_userdata);
+ return -1;
+}
+
+bool
+sc_server_start(struct sc_server *server) {
+ bool ok = sc_thread_create(&server->thread, run_server, "server", server);
+ if (!ok) {
+ LOGE("Could not create server thread");
+ return false;
+ }
+
+ return true;
}
void
-server_destroy(struct server *server) {
- free(server->serial);
- sc_cond_destroy(&server->process_terminated_cond);
+sc_server_stop(struct sc_server *server) {
+ sc_mutex_lock(&server->mutex);
+ server->stopped = true;
+ sc_cond_signal(&server->cond_stopped);
+ sc_intr_interrupt(&server->intr);
+ sc_mutex_unlock(&server->mutex);
+
+ sc_thread_join(&server->thread, NULL);
+}
+
+void
+sc_server_destroy(struct sc_server *server) {
+ sc_server_params_destroy(&server->params);
+ sc_intr_destroy(&server->intr);
+ sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
}
diff --git a/app/src/server.h b/app/src/server.h
index c249b374..3859c328 100644
--- a/app/src/server.h
+++ b/app/src/server.h
@@ -8,31 +8,21 @@
#include
#include "adb.h"
+#include "adb_tunnel.h"
#include "coords.h"
-#include "scrcpy.h"
+#include "options.h"
+#include "util/intr.h"
#include "util/log.h"
#include "util/net.h"
#include "util/thread.h"
-struct server {
- char *serial;
- process_t process;
- sc_thread wait_server_thread;
- atomic_flag server_socket_closed;
-
- sc_mutex mutex;
- sc_cond process_terminated_cond;
- bool process_terminated;
-
- socket_t server_socket; // only used if !tunnel_forward
- socket_t video_socket;
- socket_t control_socket;
- uint16_t local_port; // selected from port_range
- bool tunnel_enabled;
- bool tunnel_forward; // use "adb forward" instead of "adb reverse"
+#define SC_DEVICE_NAME_FIELD_LENGTH 64
+struct sc_server_info {
+ char device_name[SC_DEVICE_NAME_FIELD_LENGTH];
+ struct sc_size frame_size;
};
-struct server_params {
+struct sc_server_params {
const char *serial;
enum sc_log_level log_level;
const char *crop;
@@ -51,26 +41,62 @@ struct server_params {
bool power_off_on_close;
};
-// init default values
-bool
-server_init(struct server *server);
+struct sc_server {
+ // The internal allocated strings are copies owned by the server
+ struct sc_server_params params;
-// push, enable tunnel et start the server
-bool
-server_start(struct server *server, const struct server_params *params);
+ sc_thread thread;
+ struct sc_server_info info; // initialized once connected
-#define DEVICE_NAME_FIELD_LENGTH 64
-// block until the communication with the server is established
-// device_name must point to a buffer of at least DEVICE_NAME_FIELD_LENGTH bytes
+ sc_mutex mutex;
+ sc_cond cond_stopped;
+ bool stopped;
+
+ struct sc_intr intr;
+ struct sc_adb_tunnel tunnel;
+
+ sc_socket video_socket;
+ sc_socket control_socket;
+
+ const struct sc_server_callbacks *cbs;
+ void *cbs_userdata;
+};
+
+struct sc_server_callbacks {
+ /**
+ * Called when the server failed to connect
+ *
+ * If it is called, then on_connected() and on_disconnected() will never be
+ * called.
+ */
+ void (*on_connection_failed)(struct sc_server *server, void *userdata);
+
+ /**
+ * Called on server connection
+ */
+ void (*on_connected)(struct sc_server *server, void *userdata);
+
+ /**
+ * Called on server disconnection (after it has been connected)
+ */
+ void (*on_disconnected)(struct sc_server *server, void *userdata);
+};
+
+// init the server with the given params
bool
-server_connect_to(struct server *server, char *device_name, struct size *size);
+sc_server_init(struct sc_server *server, const struct sc_server_params *params,
+ const struct sc_server_callbacks *cbs, void *cbs_userdata);
+
+// start the server asynchronously
+bool
+sc_server_start(struct sc_server *server);
// disconnect and kill the server process
void
-server_stop(struct server *server);
+sc_server_stop(struct sc_server *server);
// close and release sockets
void
-server_destroy(struct server *server);
+sc_server_destroy(struct sc_server *server);
#endif
diff --git a/app/src/stream.c b/app/src/stream.c
index d1b8b9f3..4c770250 100644
--- a/app/src/stream.c
+++ b/app/src/stream.c
@@ -151,7 +151,6 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
if (stream->pending) {
// the pending packet must be discarded (consumed or error)
- av_packet_unref(stream->pending);
av_packet_free(&stream->pending);
}
@@ -244,7 +243,6 @@ run_stream(void *data) {
LOGD("End of frames");
if (stream->pending) {
- av_packet_unref(stream->pending);
av_packet_free(&stream->pending);
}
@@ -262,7 +260,7 @@ end:
}
void
-stream_init(struct stream *stream, socket_t socket,
+stream_init(struct stream *stream, sc_socket socket,
const struct stream_callbacks *cbs, void *cbs_userdata) {
stream->socket = socket;
stream->pending = NULL;
diff --git a/app/src/stream.h b/app/src/stream.h
index d7047c95..362bc4a7 100644
--- a/app/src/stream.h
+++ b/app/src/stream.h
@@ -14,7 +14,7 @@
#define STREAM_MAX_SINKS 2
struct stream {
- socket_t socket;
+ sc_socket socket;
sc_thread thread;
struct sc_packet_sink *sinks[STREAM_MAX_SINKS];
@@ -35,7 +35,7 @@ struct stream_callbacks {
};
void
-stream_init(struct stream *stream, socket_t socket,
+stream_init(struct stream *stream, sc_socket socket,
const struct stream_callbacks *cbs, void *cbs_userdata);
void
diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c
new file mode 100644
index 00000000..4e9e45b3
--- /dev/null
+++ b/app/src/sys/unix/file.c
@@ -0,0 +1,75 @@
+#include "util/file.h"
+
+#include
+#include
+#include
+#include
+#include
+
+bool
+sc_file_executable_exists(const char *file) {
+ char *path = getenv("PATH");
+ if (!path)
+ return false;
+ path = strdup(path);
+ if (!path)
+ return false;
+
+ bool ret = false;
+ size_t file_len = strlen(file);
+ char *saveptr;
+ for (char *dir = strtok_r(path, ":", &saveptr); dir;
+ dir = strtok_r(NULL, ":", &saveptr)) {
+ size_t dir_len = strlen(dir);
+ char *fullpath = malloc(dir_len + file_len + 2);
+ if (!fullpath)
+ continue;
+ memcpy(fullpath, dir, dir_len);
+ fullpath[dir_len] = '/';
+ memcpy(fullpath + dir_len + 1, file, file_len + 1);
+
+ struct stat sb;
+ bool fullpath_executable = stat(fullpath, &sb) == 0 &&
+ sb.st_mode & S_IXUSR;
+ free(fullpath);
+ if (fullpath_executable) {
+ ret = true;
+ break;
+ }
+ }
+
+ free(path);
+ return ret;
+}
+
+char *
+sc_file_get_executable_path(void) {
+//
+#ifdef __linux__
+ char buf[PATH_MAX + 1]; // +1 for the null byte
+ ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX);
+ if (len == -1) {
+ perror("readlink");
+ return NULL;
+ }
+ buf[len] = '\0';
+ return strdup(buf);
+#else
+ // in practice, we only need this feature for portable builds, only used on
+ // Windows, so we don't care implementing it for every platform
+ // (it's useful to have a working version on Linux for debugging though)
+ return NULL;
+#endif
+}
+
+bool
+sc_file_is_regular(const char *path) {
+ struct stat path_stat;
+
+ if (stat(path, &path_stat)) {
+ perror("stat");
+ return false;
+ }
+ return S_ISREG(path_stat.st_mode);
+}
+
diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c
index 8683a2da..5f4a9890 100644
--- a/app/src/sys/unix/process.c
+++ b/app/src/sys/unix/process.c
@@ -1,118 +1,160 @@
#include "util/process.h"
+#include
#include
#include
-#include
#include
-#include
-#include
-#include
#include
#include
#include
#include "util/log.h"
-bool
-search_executable(const char *file) {
- char *path = getenv("PATH");
- if (!path)
- return false;
- path = strdup(path);
- if (!path)
- return false;
+enum sc_process_result
+sc_process_execute_p(const char *const argv[], sc_pid *pid,
+ int *pin, int *pout, int *perr) {
+ int in[2];
+ int out[2];
+ int err[2];
+ int internal[2]; // communication between parent and children
- bool ret = false;
- size_t file_len = strlen(file);
- char *saveptr;
- for (char *dir = strtok_r(path, ":", &saveptr); dir;
- dir = strtok_r(NULL, ":", &saveptr)) {
- size_t dir_len = strlen(dir);
- char *fullpath = malloc(dir_len + file_len + 2);
- if (!fullpath)
- continue;
- memcpy(fullpath, dir, dir_len);
- fullpath[dir_len] = '/';
- memcpy(fullpath + dir_len + 1, file, file_len + 1);
-
- struct stat sb;
- bool fullpath_executable = stat(fullpath, &sb) == 0 &&
- sb.st_mode & S_IXUSR;
- free(fullpath);
- if (fullpath_executable) {
- ret = true;
- break;
+ if (pipe(internal) == -1) {
+ perror("pipe");
+ return SC_PROCESS_ERROR_GENERIC;
+ }
+ if (pin) {
+ if (pipe(in) == -1) {
+ perror("pipe");
+ close(internal[0]);
+ close(internal[1]);
+ return SC_PROCESS_ERROR_GENERIC;
}
}
-
- free(path);
- return ret;
-}
-
-enum process_result
-process_execute(const char *const argv[], pid_t *pid) {
- int fd[2];
-
- if (pipe(fd) == -1) {
- perror("pipe");
- return PROCESS_ERROR_GENERIC;
+ if (pout) {
+ if (pipe(out) == -1) {
+ perror("pipe");
+ // clean up
+ if (pin) {
+ close(in[0]);
+ close(in[1]);
+ }
+ close(internal[0]);
+ close(internal[1]);
+ return SC_PROCESS_ERROR_GENERIC;
+ }
+ }
+ if (perr) {
+ if (pipe(err) == -1) {
+ perror("pipe");
+ // clean up
+ if (pout) {
+ close(out[0]);
+ close(out[1]);
+ }
+ if (pin) {
+ close(in[0]);
+ close(in[1]);
+ }
+ close(internal[0]);
+ close(internal[1]);
+ return SC_PROCESS_ERROR_GENERIC;
+ }
}
-
- enum process_result ret = PROCESS_SUCCESS;
*pid = fork();
if (*pid == -1) {
perror("fork");
- ret = PROCESS_ERROR_GENERIC;
- goto end;
+ // clean up
+ if (perr) {
+ close(err[0]);
+ close(err[1]);
+ }
+ if (pout) {
+ close(out[0]);
+ close(out[1]);
+ }
+ if (pin) {
+ close(in[0]);
+ close(in[1]);
+ }
+ close(internal[0]);
+ close(internal[1]);
+ return SC_PROCESS_ERROR_GENERIC;
}
- if (*pid > 0) {
- // parent close write side
- close(fd[1]);
- fd[1] = -1;
- // wait for EOF or receive errno from child
- if (read(fd[0], &ret, sizeof(ret)) == -1) {
- perror("read");
- ret = PROCESS_ERROR_GENERIC;
- goto end;
- }
- } else if (*pid == 0) {
- // child close read side
- close(fd[0]);
- if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) {
- execvp(argv[0], (char *const *)argv);
- if (errno == ENOENT) {
- ret = PROCESS_ERROR_MISSING_BINARY;
- } else {
- ret = PROCESS_ERROR_GENERIC;
+ if (*pid == 0) {
+ if (pin) {
+ if (in[0] != STDIN_FILENO) {
+ dup2(in[0], STDIN_FILENO);
+ close(in[0]);
}
+ close(in[1]);
+ }
+ if (pout) {
+ if (out[1] != STDOUT_FILENO) {
+ dup2(out[1], STDOUT_FILENO);
+ close(out[1]);
+ }
+ close(out[0]);
+ }
+ if (perr) {
+ if (err[1] != STDERR_FILENO) {
+ dup2(err[1], STDERR_FILENO);
+ close(err[1]);
+ }
+ close(err[0]);
+ }
+ close(internal[0]);
+ enum sc_process_result err;
+ if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) {
+ execvp(argv[0], (char *const *) argv);
perror("exec");
+ err = errno == ENOENT ? SC_PROCESS_ERROR_MISSING_BINARY
+ : SC_PROCESS_ERROR_GENERIC;
} else {
perror("fcntl");
- ret = PROCESS_ERROR_GENERIC;
+ err = SC_PROCESS_ERROR_GENERIC;
}
- // send ret to the parent
- if (write(fd[1], &ret, sizeof(ret)) == -1) {
+ // send err to the parent
+ if (write(internal[1], &err, sizeof(err)) == -1) {
perror("write");
}
- // close write side before exiting
- close(fd[1]);
+ close(internal[1]);
_exit(1);
}
-end:
- if (fd[0] != -1) {
- close(fd[0]);
+ // parent
+ assert(*pid > 0);
+
+ close(internal[1]);
+
+ enum sc_process_result res = SC_PROCESS_SUCCESS;
+ // wait for EOF or receive err from child
+ if (read(internal[0], &res, sizeof(res)) == -1) {
+ perror("read");
+ res = SC_PROCESS_ERROR_GENERIC;
}
- if (fd[1] != -1) {
- close(fd[1]);
+
+ close(internal[0]);
+
+ if (pin) {
+ close(in[0]);
+ *pin = in[1];
}
- return ret;
+ if (pout) {
+ *pout = out[0];
+ close(out[1]);
+ }
+ if (perr) {
+ *perr = err[0];
+ close(err[1]);
+ }
+
+ return res;
}
bool
-process_terminate(pid_t pid) {
+sc_process_terminate(pid_t pid) {
if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n",
(int) pid);
@@ -121,8 +163,8 @@ process_terminate(pid_t pid) {
return kill(pid, SIGKILL) != -1;
}
-exit_code_t
-process_wait(pid_t pid, bool close) {
+sc_exit_code
+sc_process_wait(pid_t pid, bool close) {
int code;
int options = WEXITED;
if (!close) {
@@ -133,7 +175,7 @@ process_wait(pid_t pid, bool close) {
int r = waitid(P_PID, pid, &info, options);
if (r == -1 || info.si_code != CLD_EXITED) {
// could not wait, or exited unexpectedly, probably by a signal
- code = NO_EXIT_CODE;
+ code = SC_EXIT_CODE_NONE;
} else {
code = info.si_status;
}
@@ -141,37 +183,18 @@ process_wait(pid_t pid, bool close) {
}
void
-process_close(pid_t pid) {
- process_wait(pid, true); // ignore exit code
+sc_process_close(pid_t pid) {
+ sc_process_wait(pid, true); // ignore exit code
}
-char *
-get_executable_path(void) {
-//
-#ifdef __linux__
- char buf[PATH_MAX + 1]; // +1 for the null byte
- ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX);
- if (len == -1) {
- perror("readlink");
- return NULL;
+ssize_t
+sc_pipe_read(int pipe, char *data, size_t len) {
+ return read(pipe, data, len);
+}
+
+void
+sc_pipe_close(int pipe) {
+ if (close(pipe)) {
+ perror("close pipe");
}
- buf[len] = '\0';
- return strdup(buf);
-#else
- // in practice, we only need this feature for portable builds, only used on
- // Windows, so we don't care implementing it for every platform
- // (it's useful to have a working version on Linux for debugging though)
- return NULL;
-#endif
-}
-
-bool
-is_regular_file(const char *path) {
- struct stat path_stat;
-
- if (stat(path, &path_stat)) {
- perror("stat");
- return false;
- }
- return S_ISREG(path_stat.st_mode);
}
diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c
new file mode 100644
index 00000000..5233b177
--- /dev/null
+++ b/app/src/sys/win/file.c
@@ -0,0 +1,43 @@
+#include "util/file.h"
+
+#include
+
+#include
+
+#include "util/log.h"
+#include "util/str.h"
+
+char *
+sc_file_get_executable_path(void) {
+ HMODULE hModule = GetModuleHandleW(NULL);
+ if (!hModule) {
+ return NULL;
+ }
+ WCHAR buf[MAX_PATH + 1]; // +1 for the null byte
+ int len = GetModuleFileNameW(hModule, buf, MAX_PATH);
+ if (!len) {
+ return NULL;
+ }
+ buf[len] = '\0';
+ return sc_str_from_wchars(buf);
+}
+
+bool
+sc_file_is_regular(const char *path) {
+ wchar_t *wide_path = sc_str_to_wchars(path);
+ if (!wide_path) {
+ LOGC("Could not allocate wide char string");
+ return false;
+ }
+
+ struct _stat path_stat;
+ int r = _wstat(wide_path, &path_stat);
+ free(wide_path);
+
+ if (r) {
+ perror("stat");
+ return false;
+ }
+ return S_ISREG(path_stat.st_mode);
+}
+
diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c
index aafd5d34..4dcd542e 100644
--- a/app/src/sys/win/process.c
+++ b/app/src/sys/win/process.c
@@ -1,10 +1,9 @@
#include "util/process.h"
#include
-#include
#include "util/log.h"
-#include "util/str_util.h"
+#include "util/str.h"
#define CMD_MAX_LEN 8192
@@ -14,61 +13,146 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
//
// only make it work for this very specific program
// (don't handle escaping nor quotes)
- size_t ret = xstrjoin(cmd, argv, ' ', len);
+ size_t ret = sc_str_join(cmd, argv, ' ', len);
if (ret >= len) {
- LOGE("Command too long (%" PRIsizet " chars)", len - 1);
+ LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1);
return false;
}
return true;
}
-enum process_result
-process_execute(const char *const argv[], HANDLE *handle) {
+enum sc_process_result
+sc_process_execute_p(const char *const argv[], HANDLE *handle,
+ HANDLE *pin, HANDLE *pout, HANDLE *perr) {
+ enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC;
+
+ SECURITY_ATTRIBUTES sa;
+ sa.nLength = sizeof(SECURITY_ATTRIBUTES);
+ sa.lpSecurityDescriptor = NULL;
+ sa.bInheritHandle = TRUE;
+
+ HANDLE stdin_read_handle;
+ HANDLE stdout_write_handle;
+ HANDLE stderr_write_handle;
+ if (pin) {
+ if (!CreatePipe(&stdin_read_handle, pin, &sa, 0)) {
+ perror("pipe");
+ return SC_PROCESS_ERROR_GENERIC;
+ }
+ if (!SetHandleInformation(*pin, HANDLE_FLAG_INHERIT, 0)) {
+ LOGE("SetHandleInformation stdin failed");
+ goto error_close_stdin;
+ }
+ }
+ if (pout) {
+ if (!CreatePipe(pout, &stdout_write_handle, &sa, 0)) {
+ perror("pipe");
+ goto error_close_stdin;
+ }
+ if (!SetHandleInformation(*pout, HANDLE_FLAG_INHERIT, 0)) {
+ LOGE("SetHandleInformation stdout failed");
+ goto error_close_stdout;
+ }
+ }
+ if (perr) {
+ if (!CreatePipe(perr, &stderr_write_handle, &sa, 0)) {
+ perror("pipe");
+ goto error_close_stdout;
+ }
+ if (!SetHandleInformation(*perr, HANDLE_FLAG_INHERIT, 0)) {
+ LOGE("SetHandleInformation stderr failed");
+ goto error_close_stderr;
+ }
+ }
+
STARTUPINFOW si;
PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
+ if (pin || pout || perr) {
+ si.dwFlags = STARTF_USESTDHANDLES;
+ if (pin) {
+ si.hStdInput = stdin_read_handle;
+ }
+ if (pout) {
+ si.hStdOutput = stdout_write_handle;
+ }
+ if (perr) {
+ si.hStdError = stderr_write_handle;
+ }
+ }
char *cmd = malloc(CMD_MAX_LEN);
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) {
*handle = NULL;
- return PROCESS_ERROR_GENERIC;
+ goto error_close_stderr;
}
- wchar_t *wide = utf8_to_wide_char(cmd);
+ wchar_t *wide = sc_str_to_wchars(cmd);
free(cmd);
if (!wide) {
LOGC("Could not allocate wide char string");
- return PROCESS_ERROR_GENERIC;
+ goto error_close_stderr;
}
- if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si,
+ if (!CreateProcessW(NULL, wide, NULL, NULL, TRUE, 0, NULL, NULL, &si,
&pi)) {
free(wide);
*handle = NULL;
+
if (GetLastError() == ERROR_FILE_NOT_FOUND) {
- return PROCESS_ERROR_MISSING_BINARY;
+ ret = SC_PROCESS_ERROR_MISSING_BINARY;
}
- return PROCESS_ERROR_GENERIC;
+ goto error_close_stderr;
+ }
+
+ // These handles are used by the child process, close them for this process
+ if (pin) {
+ CloseHandle(stdin_read_handle);
+ }
+ if (pout) {
+ CloseHandle(stdout_write_handle);
+ }
+ if (perr) {
+ CloseHandle(stderr_write_handle);
}
free(wide);
*handle = pi.hProcess;
- return PROCESS_SUCCESS;
+
+ return SC_PROCESS_SUCCESS;
+
+error_close_stderr:
+ if (perr) {
+ CloseHandle(*perr);
+ CloseHandle(stderr_write_handle);
+ }
+error_close_stdout:
+ if (pout) {
+ CloseHandle(*pout);
+ CloseHandle(stdout_write_handle);
+ }
+error_close_stdin:
+ if (pin) {
+ CloseHandle(*pin);
+ CloseHandle(stdin_read_handle);
+ }
+
+ return ret;
}
bool
-process_terminate(HANDLE handle) {
+sc_process_terminate(HANDLE handle) {
return TerminateProcess(handle, 1);
}
-exit_code_t
-process_wait(HANDLE handle, bool close) {
+sc_exit_code
+sc_process_wait(HANDLE handle, bool close) {
DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) {
// could not wait or retrieve the exit code
- code = NO_EXIT_CODE; // max value, it's unsigned
+ code = SC_EXIT_CODE_NONE;
}
if (close) {
CloseHandle(handle);
@@ -77,42 +161,24 @@ process_wait(HANDLE handle, bool close) {
}
void
-process_close(HANDLE handle) {
+sc_process_close(HANDLE handle) {
bool closed = CloseHandle(handle);
assert(closed);
(void) closed;
}
-char *
-get_executable_path(void) {
- HMODULE hModule = GetModuleHandleW(NULL);
- if (!hModule) {
- return NULL;
+ssize_t
+sc_read_pipe(HANDLE pipe, char *data, size_t len) {
+ DWORD r;
+ if (!ReadFile(pipe, data, len, &r, NULL)) {
+ return -1;
}
- WCHAR buf[MAX_PATH + 1]; // +1 for the null byte
- int len = GetModuleFileNameW(hModule, buf, MAX_PATH);
- if (!len) {
- return NULL;
- }
- buf[len] = '\0';
- return utf8_from_wide_char(buf);
+ return r;
}
-bool
-is_regular_file(const char *path) {
- wchar_t *wide_path = utf8_to_wide_char(path);
- if (!wide_path) {
- LOGC("Could not allocate wide char string");
- return false;
+void
+sc_close_pipe(HANDLE pipe) {
+ if (!CloseHandle(pipe)) {
+ LOGW("Cannot close pipe");
}
-
- struct _stat path_stat;
- int r = _wstat(wide_path, &path_stat);
- free(wide_path);
-
- if (r) {
- perror("stat");
- return false;
- }
- return S_ISREG(path_stat.st_mode);
}
diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c
deleted file mode 100644
index df1f9e53..00000000
--- a/app/src/tiny_xpm.c
+++ /dev/null
@@ -1,119 +0,0 @@
-#include "tiny_xpm.h"
-
-#include
-#include
-#include
-#include
-#include
-
-#include "util/log.h"
-
-struct index {
- char c;
- uint32_t color;
-};
-
-static bool
-find_color(struct index *index, int len, char c, uint32_t *color) {
- // there are typically very few color, so it's ok to iterate over the array
- for (int i = 0; i < len; ++i) {
- if (index[i].c == c) {
- *color = index[i].color;
- return true;
- }
- }
- *color = 0;
- return false;
-}
-
-// We encounter some problems with SDL2_image on MSYS2 (Windows),
-// so here is our own XPM parsing not to depend on SDL_image.
-//
-// We do not hardcode the binary image to keep some flexibility to replace the
-// icon easily (just by replacing icon.xpm).
-//
-// Parameter is not "const char *" because XPM formats are generally stored in a
-// (non-const) "char *"
-SDL_Surface *
-read_xpm(char *xpm[]) {
-#ifndef NDEBUG
- // patch the XPM to change the icon color in debug mode
- xpm[2] = ". c #CC00CC";
-#endif
-
- char *endptr;
- // *** No error handling, assume the XPM source is valid ***
- // (it's in our source repo)
- // Assertions are only checked in debug
- int width = strtol(xpm[0], &endptr, 10);
- int height = strtol(endptr + 1, &endptr, 10);
- int colors = strtol(endptr + 1, &endptr, 10);
- int chars = strtol(endptr + 1, &endptr, 10);
-
- // sanity checks
- assert(0 <= width && width < 256);
- assert(0 <= height && height < 256);
- assert(0 <= colors && colors < 256);
- assert(chars == 1); // this implementation does not support more
-
- (void) chars;
-
- // init index
- struct index index[colors];
- for (int i = 0; i < colors; ++i) {
- const char *line = xpm[1+i];
- index[i].c = line[0];
- assert(line[1] == '\t');
- assert(line[2] == 'c');
- assert(line[3] == ' ');
- if (line[4] == '#') {
- index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10);
- assert(*endptr == '\0');
- } else {
- assert(!strcmp("None", &line[4]));
- index[i].color = 0;
- }
- }
-
- // parse image
- uint32_t *pixels = SDL_malloc(4 * width * height);
- if (!pixels) {
- LOGE("Could not allocate icon memory");
- return NULL;
- }
- for (int y = 0; y < height; ++y) {
- const char *line = xpm[1 + colors + y];
- for (int x = 0; x < width; ++x) {
- char c = line[x];
- uint32_t color;
- bool color_found = find_color(index, colors, c, &color);
- assert(color_found);
- (void) color_found;
- pixels[y * width + x] = color;
- }
- }
-
-#if SDL_BYTEORDER == SDL_BIG_ENDIAN
- uint32_t amask = 0x000000ff;
- uint32_t rmask = 0x0000ff00;
- uint32_t gmask = 0x00ff0000;
- uint32_t bmask = 0xff000000;
-#else // little endian, like x86
- uint32_t amask = 0xff000000;
- uint32_t rmask = 0x00ff0000;
- uint32_t gmask = 0x0000ff00;
- uint32_t bmask = 0x000000ff;
-#endif
-
- SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels,
- width, height,
- 32, 4 * width,
- rmask, gmask, bmask, amask);
- if (!surface) {
- LOGE("Could not create icon surface");
- return NULL;
- }
- // make the surface own the raw pixels
- surface->flags &= ~SDL_PREALLOC;
- return surface;
-}
diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h
deleted file mode 100644
index 29b42d14..00000000
--- a/app/src/tiny_xpm.h
+++ /dev/null
@@ -1,11 +0,0 @@
-#ifndef TINYXPM_H
-#define TINYXPM_H
-
-#include "common.h"
-
-#include
-
-SDL_Surface *
-read_xpm(char *xpm[]);
-
-#endif
diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h
index 64ab0de9..0214ab3e 100644
--- a/app/src/trait/frame_sink.h
+++ b/app/src/trait/frame_sink.h
@@ -1,5 +1,5 @@
-#ifndef SC_FRAME_SINK
-#define SC_FRAME_SINK
+#ifndef SC_FRAME_SINK_H
+#define SC_FRAME_SINK_H
#include "common.h"
diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h
new file mode 100644
index 00000000..5790310b
--- /dev/null
+++ b/app/src/trait/key_processor.h
@@ -0,0 +1,29 @@
+#ifndef SC_KEY_PROCESSOR_H
+#define SC_KEY_PROCESSOR_H
+
+#include "common.h"
+
+#include
+#include
+
+#include
+
+/**
+ * Key processor trait.
+ *
+ * Component able to process and inject keys should implement this trait.
+ */
+struct sc_key_processor {
+ const struct sc_key_processor_ops *ops;
+};
+
+struct sc_key_processor_ops {
+ void
+ (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event);
+
+ void
+ (*process_text)(struct sc_key_processor *kp,
+ const SDL_TextInputEvent *event);
+};
+
+#endif
diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h
new file mode 100644
index 00000000..f3548574
--- /dev/null
+++ b/app/src/trait/mouse_processor.h
@@ -0,0 +1,39 @@
+#ifndef SC_MOUSE_PROCESSOR_H
+#define SC_MOUSE_PROCESSOR_H
+
+#include "common.h"
+
+#include
+#include
+
+#include
+
+/**
+ * Mouse processor trait.
+ *
+ * Component able to process and inject mouse events should implement this
+ * trait.
+ */
+struct sc_mouse_processor {
+ const struct sc_mouse_processor_ops *ops;
+};
+
+struct sc_mouse_processor_ops {
+ void
+ (*process_mouse_motion)(struct sc_mouse_processor *mp,
+ const SDL_MouseMotionEvent *event);
+
+ void
+ (*process_touch)(struct sc_mouse_processor *mp,
+ const SDL_TouchFingerEvent *event);
+
+ void
+ (*process_mouse_button)(struct sc_mouse_processor *mp,
+ const SDL_MouseButtonEvent *event);
+
+ void
+ (*process_mouse_wheel)(struct sc_mouse_processor *mp,
+ const SDL_MouseWheelEvent *event);
+};
+
+#endif
diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h
index fe9c137d..1fef765f 100644
--- a/app/src/trait/packet_sink.h
+++ b/app/src/trait/packet_sink.h
@@ -1,5 +1,5 @@
-#ifndef SC_PACKET_SINK
-#define SC_PACKET_SINK
+#ifndef SC_PACKET_SINK_H
+#define SC_PACKET_SINK_H
#include "common.h"
diff --git a/app/src/util/file.c b/app/src/util/file.c
new file mode 100644
index 00000000..59be2d91
--- /dev/null
+++ b/app/src/util/file.c
@@ -0,0 +1,48 @@
+#include "file.h"
+
+#include
+#include
+
+#include "util/log.h"
+
+char *
+sc_file_get_local_path(const char *name) {
+ char *executable_path = sc_file_get_executable_path();
+ if (!executable_path) {
+ return NULL;
+ }
+
+ // dirname() does not work correctly everywhere, so get the parent
+ // directory manually.
+ // See
+ char *p = strrchr(executable_path, SC_PATH_SEPARATOR);
+ if (!p) {
+ LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')",
+ executable_path, SC_PATH_SEPARATOR);
+ free(executable_path);
+ return NULL;
+ }
+
+ *p = '\0'; // modify executable_path in place
+ char *dir = executable_path;
+ size_t dirlen = strlen(dir);
+ size_t namelen = strlen(name);
+
+ size_t len = dirlen + namelen + 2; // +2: '/' and '\0'
+ char *file_path = malloc(len);
+ if (!file_path) {
+ LOGE("Could not alloc path");
+ free(executable_path);
+ return NULL;
+ }
+
+ memcpy(file_path, dir, dirlen);
+ file_path[dirlen] = SC_PATH_SEPARATOR;
+ // namelen + 1 to copy the final '\0'
+ memcpy(&file_path[dirlen + 1], name, namelen + 1);
+
+ free(executable_path);
+
+ return file_path;
+}
+
diff --git a/app/src/util/file.h b/app/src/util/file.h
new file mode 100644
index 00000000..089f6f75
--- /dev/null
+++ b/app/src/util/file.h
@@ -0,0 +1,49 @@
+#ifndef SC_FILE_H
+#define SC_FILE_H
+
+#include "common.h"
+
+#include
+
+#ifdef _WIN32
+# define SC_PATH_SEPARATOR '\\'
+#else
+# define SC_PATH_SEPARATOR '/'
+#endif
+
+#ifndef _WIN32
+/**
+ * Indicate if an executable exists using $PATH
+ *
+ * In practice, it is only used to know if a package manager is available on
+ * the system. It is only implemented on Linux.
+ */
+bool
+sc_file_executable_exists(const char *file);
+#endif
+
+/**
+ * Return the absolute path of the executable (the scrcpy binary)
+ *
+ * The result must be freed by the caller using free(). It may return NULL on
+ * error.
+ */
+char *
+sc_file_get_executable_path(void);
+
+/**
+ * Return the absolute path of a file in the same directory as the executable
+ *
+ * The result must be freed by the caller using free(). It may return NULL on
+ * error.
+ */
+char *
+sc_file_get_local_path(const char *name);
+
+/**
+ * Indicate if the file exists and is not a directory
+ */
+bool
+sc_file_is_regular(const char *path);
+
+#endif
diff --git a/app/src/util/intr.c b/app/src/util/intr.c
new file mode 100644
index 00000000..50d9abbe
--- /dev/null
+++ b/app/src/util/intr.c
@@ -0,0 +1,83 @@
+#include "intr.h"
+
+#include "util/log.h"
+
+#include
+
+bool
+sc_intr_init(struct sc_intr *intr) {
+ bool ok = sc_mutex_init(&intr->mutex);
+ if (!ok) {
+ LOGE("Could not init intr mutex");
+ return false;
+ }
+
+ intr->socket = SC_SOCKET_NONE;
+ intr->process = SC_PROCESS_NONE;
+
+ atomic_store_explicit(&intr->interrupted, false, memory_order_relaxed);
+
+ return true;
+}
+
+bool
+sc_intr_set_socket(struct sc_intr *intr, sc_socket socket) {
+ assert(intr->process == SC_PROCESS_NONE);
+
+ sc_mutex_lock(&intr->mutex);
+ bool interrupted =
+ atomic_load_explicit(&intr->interrupted, memory_order_relaxed);
+ if (!interrupted) {
+ intr->socket = socket;
+ }
+ sc_mutex_unlock(&intr->mutex);
+
+ return !interrupted;
+}
+
+bool
+sc_intr_set_process(struct sc_intr *intr, sc_pid pid) {
+ assert(intr->socket == SC_SOCKET_NONE);
+
+ sc_mutex_lock(&intr->mutex);
+ bool interrupted =
+ atomic_load_explicit(&intr->interrupted, memory_order_relaxed);
+ if (!interrupted) {
+ intr->process = pid;
+ }
+ sc_mutex_unlock(&intr->mutex);
+
+ return !interrupted;
+}
+
+void
+sc_intr_interrupt(struct sc_intr *intr) {
+ sc_mutex_lock(&intr->mutex);
+
+ atomic_store_explicit(&intr->interrupted, true, memory_order_relaxed);
+
+ // No more than one component to interrupt
+ assert(intr->socket == SC_SOCKET_NONE ||
+ intr->process == SC_PROCESS_NONE);
+
+ if (intr->socket != SC_SOCKET_NONE) {
+ LOGD("Interrupting socket");
+ net_interrupt(intr->socket);
+ intr->socket = SC_SOCKET_NONE;
+ }
+ if (intr->process != SC_PROCESS_NONE) {
+ LOGD("Interrupting process");
+ sc_process_terminate(intr->process);
+ intr->process = SC_PROCESS_NONE;
+ }
+
+ sc_mutex_unlock(&intr->mutex);
+}
+
+void
+sc_intr_destroy(struct sc_intr *intr) {
+ assert(intr->socket == SC_SOCKET_NONE);
+ assert(intr->process == SC_PROCESS_NONE);
+
+ sc_mutex_destroy(&intr->mutex);
+}
diff --git a/app/src/util/intr.h b/app/src/util/intr.h
new file mode 100644
index 00000000..1c20f6df
--- /dev/null
+++ b/app/src/util/intr.h
@@ -0,0 +1,78 @@
+#ifndef SC_INTR_H
+#define SC_INTR_H
+
+#include "common.h"
+
+#include
+#include
+
+#include "net.h"
+#include "process.h"
+#include "thread.h"
+
+/**
+ * Interruptor to wake up a blocking call from another thread
+ *
+ * It allows to register a socket or a process before a blocking call, and
+ * interrupt/close from another thread to wake up the blocking call.
+ */
+struct sc_intr {
+ sc_mutex mutex;
+
+ sc_socket socket;
+ sc_pid process;
+
+ // Written protected by the mutex to avoid race conditions against
+ // sc_intr_set_socket() and sc_intr_set_process(), but can be read
+ // (atomically) without mutex
+ atomic_bool interrupted;
+};
+
+/**
+ * Initialize an interruptor
+ */
+bool
+sc_intr_init(struct sc_intr *intr);
+
+/**
+ * Set a socket as the interruptible component
+ *
+ * Call with SC_SOCKET_NONE to unset.
+ */
+bool
+sc_intr_set_socket(struct sc_intr *intr, sc_socket socket);
+
+/**
+ * Set a process as the interruptible component
+ *
+ * Call with SC_PROCESS_NONE to unset.
+ */
+bool
+sc_intr_set_process(struct sc_intr *intr, sc_pid socket);
+
+/**
+ * Interrupt the current interruptible component
+ *
+ * Must be called from a different thread.
+ */
+void
+sc_intr_interrupt(struct sc_intr *intr);
+
+/**
+ * Read the interrupted state
+ *
+ * It is exposed as a static inline function because it just loads from an
+ * atomic.
+ */
+static inline bool
+sc_intr_is_interrupted(struct sc_intr *intr) {
+ return atomic_load_explicit(&intr->interrupted, memory_order_relaxed);
+}
+
+/**
+ * Destroy the interruptor
+ */
+void
+sc_intr_destroy(struct sc_intr *intr);
+
+#endif
diff --git a/app/src/util/log.h b/app/src/util/log.h
index 30934b5c..4157d6e5 100644
--- a/app/src/util/log.h
+++ b/app/src/util/log.h
@@ -5,7 +5,7 @@
#include
-#include "scrcpy.h"
+#include "options.h"
#define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
#define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__)
diff --git a/app/src/util/net.c b/app/src/util/net.c
index bbf57bbc..8595bc79 100644
--- a/app/src/util/net.c
+++ b/app/src/util/net.c
@@ -1,5 +1,6 @@
#include "net.h"
+#include
#include
#include
@@ -7,6 +8,7 @@
#ifdef __WINDOWS__
typedef int socklen_t;
+ typedef SOCKET sc_raw_socket;
#else
# include
# include
@@ -17,105 +19,9 @@
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
typedef struct in_addr IN_ADDR;
+ typedef int sc_raw_socket;
#endif
-socket_t
-net_connect(uint32_t addr, uint16_t port) {
- socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock == INVALID_SOCKET) {
- perror("socket");
- return INVALID_SOCKET;
- }
-
- SOCKADDR_IN sin;
- sin.sin_family = AF_INET;
- sin.sin_addr.s_addr = htonl(addr);
- sin.sin_port = htons(port);
-
- if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
- perror("connect");
- net_close(sock);
- return INVALID_SOCKET;
- }
-
- return sock;
-}
-
-socket_t
-net_listen(uint32_t addr, uint16_t port, int backlog) {
- socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock == INVALID_SOCKET) {
- perror("socket");
- return INVALID_SOCKET;
- }
-
- int reuse = 1;
- if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse,
- sizeof(reuse)) == -1) {
- perror("setsockopt(SO_REUSEADDR)");
- }
-
- SOCKADDR_IN sin;
- sin.sin_family = AF_INET;
- sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY
- sin.sin_port = htons(port);
-
- if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
- perror("bind");
- net_close(sock);
- return INVALID_SOCKET;
- }
-
- if (listen(sock, backlog) == SOCKET_ERROR) {
- perror("listen");
- net_close(sock);
- return INVALID_SOCKET;
- }
-
- return sock;
-}
-
-socket_t
-net_accept(socket_t server_socket) {
- SOCKADDR_IN csin;
- socklen_t sinsize = sizeof(csin);
- return accept(server_socket, (SOCKADDR *) &csin, &sinsize);
-}
-
-ssize_t
-net_recv(socket_t socket, void *buf, size_t len) {
- return recv(socket, buf, len, 0);
-}
-
-ssize_t
-net_recv_all(socket_t socket, void *buf, size_t len) {
- return recv(socket, buf, len, MSG_WAITALL);
-}
-
-ssize_t
-net_send(socket_t socket, const void *buf, size_t len) {
- return send(socket, buf, len, 0);
-}
-
-ssize_t
-net_send_all(socket_t socket, const void *buf, size_t len) {
- ssize_t w = 0;
- while (len > 0) {
- w = send(socket, buf, len, 0);
- if (w == -1) {
- return -1;
- }
- len -= w;
- buf = (char *) buf + w;
- }
- return w;
-}
-
-bool
-net_shutdown(socket_t socket, int how) {
- return !shutdown(socket, how);
-}
-
bool
net_init(void) {
#ifdef __WINDOWS__
@@ -136,11 +42,186 @@ net_cleanup(void) {
#endif
}
-bool
-net_close(socket_t socket) {
+static inline sc_socket
+wrap(sc_raw_socket sock) {
#ifdef __WINDOWS__
- return !closesocket(socket);
+ if (sock == INVALID_SOCKET) {
+ return SC_SOCKET_NONE;
+ }
+
+ struct sc_socket_windows *socket = malloc(sizeof(*socket));
+ if (!socket) {
+ closesocket(sock);
+ return SC_SOCKET_NONE;
+ }
+
+ socket->socket = sock;
+ socket->closed = (atomic_flag) ATOMIC_FLAG_INIT;
+
+ return socket;
#else
- return !close(socket);
+ return sock;
+#endif
+}
+
+static inline sc_raw_socket
+unwrap(sc_socket socket) {
+#ifdef __WINDOWS__
+ if (socket == SC_SOCKET_NONE) {
+ return INVALID_SOCKET;
+ }
+
+ return socket->socket;
+#else
+ return socket;
+#endif
+}
+
+static void
+net_perror(const char *s) {
+#ifdef _WIN32
+ int error = WSAGetLastError();
+ char *wsa_message;
+ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+ NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (char *) &wsa_message, 0, NULL);
+ // no explicit '\n', wsa_message already contains a trailing '\n'
+ fprintf(stderr, "%s: [%d] %s", s, error, wsa_message);
+ LocalFree(wsa_message);
+#else
+ perror(s);
+#endif
+}
+
+sc_socket
+net_socket(void) {
+ sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0);
+ sc_socket sock = wrap(raw_sock);
+ if (sock == SC_SOCKET_NONE) {
+ net_perror("socket");
+ }
+ return sock;
+}
+
+bool
+net_connect(sc_socket socket, uint32_t addr, uint16_t port) {
+ sc_raw_socket raw_sock = unwrap(socket);
+
+ SOCKADDR_IN sin;
+ sin.sin_family = AF_INET;
+ sin.sin_addr.s_addr = htonl(addr);
+ sin.sin_port = htons(port);
+
+ if (connect(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
+ net_perror("connect");
+ return false;
+ }
+
+ return true;
+}
+
+bool
+net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog) {
+ sc_raw_socket raw_sock = unwrap(socket);
+
+ int reuse = 1;
+ if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse,
+ sizeof(reuse)) == -1) {
+ net_perror("setsockopt(SO_REUSEADDR)");
+ }
+
+ SOCKADDR_IN sin;
+ sin.sin_family = AF_INET;
+ sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY
+ sin.sin_port = htons(port);
+
+ if (bind(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
+ net_perror("bind");
+ return false;
+ }
+
+ if (listen(raw_sock, backlog) == SOCKET_ERROR) {
+ net_perror("listen");
+ return false;
+ }
+
+ return true;
+}
+
+sc_socket
+net_accept(sc_socket server_socket) {
+ sc_raw_socket raw_server_socket = unwrap(server_socket);
+
+ SOCKADDR_IN csin;
+ socklen_t sinsize = sizeof(csin);
+ sc_raw_socket raw_sock =
+ accept(raw_server_socket, (SOCKADDR *) &csin, &sinsize);
+
+ return wrap(raw_sock);
+}
+
+ssize_t
+net_recv(sc_socket socket, void *buf, size_t len) {
+ sc_raw_socket raw_sock = unwrap(socket);
+ return recv(raw_sock, buf, len, 0);
+}
+
+ssize_t
+net_recv_all(sc_socket socket, void *buf, size_t len) {
+ sc_raw_socket raw_sock = unwrap(socket);
+ return recv(raw_sock, buf, len, MSG_WAITALL);
+}
+
+ssize_t
+net_send(sc_socket socket, const void *buf, size_t len) {
+ sc_raw_socket raw_sock = unwrap(socket);
+ return send(raw_sock, buf, len, 0);
+}
+
+ssize_t
+net_send_all(sc_socket socket, const void *buf, size_t len) {
+ size_t copied = 0;
+ while (len > 0) {
+ ssize_t w = net_send(socket, buf, len);
+ if (w == -1) {
+ return copied ? (ssize_t) copied : -1;
+ }
+ len -= w;
+ buf = (char *) buf + w;
+ copied += w;
+ }
+ return copied;
+}
+
+bool
+net_interrupt(sc_socket socket) {
+ assert(socket != SC_SOCKET_NONE);
+
+ sc_raw_socket raw_sock = unwrap(socket);
+
+#ifdef __WINDOWS__
+ if (!atomic_flag_test_and_set(&socket->closed)) {
+ return !closesocket(raw_sock);
+ }
+ return true;
+#else
+ return !shutdown(raw_sock, SHUT_RDWR);
+#endif
+}
+
+#include
+bool
+net_close(sc_socket socket) {
+ sc_raw_socket raw_sock = unwrap(socket);
+
+#ifdef __WINDOWS__
+ bool ret = true;
+ if (!atomic_flag_test_and_set(&socket->closed)) {
+ ret = !closesocket(raw_sock);
+ }
+ free(socket);
+ return ret;
+#else
+ return !close(raw_sock);
#endif
}
diff --git a/app/src/util/net.h b/app/src/util/net.h
index d3b1f941..57fd6c5e 100644
--- a/app/src/util/net.h
+++ b/app/src/util/net.h
@@ -8,50 +8,64 @@
#include
#ifdef __WINDOWS__
+
# include
- #define SHUT_RD SD_RECEIVE
- #define SHUT_WR SD_SEND
- #define SHUT_RDWR SD_BOTH
- typedef SOCKET socket_t;
-#else
+# include
+# define SC_SOCKET_NONE NULL
+ typedef struct sc_socket_windows {
+ SOCKET socket;
+ atomic_flag closed;
+ } *sc_socket;
+
+#else // not __WINDOWS__
+
# include
-# define INVALID_SOCKET -1
- typedef int socket_t;
+# define SC_SOCKET_NONE -1
+ typedef int sc_socket;
+
#endif
+#define IPV4_LOCALHOST 0x7F000001
+
bool
net_init(void);
void
net_cleanup(void);
-socket_t
-net_connect(uint32_t addr, uint16_t port);
+sc_socket
+net_socket(void);
-socket_t
-net_listen(uint32_t addr, uint16_t port, int backlog);
+bool
+net_connect(sc_socket socket, uint32_t addr, uint16_t port);
-socket_t
-net_accept(socket_t server_socket);
+bool
+net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog);
+
+sc_socket
+net_accept(sc_socket server_socket);
// the _all versions wait/retry until len bytes have been written/read
ssize_t
-net_recv(socket_t socket, void *buf, size_t len);
+net_recv(sc_socket socket, void *buf, size_t len);
ssize_t
-net_recv_all(socket_t socket, void *buf, size_t len);
+net_recv_all(sc_socket socket, void *buf, size_t len);
ssize_t
-net_send(socket_t socket, const void *buf, size_t len);
+net_send(sc_socket socket, const void *buf, size_t len);
ssize_t
-net_send_all(socket_t socket, const void *buf, size_t len);
+net_send_all(sc_socket socket, const void *buf, size_t len);
-// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both)
+// Shutdown the socket (or close on Windows) so that any blocking send() or
+// recv() are interrupted.
bool
-net_shutdown(socket_t socket, int how);
+net_interrupt(sc_socket socket);
+// Close the socket.
+// A socket must always be closed, even if net_interrupt() has been called.
bool
-net_close(socket_t socket);
+net_close(sc_socket socket);
#endif
diff --git a/app/src/util/net_intr.c b/app/src/util/net_intr.c
new file mode 100644
index 00000000..bb70010b
--- /dev/null
+++ b/app/src/util/net_intr.c
@@ -0,0 +1,97 @@
+#include "net_intr.h"
+
+bool
+net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
+ uint16_t port) {
+ if (!sc_intr_set_socket(intr, socket)) {
+ // Already interrupted
+ return false;
+ }
+
+ bool ret = net_connect(socket, addr, port);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return ret;
+}
+
+bool
+net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
+ uint16_t port, int backlog) {
+ if (!sc_intr_set_socket(intr, socket)) {
+ // Already interrupted
+ return false;
+ }
+
+ bool ret = net_listen(socket, addr, port, backlog);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return ret;
+}
+
+sc_socket
+net_accept_intr(struct sc_intr *intr, sc_socket server_socket) {
+ if (!sc_intr_set_socket(intr, server_socket)) {
+ // Already interrupted
+ return SC_SOCKET_NONE;
+ }
+
+ sc_socket socket = net_accept(server_socket);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return socket;
+}
+
+ssize_t
+net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) {
+ if (!sc_intr_set_socket(intr, socket)) {
+ // Already interrupted
+ return -1;
+ }
+
+ ssize_t r = net_recv(socket, buf, len);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return r;
+}
+
+ssize_t
+net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf,
+ size_t len) {
+ if (!sc_intr_set_socket(intr, socket)) {
+ // Already interrupted
+ return -1;
+ }
+
+ ssize_t r = net_recv_all(socket, buf, len);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return r;
+}
+
+ssize_t
+net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf,
+ size_t len) {
+ if (!sc_intr_set_socket(intr, socket)) {
+ // Already interrupted
+ return -1;
+ }
+
+ ssize_t w = net_send(socket, buf, len);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return w;
+}
+
+ssize_t
+net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf,
+ size_t len) {
+ if (!sc_intr_set_socket(intr, socket)) {
+ // Already interrupted
+ return -1;
+ }
+
+ ssize_t w = net_send_all(socket, buf, len);
+
+ sc_intr_set_socket(intr, SC_SOCKET_NONE);
+ return w;
+}
diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h
new file mode 100644
index 00000000..a83fadda
--- /dev/null
+++ b/app/src/util/net_intr.h
@@ -0,0 +1,35 @@
+#ifndef SC_NET_INTR_H
+#define SC_NET_INTR_H
+
+#include "common.h"
+
+#include "intr.h"
+#include "net.h"
+
+bool
+net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
+ uint16_t port);
+
+bool
+net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
+ uint16_t port, int backlog);
+
+sc_socket
+net_accept_intr(struct sc_intr *intr, sc_socket server_socket);
+
+ssize_t
+net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len);
+
+ssize_t
+net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf,
+ size_t len);
+
+ssize_t
+net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf,
+ size_t len);
+
+ssize_t
+net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf,
+ size_t len);
+
+#endif
diff --git a/app/src/util/process.c b/app/src/util/process.c
index 5edeeee6..28f51edd 100644
--- a/app/src/util/process.c
+++ b/app/src/util/process.c
@@ -1,17 +1,25 @@
#include "process.h"
+#include
+#include
#include "log.h"
+enum sc_process_result
+sc_process_execute(const char *const argv[], sc_pid *pid) {
+ return sc_process_execute_p(argv, pid, NULL, NULL, NULL);
+}
+
bool
-process_check_success(process_t proc, const char *name, bool close) {
- if (proc == PROCESS_NONE) {
+sc_process_check_success(sc_pid pid, const char *name, bool close) {
+ if (pid == SC_PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name);
return false;
}
- exit_code_t exit_code = process_wait(proc, close);
+ sc_exit_code exit_code = sc_process_wait(pid, close);
if (exit_code) {
- if (exit_code != NO_EXIT_CODE) {
- LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code);
+ if (exit_code != SC_EXIT_CODE_NONE) {
+ LOGE("\"%s\" returned with value %" SC_PRIexitcode, name,
+ exit_code);
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
@@ -19,3 +27,95 @@ process_check_success(process_t proc, const char *name, bool close) {
}
return true;
}
+
+ssize_t
+sc_pipe_read_all(sc_pipe pipe, char *data, size_t len) {
+ size_t copied = 0;
+ while (len > 0) {
+ ssize_t r = sc_pipe_read(pipe, data, len);
+ if (r <= 0) {
+ return copied ? (ssize_t) copied : r;
+ }
+ len -= r;
+ data += r;
+ copied += r;
+ }
+ return copied;
+}
+
+static int
+run_observer(void *data) {
+ struct sc_process_observer *observer = data;
+ sc_process_wait(observer->pid, false); // ignore exit code
+
+ sc_mutex_lock(&observer->mutex);
+ observer->terminated = true;
+ sc_cond_signal(&observer->cond_terminated);
+ sc_mutex_unlock(&observer->mutex);
+
+ if (observer->listener) {
+ observer->listener->on_terminated(observer->listener_userdata);
+ }
+
+ return 0;
+}
+
+bool
+sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid,
+ const struct sc_process_listener *listener,
+ void *listener_userdata) {
+ // Either no listener, or on_terminated() is defined
+ assert(!listener || listener->on_terminated);
+
+ bool ok = sc_mutex_init(&observer->mutex);
+ if (!ok) {
+ return false;
+ }
+
+ ok = sc_cond_init(&observer->cond_terminated);
+ if (!ok) {
+ sc_mutex_destroy(&observer->mutex);
+ return false;
+ }
+
+ observer->pid = pid;
+ observer->listener = listener;
+ observer->listener_userdata = listener_userdata;
+ observer->terminated = false;
+
+ ok = sc_thread_create(&observer->thread, run_observer, "process_observer",
+ observer);
+ if (!ok) {
+ sc_cond_destroy(&observer->cond_terminated);
+ sc_mutex_destroy(&observer->mutex);
+ return false;
+ }
+
+ return true;
+}
+
+bool
+sc_process_observer_timedwait(struct sc_process_observer *observer,
+ sc_tick deadline) {
+ sc_mutex_lock(&observer->mutex);
+ bool timed_out = false;
+ while (!observer->terminated && !timed_out) {
+ timed_out = !sc_cond_timedwait(&observer->cond_terminated,
+ &observer->mutex, deadline);
+ }
+ bool terminated = observer->terminated;
+ sc_mutex_unlock(&observer->mutex);
+
+ return terminated;
+}
+
+void
+sc_process_observer_join(struct sc_process_observer *observer) {
+ sc_thread_join(&observer->thread, NULL);
+}
+
+void
+sc_process_observer_destroy(struct sc_process_observer *observer) {
+ sc_cond_destroy(&observer->cond_terminated);
+ sc_mutex_destroy(&observer->mutex);
+}
diff --git a/app/src/util/process.h b/app/src/util/process.h
index 7838a848..7964be5c 100644
--- a/app/src/util/process.h
+++ b/app/src/util/process.h
@@ -4,78 +4,173 @@
#include "common.h"
#include
+#include "util/thread.h"
#ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
# include
# include
-# define PATH_SEPARATOR '\\'
-# define PRIexitcode "lu"
+# define SC_PRIexitcode "lu"
//
-# define PRIsizet "Iu"
-# define PROCESS_NONE NULL
-# define NO_EXIT_CODE -1u // max value as unsigned
- typedef HANDLE process_t;
- typedef DWORD exit_code_t;
+# define SC_PRIsizet "Iu"
+# define SC_PROCESS_NONE NULL
+# define SC_EXIT_CODE_NONE -1u // max value as unsigned
+ typedef HANDLE sc_pid;
+ typedef DWORD sc_exit_code;
+ typedef HANDLE sc_pipe;
#else
# include
-# define PATH_SEPARATOR '/'
-# define PRIsizet "zu"
-# define PRIexitcode "d"
-# define PROCESS_NONE -1
-# define NO_EXIT_CODE -1
- typedef pid_t process_t;
- typedef int exit_code_t;
+# define SC_PRIsizet "zu"
+# define SC_PRIexitcode "d"
+# define SC_PROCESS_NONE -1
+# define SC_EXIT_CODE_NONE -1
+ typedef pid_t sc_pid;
+ typedef int sc_exit_code;
+ typedef int sc_pipe;
#endif
-enum process_result {
- PROCESS_SUCCESS,
- PROCESS_ERROR_GENERIC,
- PROCESS_ERROR_MISSING_BINARY,
+struct sc_process_listener {
+ void (*on_terminated)(void *userdata);
};
-// execute the command and write the result to the output parameter "process"
-enum process_result
-process_execute(const char *const argv[], process_t *process);
+/**
+ * Tool to observe process termination
+ *
+ * To keep things simple and multiplatform, it runs a separate thread to wait
+ * for process termination (without closing the process to avoid race
+ * conditions).
+ *
+ * It allows a caller to block until the process is terminated (with a
+ * timeout), and to be notified asynchronously from the observer thread.
+ *
+ * The process is not owned by the observer (the observer will never close it).
+ */
+struct sc_process_observer {
+ sc_pid pid;
-// kill the process
+ sc_mutex mutex;
+ sc_cond cond_terminated;
+ bool terminated;
+
+ sc_thread thread;
+ const struct sc_process_listener *listener;
+ void *listener_userdata;
+};
+
+enum sc_process_result {
+ SC_PROCESS_SUCCESS,
+ SC_PROCESS_ERROR_GENERIC,
+ SC_PROCESS_ERROR_MISSING_BINARY,
+};
+
+/**
+ * Execute the command and write the process id to `pid`
+ */
+enum sc_process_result
+sc_process_execute(const char *const argv[], sc_pid *pid);
+
+/**
+ * Execute the command and write the process id to `pid`
+ *
+ * If not NULL, provide a pipe for stdin (`pin`), stdout (`pout`) and stderr
+ * (`perr`).
+ */
+enum sc_process_result
+sc_process_execute_p(const char *const argv[], sc_pid *pid,
+ sc_pipe *pin, sc_pipe *pout, sc_pipe *perr);
+
+/**
+ * Kill the process
+ */
bool
-process_terminate(process_t pid);
+sc_process_terminate(sc_pid pid);
-// wait and close the process (like waitpid())
-// the "close" flag indicates if the process must be "closed" (reaped)
-// (passing false is equivalent to enable WNOWAIT in waitid())
-exit_code_t
-process_wait(process_t pid, bool close);
+/**
+ * Wait and close the process (similar to waitpid())
+ *
+ * The `close` flag indicates if the process must be _closed_ (reaped) (passing
+ * false is equivalent to enable WNOWAIT in waitid()).
+ */
+sc_exit_code
+sc_process_wait(sc_pid pid, bool close);
-// close the process
-//
-// Semantically, process_wait(close) = process_wait(noclose) + process_close
+/**
+ * Close (reap) the process
+ *
+ * Semantically:
+ * sc_process_wait(close) = sc_process_wait(noclose) + sc_process_close()
+ */
void
-process_close(process_t pid);
+sc_process_close(sc_pid pid);
-// convenience function to wait for a successful process execution
-// automatically log process errors with the provided process name
+/**
+ * Convenience function to wait for a successful process execution
+ *
+ * Automatically log process errors with the provided process name.
+ */
bool
-process_check_success(process_t proc, const char *name, bool close);
+sc_process_check_success(sc_pid pid, const char *name, bool close);
-#ifndef _WIN32
-// only used to find package manager, not implemented for Windows
+/**
+ * Read from the pipe
+ *
+ * Same semantic as read().
+ */
+ssize_t
+sc_pipe_read(sc_pipe pipe, char *data, size_t len);
+
+/**
+ * Read exactly `len` chars from a pipe (unless EOF)
+ */
+ssize_t
+sc_pipe_read_all(sc_pipe pipe, char *data, size_t len);
+
+/**
+ * Close the pipe
+ */
+void
+sc_pipe_close(sc_pipe pipe);
+
+/**
+ * Start observing process
+ *
+ * The listener is optional. If set, its callback will be called from the
+ * observer thread once the process is terminated.
+ */
bool
-search_executable(const char *file);
-#endif
+sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid,
+ const struct sc_process_listener *listener,
+ void *listener_userdata);
-// return the absolute path of the executable (the scrcpy binary)
-// may be NULL on error; to be freed by free()
-char *
-get_executable_path(void);
-
-// returns true if the file exists and is not a directory
+/**
+ * Wait for process termination until a deadline
+ *
+ * Return true if the process is already terminated. Return false if the
+ * process terminatation has not been detected yet (however, it may have
+ * terminated in the meantime).
+ *
+ * To wait without timeout/deadline, just use sc_process_wait() instead.
+ */
bool
-is_regular_file(const char *path);
+sc_process_observer_timedwait(struct sc_process_observer *observer,
+ sc_tick deadline);
+
+/**
+ * Join the observer thread
+ */
+void
+sc_process_observer_join(struct sc_process_observer *observer);
+
+/**
+ * Destroy the observer
+ *
+ * This does not close the associated process.
+ */
+void
+sc_process_observer_destroy(struct sc_process_observer *observer);
#endif
diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c
new file mode 100644
index 00000000..bb483123
--- /dev/null
+++ b/app/src/util/process_intr.c
@@ -0,0 +1,16 @@
+#include "process_intr.h"
+
+bool
+sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
+ const char *name) {
+ if (!sc_intr_set_process(intr, pid)) {
+ // Already interrupted
+ return false;
+ }
+
+ // Always pass close=false, interrupting would be racy otherwise
+ bool ret = sc_process_check_success(pid, name, false);
+
+ sc_intr_set_process(intr, SC_PROCESS_NONE);
+ return ret;
+}
diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h
new file mode 100644
index 00000000..ff0dfc76
--- /dev/null
+++ b/app/src/util/process_intr.h
@@ -0,0 +1,13 @@
+#ifndef SC_PROCESS_INTR_H
+#define SC_PROCESS_INTR_H
+
+#include "common.h"
+
+#include "intr.h"
+#include "process.h"
+
+bool
+sc_process_check_success_intr(struct sc_intr *intr, sc_pid pid,
+ const char *name);
+
+#endif
diff --git a/app/src/util/queue.h b/app/src/util/queue.h
index 0681070c..2233eca0 100644
--- a/app/src/util/queue.h
+++ b/app/src/util/queue.h
@@ -1,6 +1,6 @@
// generic intrusive FIFO queue
-#ifndef QUEUE_H
-#define QUEUE_H
+#ifndef SC_QUEUE_H
+#define SC_QUEUE_H
#include "common.h"
@@ -10,15 +10,15 @@
// To define a queue type of "struct foo":
// struct queue_foo QUEUE(struct foo);
-#define QUEUE(TYPE) { \
+#define SC_QUEUE(TYPE) { \
TYPE *first; \
TYPE *last; \
}
-#define queue_init(PQ) \
+#define sc_queue_init(PQ) \
(void) ((PQ)->first = (PQ)->last = NULL)
-#define queue_is_empty(PQ) \
+#define sc_queue_is_empty(PQ) \
!(PQ)->first
// NEXTFIELD is the field in the ITEM type used for intrusive linked-list
@@ -30,30 +30,30 @@
// };
//
// // define the type "struct my_queue"
-// struct my_queue QUEUE(struct foo);
+// struct my_queue SC_QUEUE(struct foo);
//
// struct my_queue queue;
-// queue_init(&queue);
+// sc_queue_init(&queue);
//
// struct foo v1 = { .value = 42 };
// struct foo v2 = { .value = 27 };
//
-// queue_push(&queue, next, v1);
-// queue_push(&queue, next, v2);
+// sc_queue_push(&queue, next, v1);
+// sc_queue_push(&queue, next, v2);
//
// struct foo *foo;
-// queue_take(&queue, next, &foo);
+// sc_queue_take(&queue, next, &foo);
// assert(foo->value == 42);
-// queue_take(&queue, next, &foo);
+// sc_queue_take(&queue, next, &foo);
// assert(foo->value == 27);
-// assert(queue_is_empty(&queue));
+// assert(sc_queue_is_empty(&queue));
//
// push a new item into the queue
-#define queue_push(PQ, NEXTFIELD, ITEM) \
+#define sc_queue_push(PQ, NEXTFIELD, ITEM) \
(void) ({ \
(ITEM)->NEXTFIELD = NULL; \
- if (queue_is_empty(PQ)) { \
+ if (sc_queue_is_empty(PQ)) { \
(PQ)->first = (PQ)->last = (ITEM); \
} else { \
(PQ)->last->NEXTFIELD = (ITEM); \
@@ -65,9 +65,9 @@
// the result is stored in *(PITEM)
// (without typeof(), we could not store a local variable having the correct
// type so that we can "return" it)
-#define queue_take(PQ, NEXTFIELD, PITEM) \
+#define sc_queue_take(PQ, NEXTFIELD, PITEM) \
(void) ({ \
- assert(!queue_is_empty(PQ)); \
+ assert(!sc_queue_is_empty(PQ)); \
*(PITEM) = (PQ)->first; \
(PQ)->first = (PQ)->first->NEXTFIELD; \
})
diff --git a/app/src/util/str_util.c b/app/src/util/str.c
similarity index 59%
rename from app/src/util/str_util.c
rename to app/src/util/str.c
index 287c08de..7935c6bb 100644
--- a/app/src/util/str_util.c
+++ b/app/src/util/str.c
@@ -1,9 +1,11 @@
-#include "str_util.h"
+#include "str.h"
+#include
#include
#include
#include
#include
+#include "util/strbuf.h"
#ifdef _WIN32
# include
@@ -11,7 +13,7 @@
#endif
size_t
-xstrncpy(char *dest, const char *src, size_t n) {
+sc_strncpy(char *dest, const char *src, size_t n) {
size_t i;
for (i = 0; i < n - 1 && src[i] != '\0'; ++i)
dest[i] = src[i];
@@ -21,7 +23,7 @@ xstrncpy(char *dest, const char *src, size_t n) {
}
size_t
-xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
+sc_str_join(char *dst, const char *const tokens[], char sep, size_t n) {
const char *const *remaining = tokens;
const char *token = *remaining++;
size_t i = 0;
@@ -31,7 +33,7 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) {
if (i == n)
goto truncated;
}
- size_t w = xstrncpy(dst + i, token, n - i);
+ size_t w = sc_strncpy(dst + i, token, n - i);
if (w >= n - i)
goto truncated;
i += w;
@@ -45,7 +47,7 @@ truncated:
}
char *
-strquote(const char *src) {
+sc_str_quote(const char *src) {
size_t len = strlen(src);
char *quoted = malloc(len + 3);
if (!quoted) {
@@ -59,7 +61,7 @@ strquote(const char *src) {
}
bool
-parse_integer(const char *s, long *out) {
+sc_str_parse_integer(const char *s, long *out) {
char *endptr;
if (*s == '\0') {
return false;
@@ -78,7 +80,8 @@ parse_integer(const char *s, long *out) {
}
size_t
-parse_integers(const char *s, const char sep, size_t max_items, long *out) {
+sc_str_parse_integers(const char *s, const char sep, size_t max_items,
+ long *out) {
size_t count = 0;
char *endptr;
do {
@@ -107,7 +110,7 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out) {
}
bool
-parse_integer_with_suffix(const char *s, long *out) {
+sc_str_parse_integer_with_suffix(const char *s, long *out) {
char *endptr;
if (*s == '\0') {
return false;
@@ -141,7 +144,7 @@ parse_integer_with_suffix(const char *s, long *out) {
}
bool
-strlist_contains(const char *list, char sep, const char *s) {
+sc_str_list_contains(const char *list, char sep, const char *s) {
char *p;
do {
p = strchr(list, sep);
@@ -159,7 +162,7 @@ strlist_contains(const char *list, char sep, const char *s) {
}
size_t
-utf8_truncation_index(const char *utf8, size_t max_len) {
+sc_str_utf8_truncation_index(const char *utf8, size_t max_len) {
size_t len = strlen(utf8);
if (len <= max_len) {
return len;
@@ -177,7 +180,7 @@ utf8_truncation_index(const char *utf8, size_t max_len) {
#ifdef _WIN32
wchar_t *
-utf8_to_wide_char(const char *utf8) {
+sc_str_to_wchars(const char *utf8) {
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0);
if (!len) {
return NULL;
@@ -193,7 +196,7 @@ utf8_to_wide_char(const char *utf8) {
}
char *
-utf8_from_wide_char(const wchar_t *ws) {
+sc_str_from_wchars(const wchar_t *ws) {
int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
if (!len) {
return NULL;
@@ -209,3 +212,82 @@ utf8_from_wide_char(const wchar_t *ws) {
}
#endif
+
+char *
+sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) {
+ assert(indent < columns);
+
+ struct sc_strbuf buf;
+
+ // The output string should not be much longer than the input string (just
+ // a few '\n' added), so this initial capacity should hopefully almost
+ // always avoid internal realloc() in string buffer
+ size_t cap = strlen(input) * 3 / 2;
+
+ if (!sc_strbuf_init(&buf, cap)) {
+ return false;
+ }
+
+#define APPEND(S,N) if (!sc_strbuf_append(&buf, S, N)) goto error
+#define APPEND_CHAR(C) if (!sc_strbuf_append_char(&buf, C)) goto error
+#define APPEND_N(C,N) if (!sc_strbuf_append_n(&buf, C, N)) goto error
+#define APPEND_INDENT() if (indent) APPEND_N(' ', indent)
+
+ APPEND_INDENT();
+
+ // The last separator encountered, it must be inserted only conditionnaly,
+ // depending on the next token
+ char pending = 0;
+
+ // col tracks the current column in the current line
+ size_t col = indent;
+ while (*input) {
+ size_t sep_idx = strcspn(input, "\n ");
+ size_t new_col = col + sep_idx;
+ if (pending == ' ') {
+ // The pending space counts
+ ++new_col;
+ }
+ bool wrap = new_col > columns;
+
+ char sep = input[sep_idx];
+ if (sep == ' ')
+ sep = ' ';
+
+ if (wrap) {
+ APPEND_CHAR('\n');
+ APPEND_INDENT();
+ col = indent;
+ } else if (pending) {
+ APPEND_CHAR(pending);
+ ++col;
+ if (pending == '\n')
+ {
+ APPEND_INDENT();
+ col = indent;
+ }
+ }
+
+ if (sep_idx) {
+ APPEND(input, sep_idx);
+ col += sep_idx;
+ }
+
+ pending = sep;
+
+ input += sep_idx;
+ if (*input != '\0') {
+ // Skip the separator
+ ++input;
+ }
+ }
+
+ if (pending)
+ APPEND_CHAR(pending);
+
+ return buf.s;
+
+error:
+ free(buf.s);
+ return NULL;
+}
diff --git a/app/src/util/str.h b/app/src/util/str.h
new file mode 100644
index 00000000..54e32808
--- /dev/null
+++ b/app/src/util/str.h
@@ -0,0 +1,106 @@
+#ifndef SC_STR_H
+#define SC_STR_H
+
+#include "common.h"
+
+#include
+#include
+
+/**
+ * Like strncpy(), except:
+ * - it copies at most n-1 chars
+ * - the dest string is nul-terminated
+ * - it does not write useless bytes if strlen(src) < n
+ * - it returns the number of chars actually written (max n-1) if src has
+ * been copied completely, or n if src has been truncated
+ */
+size_t
+sc_strncpy(char *dest, const char *src, size_t n);
+
+/**
+ * Join tokens by separator `sep` into `dst`
+ *
+ * Return the number of chars actually written (max n-1) if no truncation
+ * occurred, or n if truncated.
+ */
+size_t
+sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
+
+/**
+ * Quote a string
+ *
+ * Return a new allocated string, surrounded with quotes (`"`).
+ */
+char *
+sc_str_quote(const char *src);
+
+/**
+ * Parse `s` as an integer into `out`
+ *
+ * Return true if the conversion succeeded, false otherwise.
+ */
+bool
+sc_str_parse_integer(const char *s, long *out);
+
+/**
+ * Parse `s` as integers separated by `sep` (for example `1234:2000`) into `out`
+ *
+ * Returns the number of integers on success, 0 on failure.
+ */
+size_t
+sc_str_parse_integers(const char *s, const char sep, size_t max_items,
+ long *out);
+
+/**
+ * Parse `s` as an integer into `out`
+ *
+ * Like `sc_str_parse_integer()`, but accept 'k'/'K' (x1000) and 'm'/'M'
+ * (x1000000) as suffixes.
+ *
+ * Return true if the conversion succeeded, false otherwise.
+ */
+bool
+sc_str_parse_integer_with_suffix(const char *s, long *out);
+
+/**
+ * Search `s` in the list separated by `sep`
+ *
+ * For example, sc_str_list_contains("a,bc,def", ',', "bc") returns true.
+ */
+bool
+sc_str_list_contains(const char *list, char sep, const char *s);
+
+/**
+ * Return the index to truncate a UTF-8 string at a valid position
+ */
+size_t
+sc_str_utf8_truncation_index(const char *utf8, size_t max_len);
+
+#ifdef _WIN32
+/**
+ * Convert a UTF-8 string to a wchar_t string
+ *
+ * Return the new allocated string, to be freed by the caller.
+ */
+wchar_t *
+sc_str_to_wchars(const char *utf8);
+
+/**
+ * Convert a wchar_t string to a UTF-8 string
+ *
+ * Return the new allocated string, to be freed by the caller.
+ */
+char *
+sc_str_from_wchars(const wchar_t *s);
+#endif
+
+/**
+ * Wrap input lines to fit in `columns` columns
+ *
+ * Break input lines at word boundaries (spaces) so that they fit in `columns`
+ * columns, left-indented by `indent` spaces.
+ */
+char *
+sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent);
+
+#endif
diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h
deleted file mode 100644
index 361d2bdd..00000000
--- a/app/src/util/str_util.h
+++ /dev/null
@@ -1,65 +0,0 @@
-#ifndef STRUTIL_H
-#define STRUTIL_H
-
-#include "common.h"
-
-#include
-#include
-
-// like strncpy, except:
-// - it copies at most n-1 chars
-// - the dest string is nul-terminated
-// - it does not write useless bytes if strlen(src) < n
-// - it returns the number of chars actually written (max n-1) if src has
-// been copied completely, or n if src has been truncated
-size_t
-xstrncpy(char *dest, const char *src, size_t n);
-
-// join tokens by sep into dst
-// returns the number of chars actually written (max n-1) if no truncation
-// occurred, or n if truncated
-size_t
-xstrjoin(char *dst, const char *const tokens[], char sep, size_t n);
-
-// quote a string
-// returns the new allocated string, to be freed by the caller
-char *
-strquote(const char *src);
-
-// parse s as an integer into value
-// returns true if the conversion succeeded, false otherwise
-bool
-parse_integer(const char *s, long *out);
-
-// parse s as integers separated by sep (for example '1234:2000')
-// returns the number of integers on success, 0 on failure
-size_t
-parse_integers(const char *s, const char sep, size_t max_items, long *out);
-
-// parse s as an integer into value
-// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as
-// suffix
-// returns true if the conversion succeeded, false otherwise
-bool
-parse_integer_with_suffix(const char *s, long *out);
-
-// search s in the list separated by sep
-// for example, strlist_contains("a,bc,def", ',', "bc") returns true
-bool
-strlist_contains(const char *list, char sep, const char *s);
-
-// return the index to truncate a UTF-8 string at a valid position
-size_t
-utf8_truncation_index(const char *utf8, size_t max_len);
-
-#ifdef _WIN32
-// convert a UTF-8 string to a wchar_t string
-// returns the new allocated string, to be freed by the caller
-wchar_t *
-utf8_to_wide_char(const char *utf8);
-
-char *
-utf8_from_wide_char(const wchar_t *s);
-#endif
-
-#endif
diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c
new file mode 100644
index 00000000..b2b6f494
--- /dev/null
+++ b/app/src/util/strbuf.c
@@ -0,0 +1,87 @@
+#include "strbuf.h"
+
+#include
+#include
+#include
+
+#include
+
+bool
+sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) {
+ buf->s = malloc(init_cap + 1); // +1 for '\0'
+ if (!buf->s) {
+ return false;
+ }
+
+ buf->len = 0;
+ buf->cap = init_cap;
+ return true;
+}
+
+static bool
+sc_strbuf_reserve(struct sc_strbuf *buf, size_t len) {
+ if (buf->len + len > buf->cap) {
+ size_t new_cap = buf->cap * 3 / 2 + len;
+ char *s = realloc(buf->s, new_cap + 1); // +1 for '\0'
+ if (!s) {
+ // Leave the old buf->s
+ return false;
+ }
+ buf->s = s;
+ buf->cap = new_cap;
+ }
+ return true;
+}
+
+bool
+sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len) {
+ assert(s);
+ assert(*s);
+ assert(strlen(s) >= len);
+ if (!sc_strbuf_reserve(buf, len)) {
+ return false;
+ }
+
+ memcpy(&buf->s[buf->len], s, len);
+ buf->len += len;
+ buf->s[buf->len] = '\0';
+
+ return true;
+}
+
+bool
+sc_strbuf_append_char(struct sc_strbuf *buf, const char c) {
+ if (!sc_strbuf_reserve(buf, 1)) {
+ return false;
+ }
+
+ buf->s[buf->len] = c;
+ buf->len ++;
+ buf->s[buf->len] = '\0';
+
+ return true;
+}
+
+bool
+sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n) {
+ if (!sc_strbuf_reserve(buf, n)) {
+ return false;
+ }
+
+ memset(&buf->s[buf->len], c, n);
+ buf->len += n;
+ buf->s[buf->len] = '\0';
+
+ return true;
+}
+
+void
+sc_strbuf_shrink(struct sc_strbuf *buf) {
+ assert(buf->len <= buf->cap);
+ if (buf->len != buf->cap) {
+ char *s = realloc(buf->s, buf->len + 1); // +1 for '\0'
+ assert(s); // decreasing the size may not fail
+ buf->s = s;
+ buf->cap = buf->len;
+ }
+}
diff --git a/app/src/util/strbuf.h b/app/src/util/strbuf.h
new file mode 100644
index 00000000..1878df2f
--- /dev/null
+++ b/app/src/util/strbuf.h
@@ -0,0 +1,73 @@
+#ifndef SC_STRBUF_H
+#define SC_STRBUF_H
+
+#include "common.h"
+
+#include
+#include
+#include
+
+struct sc_strbuf {
+ char *s;
+ size_t len;
+ size_t cap;
+};
+
+/**
+ * Initialize the string buffer
+ *
+ * `buf->s` must be manually freed by the caller.
+ */
+bool
+sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap);
+
+/**
+ * Append a string
+ *
+ * Append `len` characters from `s` to the buffer.
+ */
+bool
+sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len);
+
+/**
+ * Append a char
+ *
+ * Append a single character to the buffer.
+ */
+bool
+sc_strbuf_append_char(struct sc_strbuf *buf, const char c);
+
+/**
+ * Append a char `n` times
+ *
+ * Append the same characters `n` times to the buffer.
+ */
+bool
+sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n);
+
+/**
+ * Append a NUL-terminated string
+ */
+static inline bool
+sc_strbuf_append_str(struct sc_strbuf *buf, const char *s) {
+ return sc_strbuf_append(buf, s, strlen(s));
+}
+
+/**
+ * Append a static string
+ *
+ * Append a string whose size is known at compile time (for
+ * example a string literal).
+ */
+#define sc_strbuf_append_staticstr(BUF, S) \
+ sc_strbuf_append(BUF, S, sizeof(S) - 1)
+
+/**
+ * Shrink the buffer capacity to its current length
+ *
+ * This resizes `buf->s` to fit the content.
+ */
+void
+sc_strbuf_shrink(struct sc_strbuf *buf);
+
+#endif
diff --git a/app/src/util/term.c b/app/src/util/term.c
new file mode 100644
index 00000000..ff6bc4b1
--- /dev/null
+++ b/app/src/util/term.c
@@ -0,0 +1,51 @@
+#include "term.h"
+
+#include
+
+#ifdef _WIN32
+# include
+#else
+# include
+# include
+#endif
+
+bool
+sc_term_get_size(unsigned *rows, unsigned *cols) {
+#ifdef _WIN32
+ CONSOLE_SCREEN_BUFFER_INFO csbi;
+
+ bool ok =
+ GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
+ if (!ok) {
+ return false;
+ }
+
+ if (rows) {
+ assert(csbi.srWindow.Bottom >= csbi.srWindow.Top);
+ *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
+ }
+
+ if (cols) {
+ assert(csbi.srWindow.Right >= csbi.srWindow.Left);
+ *cols = csbi.srWindow.Right - csbi.srWindow.Left + 1;
+ }
+
+ return true;
+#else
+ struct winsize ws;
+ int r = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
+ if (r == -1) {
+ return false;
+ }
+
+ if (rows) {
+ *rows = ws.ws_row;
+ }
+
+ if (cols) {
+ *cols = ws.ws_col;
+ }
+
+ return true;
+#endif
+}
diff --git a/app/src/util/term.h b/app/src/util/term.h
new file mode 100644
index 00000000..0211bcb4
--- /dev/null
+++ b/app/src/util/term.h
@@ -0,0 +1,21 @@
+#ifndef SC_TERM_H
+#define SC_TERM_H
+
+#include "common.h"
+
+#include
+
+/**
+ * Return the terminal dimensions
+ *
+ * Return false if the dimensions could not be retrieved.
+ *
+ * Otherwise, return true, and:
+ * - if `rows` is not NULL, then the number of rows is written to `*rows`.
+ * - if `columns` is not NULL, then the number of columns is written to
+ * `*columns`.
+ */
+bool
+sc_term_get_size(unsigned *rows, unsigned *cols);
+
+#endif
diff --git a/app/src/util/thread.c b/app/src/util/thread.c
index a0a99f20..2c376e97 100644
--- a/app/src/util/thread.c
+++ b/app/src/util/thread.c
@@ -31,7 +31,7 @@ sc_mutex_init(sc_mutex *mutex) {
mutex->mutex = sdl_mutex;
#ifndef NDEBUG
- mutex->locker = 0;
+ atomic_init(&mutex->locker, 0);
#endif
return true;
}
@@ -52,7 +52,8 @@ sc_mutex_lock(sc_mutex *mutex) {
abort();
}
- mutex->locker = sc_thread_get_id();
+ atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
+ memory_order_relaxed);
#else
(void) r;
#endif
@@ -62,7 +63,7 @@ void
sc_mutex_unlock(sc_mutex *mutex) {
#ifndef NDEBUG
assert(sc_mutex_held(mutex));
- mutex->locker = 0;
+ atomic_store_explicit(&mutex->locker, 0, memory_order_relaxed);
#endif
int r = SDL_UnlockMutex(mutex->mutex);
#ifndef NDEBUG
@@ -83,7 +84,9 @@ sc_thread_get_id(void) {
#ifndef NDEBUG
bool
sc_mutex_held(struct sc_mutex *mutex) {
- return mutex->locker == sc_thread_get_id();
+ sc_thread_id locker_id =
+ atomic_load_explicit(&mutex->locker, memory_order_relaxed);
+ return locker_id == sc_thread_get_id();
}
#endif
@@ -112,14 +115,21 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) {
abort();
}
- mutex->locker = sc_thread_get_id();
+ atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
+ memory_order_relaxed);
#else
(void) r;
#endif
}
bool
-sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) {
+sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) {
+ sc_tick now = sc_tick_now();
+ if (deadline <= now) {
+ return false; // timeout
+ }
+
+ uint32_t ms = SC_TICK_TO_MS(deadline - now);
int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms);
#ifndef NDEBUG
if (r < 0) {
@@ -127,7 +137,8 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) {
abort();
}
- mutex->locker = sc_thread_get_id();
+ atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
+ memory_order_relaxed);
#endif
assert(r == 0 || r == SDL_MUTEX_TIMEDOUT);
return r == 0;
diff --git a/app/src/util/thread.h b/app/src/util/thread.h
index d23e1432..7add6f1c 100644
--- a/app/src/util/thread.h
+++ b/app/src/util/thread.h
@@ -3,8 +3,10 @@
#include "common.h"
+#include
#include
-#include
+
+#include "tick.h"
/* Forward declarations */
typedef struct SDL_Thread SDL_Thread;
@@ -12,7 +14,8 @@ typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
typedef int sc_thread_fn(void *);
-typedef unsigned int sc_thread_id;
+typedef unsigned sc_thread_id;
+typedef atomic_uint sc_atomic_thread_id;
typedef struct sc_thread {
SDL_Thread *thread;
@@ -21,7 +24,7 @@ typedef struct sc_thread {
typedef struct sc_mutex {
SDL_mutex *mutex;
#ifndef NDEBUG
- sc_thread_id locker;
+ sc_atomic_thread_id locker;
#endif
} sc_mutex;
@@ -70,7 +73,7 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex);
// return true on signaled, false on timeout
bool
-sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms);
+sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline);
void
sc_cond_signal(sc_cond *cond);
diff --git a/app/src/util/tick.c b/app/src/util/tick.c
new file mode 100644
index 00000000..b85ce971
--- /dev/null
+++ b/app/src/util/tick.c
@@ -0,0 +1,16 @@
+#include "tick.h"
+
+#include
+
+sc_tick
+sc_tick_now(void) {
+ // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed
+ // in microseconds to store PTS without precision loss.
+ //
+ // As an alternative, SDL_GetPerformanceCounter() and
+ // SDL_GetPerformanceFrequency() could be used, but:
+ // - the conversions (avoiding overflow) are expansive, since the
+ // frequency is not known at compile time;
+ // - in practice, we don't need more precision for now.
+ return (sc_tick) SDL_GetTicks() * 1000;
+}
diff --git a/app/src/util/tick.h b/app/src/util/tick.h
new file mode 100644
index 00000000..47d02529
--- /dev/null
+++ b/app/src/util/tick.h
@@ -0,0 +1,23 @@
+#ifndef SC_TICK_H
+#define SC_TICK_H
+
+#include "common.h"
+
+#include
+
+typedef int64_t sc_tick;
+#define PRItick PRIi64
+#define SC_TICK_FREQ 1000000 // microsecond
+
+// To be adapted if SC_TICK_FREQ changes
+#define SC_TICK_TO_US(tick) (tick)
+#define SC_TICK_TO_MS(tick) ((tick) / 1000)
+#define SC_TICK_TO_SEC(tick) ((tick) / 1000000)
+#define SC_TICK_FROM_US(us) (us)
+#define SC_TICK_FROM_MS(ms) ((ms) * 1000)
+#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000)
+
+sc_tick
+sc_tick_now(void);
+
+#endif
diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c
index bd184b4d..753d5b6a 100644
--- a/app/src/v4l2_sink.c
+++ b/app/src/v4l2_sink.c
@@ -1,7 +1,7 @@
#include "v4l2_sink.h"
#include "util/log.h"
-#include "util/str_util.h"
+#include "util/str.h"
/** Downcast frame_sink to sc_v4l2_sink */
#define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink)
@@ -21,7 +21,7 @@ find_muxer(const char *name) {
oformat = av_oformat_next(oformat);
#endif
// until null or containing the requested name
- } while (oformat && !strlist_contains(oformat->name, ',', name));
+ } while (oformat && !sc_str_list_contains(oformat->name, ',', name));
return oformat;
}
@@ -112,7 +112,7 @@ run_v4l2_sink(void *data) {
for (;;) {
sc_mutex_lock(&vs->mutex);
- while (!vs->stopped && vs->vb.pending_frame_consumed) {
+ while (!vs->stopped && !vs->has_frame) {
sc_cond_wait(&vs->cond, &vs->mutex);
}
@@ -121,9 +121,11 @@ run_v4l2_sink(void *data) {
break;
}
+ vs->has_frame = false;
sc_mutex_unlock(&vs->mutex);
- video_buffer_consume(&vs->vb, vs->frame);
+ sc_video_buffer_consume(&vs->vb, vs->frame);
+
bool ok = encode_and_write_frame(vs, vs->frame);
av_frame_unref(vs->frame);
if (!ok) {
@@ -137,17 +139,42 @@ run_v4l2_sink(void *data) {
return 0;
}
+static void
+sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
+ void *userdata) {
+ (void) vb;
+ struct sc_v4l2_sink *vs = userdata;
+
+ if (!previous_skipped) {
+ sc_mutex_lock(&vs->mutex);
+ vs->has_frame = true;
+ sc_cond_signal(&vs->cond);
+ sc_mutex_unlock(&vs->mutex);
+ }
+}
+
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
- bool ok = video_buffer_init(&vs->vb);
+ static const struct sc_video_buffer_callbacks cbs = {
+ .on_new_frame = sc_video_buffer_on_new_frame,
+ };
+
+ bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs);
if (!ok) {
+ LOGE("Could not initialize video buffer");
return false;
}
+ ok = sc_video_buffer_start(&vs->vb);
+ if (!ok) {
+ LOGE("Could not start video buffer");
+ goto error_video_buffer_destroy;
+ }
+
ok = sc_mutex_init(&vs->mutex);
if (!ok) {
LOGC("Could not create mutex");
- goto error_video_buffer_destroy;
+ goto error_video_buffer_stop_and_join;
}
ok = sc_cond_init(&vs->cond);
@@ -156,8 +183,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
goto error_mutex_destroy;
}
- // FIXME
- const AVOutputFormat *format = find_muxer("video4linux2,v4l2");
+ const AVOutputFormat *format = find_muxer("v4l2");
+ if (!format) {
+ // Alternative name
+ format = find_muxer("video4linux2");
+ }
if (!format) {
LOGE("Could not find v4l2 muxer");
goto error_cond_destroy;
@@ -241,6 +271,10 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
goto error_av_frame_free;
}
+ vs->has_frame = false;
+ vs->header_written = false;
+ vs->stopped = false;
+
LOGD("Starting v4l2 thread");
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs);
if (!ok) {
@@ -248,9 +282,6 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
goto error_av_packet_free;
}
- vs->header_written = false;
- vs->stopped = false;
-
LOGI("v4l2 sink started to device: %s", vs->device_name);
return true;
@@ -271,8 +302,11 @@ error_cond_destroy:
sc_cond_destroy(&vs->cond);
error_mutex_destroy:
sc_mutex_destroy(&vs->mutex);
+error_video_buffer_stop_and_join:
+ sc_video_buffer_stop(&vs->vb);
+ sc_video_buffer_join(&vs->vb);
error_video_buffer_destroy:
- video_buffer_destroy(&vs->vb);
+ sc_video_buffer_destroy(&vs->vb);
return false;
}
@@ -284,7 +318,10 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
sc_cond_signal(&vs->cond);
sc_mutex_unlock(&vs->mutex);
+ sc_video_buffer_stop(&vs->vb);
+
sc_thread_join(&vs->thread, NULL);
+ sc_video_buffer_join(&vs->vb);
av_packet_free(&vs->packet);
av_frame_free(&vs->frame);
@@ -294,20 +331,12 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
avformat_free_context(vs->format_ctx);
sc_cond_destroy(&vs->cond);
sc_mutex_destroy(&vs->mutex);
- video_buffer_destroy(&vs->vb);
+ sc_video_buffer_destroy(&vs->vb);
}
static bool
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
- bool ok = video_buffer_push(&vs->vb, frame, NULL);
- if (!ok) {
- return false;
- }
-
- // signal possible change of vs->vb.pending_frame_consumed
- sc_cond_signal(&vs->cond);
-
- return true;
+ return sc_video_buffer_push(&vs->vb, frame);
}
static bool
@@ -330,7 +359,7 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
- struct size frame_size) {
+ struct sc_size frame_size, sc_tick buffering_time) {
vs->device_name = strdup(device_name);
if (!vs->device_name) {
LOGE("Could not strdup v4l2 device name");
@@ -338,6 +367,7 @@ sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
}
vs->frame_size = frame_size;
+ vs->buffering_time = buffering_time;
static const struct sc_frame_sink_ops ops = {
.open = sc_v4l2_frame_sink_open,
diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h
index 81bcdd1e..8737a607 100644
--- a/app/src/v4l2_sink.h
+++ b/app/src/v4l2_sink.h
@@ -6,22 +6,25 @@
#include "coords.h"
#include "trait/frame_sink.h"
#include "video_buffer.h"
+#include "util/tick.h"
#include
struct sc_v4l2_sink {
struct sc_frame_sink frame_sink; // frame sink trait
- struct video_buffer vb;
+ struct sc_video_buffer vb;
AVFormatContext *format_ctx;
AVCodecContext *encoder_ctx;
char *device_name;
- struct size frame_size;
+ struct sc_size frame_size;
+ sc_tick buffering_time;
sc_thread thread;
sc_mutex mutex;
sc_cond cond;
+ bool has_frame;
bool stopped;
bool header_written;
@@ -31,7 +34,7 @@ struct sc_v4l2_sink {
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
- struct size frame_size);
+ struct sc_size frame_size, sc_tick buffering_time);
void
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c
index 7adf098b..f71a4e78 100644
--- a/app/src/video_buffer.c
+++ b/app/src/video_buffer.c
@@ -1,88 +1,255 @@
#include "video_buffer.h"
#include
+#include
+
#include
#include
#include "util/log.h"
-bool
-video_buffer_init(struct video_buffer *vb) {
- vb->pending_frame = av_frame_alloc();
- if (!vb->pending_frame) {
- return false;
+#define SC_BUFFERING_NDEBUG // comment to debug
+
+static struct sc_video_buffer_frame *
+sc_video_buffer_frame_new(const AVFrame *frame) {
+ struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame));
+ if (!vb_frame) {
+ return NULL;
}
- vb->tmp_frame = av_frame_alloc();
- if (!vb->tmp_frame) {
- av_frame_free(&vb->pending_frame);
- return false;
+ vb_frame->frame = av_frame_alloc();
+ if (!vb_frame->frame) {
+ free(vb_frame);
+ return NULL;
}
- bool ok = sc_mutex_init(&vb->mutex);
+ if (av_frame_ref(vb_frame->frame, frame)) {
+ av_frame_free(&vb_frame->frame);
+ free(vb_frame);
+ return NULL;
+ }
+
+ return vb_frame;
+}
+
+static void
+sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) {
+ av_frame_unref(vb_frame->frame);
+ av_frame_free(&vb_frame->frame);
+ free(vb_frame);
+}
+
+static bool
+sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) {
+ bool previous_skipped;
+ bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped);
if (!ok) {
- av_frame_free(&vb->pending_frame);
- av_frame_free(&vb->tmp_frame);
return false;
}
- // there is initially no frame, so consider it has already been consumed
- vb->pending_frame_consumed = true;
-
+ vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata);
return true;
}
-void
-video_buffer_destroy(struct video_buffer *vb) {
- sc_mutex_destroy(&vb->mutex);
- av_frame_free(&vb->pending_frame);
- av_frame_free(&vb->tmp_frame);
-}
+static int
+run_buffering(void *data) {
+ struct sc_video_buffer *vb = data;
-static inline void
-swap_frames(AVFrame **lhs, AVFrame **rhs) {
- AVFrame *tmp = *lhs;
- *lhs = *rhs;
- *rhs = tmp;
+ assert(vb->buffering_time > 0);
+
+ for (;;) {
+ sc_mutex_lock(&vb->b.mutex);
+
+ while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) {
+ sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex);
+ }
+
+ if (vb->b.stopped) {
+ sc_mutex_unlock(&vb->b.mutex);
+ goto stopped;
+ }
+
+ struct sc_video_buffer_frame *vb_frame;
+ sc_queue_take(&vb->b.queue, next, &vb_frame);
+
+ sc_tick max_deadline = sc_tick_now() + vb->buffering_time;
+ // PTS (written by the server) are expressed in microseconds
+ sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts);
+
+ bool timed_out = false;
+ while (!vb->b.stopped && !timed_out) {
+ sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts)
+ + vb->buffering_time;
+ if (deadline > max_deadline) {
+ deadline = max_deadline;
+ }
+
+ timed_out =
+ !sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline);
+ }
+
+ if (vb->b.stopped) {
+ sc_video_buffer_frame_delete(vb_frame);
+ sc_mutex_unlock(&vb->b.mutex);
+ goto stopped;
+ }
+
+ sc_mutex_unlock(&vb->b.mutex);
+
+#ifndef SC_BUFFERING_NDEBUG
+ LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
+ pts, vb_frame->push_date, sc_tick_now());
+#endif
+
+ sc_video_buffer_offer(vb, vb_frame->frame);
+
+ sc_video_buffer_frame_delete(vb_frame);
+ }
+
+stopped:
+ // Flush queue
+ while (!sc_queue_is_empty(&vb->b.queue)) {
+ struct sc_video_buffer_frame *vb_frame;
+ sc_queue_take(&vb->b.queue, next, &vb_frame);
+ sc_video_buffer_frame_delete(vb_frame);
+ }
+
+ LOGD("Buffering thread ended");
+
+ return 0;
}
bool
-video_buffer_push(struct video_buffer *vb, const AVFrame *frame,
- bool *previous_frame_skipped) {
- sc_mutex_lock(&vb->mutex);
-
- // Use a temporary frame to preserve pending_frame in case of error.
- // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand.
- int r = av_frame_ref(vb->tmp_frame, frame);
- if (r) {
- LOGE("Could not ref frame: %d", r);
+sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
+ const struct sc_video_buffer_callbacks *cbs,
+ void *cbs_userdata) {
+ bool ok = sc_frame_buffer_init(&vb->fb);
+ if (!ok) {
return false;
}
- // Now that av_frame_ref() succeeded, we can replace the previous
- // pending_frame
- swap_frames(&vb->pending_frame, &vb->tmp_frame);
- av_frame_unref(vb->tmp_frame);
+ assert(buffering_time >= 0);
+ if (buffering_time) {
+ ok = sc_mutex_init(&vb->b.mutex);
+ if (!ok) {
+ LOGC("Could not create mutex");
+ sc_frame_buffer_destroy(&vb->fb);
+ return false;
+ }
- if (previous_frame_skipped) {
- *previous_frame_skipped = !vb->pending_frame_consumed;
+ ok = sc_cond_init(&vb->b.queue_cond);
+ if (!ok) {
+ LOGC("Could not create cond");
+ sc_mutex_destroy(&vb->b.mutex);
+ sc_frame_buffer_destroy(&vb->fb);
+ return false;
+ }
+
+ ok = sc_cond_init(&vb->b.wait_cond);
+ if (!ok) {
+ LOGC("Could not create wait cond");
+ sc_cond_destroy(&vb->b.queue_cond);
+ sc_mutex_destroy(&vb->b.mutex);
+ sc_frame_buffer_destroy(&vb->fb);
+ return false;
+ }
+
+ sc_clock_init(&vb->b.clock);
+ sc_queue_init(&vb->b.queue);
}
- vb->pending_frame_consumed = false;
- sc_mutex_unlock(&vb->mutex);
+ assert(cbs);
+ assert(cbs->on_new_frame);
+
+ vb->buffering_time = buffering_time;
+ vb->cbs = cbs;
+ vb->cbs_userdata = cbs_userdata;
+ return true;
+}
+
+bool
+sc_video_buffer_start(struct sc_video_buffer *vb) {
+ if (vb->buffering_time) {
+ bool ok =
+ sc_thread_create(&vb->b.thread, run_buffering, "buffering", vb);
+ if (!ok) {
+ LOGE("Could not start buffering thread");
+ return false;
+ }
+ }
return true;
}
void
-video_buffer_consume(struct video_buffer *vb, AVFrame *dst) {
- sc_mutex_lock(&vb->mutex);
- assert(!vb->pending_frame_consumed);
- vb->pending_frame_consumed = true;
-
- av_frame_move_ref(dst, vb->pending_frame);
- // av_frame_move_ref() resets its source frame, so no need to call
- // av_frame_unref()
-
- sc_mutex_unlock(&vb->mutex);
+sc_video_buffer_stop(struct sc_video_buffer *vb) {
+ if (vb->buffering_time) {
+ sc_mutex_lock(&vb->b.mutex);
+ vb->b.stopped = true;
+ sc_cond_signal(&vb->b.queue_cond);
+ sc_cond_signal(&vb->b.wait_cond);
+ sc_mutex_unlock(&vb->b.mutex);
+ }
+}
+
+void
+sc_video_buffer_join(struct sc_video_buffer *vb) {
+ if (vb->buffering_time) {
+ sc_thread_join(&vb->b.thread, NULL);
+ }
+}
+
+void
+sc_video_buffer_destroy(struct sc_video_buffer *vb) {
+ sc_frame_buffer_destroy(&vb->fb);
+ if (vb->buffering_time) {
+ sc_cond_destroy(&vb->b.wait_cond);
+ sc_cond_destroy(&vb->b.queue_cond);
+ sc_mutex_destroy(&vb->b.mutex);
+ }
+}
+
+bool
+sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) {
+ if (!vb->buffering_time) {
+ // No buffering
+ return sc_video_buffer_offer(vb, frame);
+ }
+
+ sc_mutex_lock(&vb->b.mutex);
+
+ sc_tick pts = SC_TICK_FROM_US(frame->pts);
+ sc_clock_update(&vb->b.clock, sc_tick_now(), pts);
+ sc_cond_signal(&vb->b.wait_cond);
+
+ if (vb->b.clock.count == 1) {
+ sc_mutex_unlock(&vb->b.mutex);
+ // First frame, offer it immediately, for two reasons:
+ // - not to delay the opening of the scrcpy window
+ // - the buffering estimation needs at least two clock points, so it
+ // could not handle the first frame
+ return sc_video_buffer_offer(vb, frame);
+ }
+
+ struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame);
+ if (!vb_frame) {
+ sc_mutex_unlock(&vb->b.mutex);
+ LOGE("Could not allocate frame");
+ return false;
+ }
+
+#ifndef SC_BUFFERING_NDEBUG
+ vb_frame->push_date = sc_tick_now();
+#endif
+ sc_queue_push(&vb->b.queue, next, vb_frame);
+ sc_cond_signal(&vb->b.queue_cond);
+
+ sc_mutex_unlock(&vb->b.mutex);
+
+ return true;
+}
+
+void
+sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) {
+ sc_frame_buffer_consume(&vb->fb, dst);
}
diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h
index b9478f4c..48777703 100644
--- a/app/src/video_buffer.h
+++ b/app/src/video_buffer.h
@@ -1,50 +1,76 @@
-#ifndef VIDEO_BUFFER_H
-#define VIDEO_BUFFER_H
+#ifndef SC_VIDEO_BUFFER_H
+#define SC_VIDEO_BUFFER_H
#include "common.h"
#include
-#include "fps_counter.h"
+#include "clock.h"
+#include "frame_buffer.h"
+#include "util/queue.h"
#include "util/thread.h"
+#include "util/tick.h"
// forward declarations
typedef struct AVFrame AVFrame;
-/**
- * A video buffer holds 1 pending frame, which is the last frame received from
- * the producer (typically, the decoder).
- *
- * If a pending frame has not been consumed when the producer pushes a new
- * frame, then it is lost. The intent is to always provide access to the very
- * last frame to minimize latency.
- *
- * The producer and the consumer typically do not live in the same thread.
- * That's the reason why the callback on_frame_available() does not provide the
- * frame as parameter: the consumer might post an event to its own thread to
- * retrieve the pending frame from there, and that frame may have changed since
- * the callback if producer pushed a new one in between.
- */
+struct sc_video_buffer_frame {
+ AVFrame *frame;
+ struct sc_video_buffer_frame *next;
+#ifndef NDEBUG
+ sc_tick push_date;
+#endif
+};
-struct video_buffer {
- AVFrame *pending_frame;
- AVFrame *tmp_frame; // To preserve the pending frame on error
+struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame);
- sc_mutex mutex;
+struct sc_video_buffer {
+ struct sc_frame_buffer fb;
- bool pending_frame_consumed;
+ sc_tick buffering_time;
+
+ // only if buffering_time > 0
+ struct {
+ sc_thread thread;
+ sc_mutex mutex;
+ sc_cond queue_cond;
+ sc_cond wait_cond;
+
+ struct sc_clock clock;
+ struct sc_video_buffer_frame_queue queue;
+ bool stopped;
+ } b; // buffering
+
+ const struct sc_video_buffer_callbacks *cbs;
+ void *cbs_userdata;
+};
+
+struct sc_video_buffer_callbacks {
+ void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped,
+ void *userdata);
};
bool
-video_buffer_init(struct video_buffer *vb);
-
-void
-video_buffer_destroy(struct video_buffer *vb);
+sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
+ const struct sc_video_buffer_callbacks *cbs,
+ void *cbs_userdata);
bool
-video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped);
+sc_video_buffer_start(struct sc_video_buffer *vb);
void
-video_buffer_consume(struct video_buffer *vb, AVFrame *dst);
+sc_video_buffer_stop(struct sc_video_buffer *vb);
+
+void
+sc_video_buffer_join(struct sc_video_buffer *vb);
+
+void
+sc_video_buffer_destroy(struct sc_video_buffer *vb);
+
+bool
+sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame);
+
+void
+sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst);
#endif
diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c
index 94740a9a..05bacbf8 100644
--- a/app/tests/test_cli.c
+++ b/app/tests/test_cli.c
@@ -4,11 +4,11 @@
#include
#include "cli.h"
-#include "scrcpy.h"
+#include "options.h"
static void test_flag_version(void) {
struct scrcpy_cli_args args = {
- .opts = SCRCPY_OPTIONS_DEFAULT,
+ .opts = scrcpy_options_default,
.help = false,
.version = false,
};
@@ -23,7 +23,7 @@ static void test_flag_version(void) {
static void test_flag_help(void) {
struct scrcpy_cli_args args = {
- .opts = SCRCPY_OPTIONS_DEFAULT,
+ .opts = scrcpy_options_default,
.help = false,
.version = false,
};
@@ -38,7 +38,7 @@ static void test_flag_help(void) {
static void test_options(void) {
struct scrcpy_cli_args args = {
- .opts = SCRCPY_OPTIONS_DEFAULT,
+ .opts = scrcpy_options_default,
.help = false,
.version = false,
};
@@ -100,7 +100,7 @@ static void test_options(void) {
static void test_options2(void) {
struct scrcpy_cli_args args = {
- .opts = SCRCPY_OPTIONS_DEFAULT,
+ .opts = scrcpy_options_default,
.help = false,
.version = false,
};
diff --git a/app/tests/test_clock.c b/app/tests/test_clock.c
new file mode 100644
index 00000000..a88d5800
--- /dev/null
+++ b/app/tests/test_clock.c
@@ -0,0 +1,79 @@
+#include "common.h"
+
+#include
+
+#include "clock.h"
+
+void test_small_rolling_sum(void) {
+ struct sc_clock clock;
+ sc_clock_init(&clock);
+
+ assert(clock.count == 0);
+ assert(clock.left_sum.system == 0);
+ assert(clock.left_sum.stream == 0);
+ assert(clock.right_sum.system == 0);
+ assert(clock.right_sum.stream == 0);
+
+ sc_clock_update(&clock, 2, 3);
+ assert(clock.count == 1);
+ assert(clock.left_sum.system == 0);
+ assert(clock.left_sum.stream == 0);
+ assert(clock.right_sum.system == 2);
+ assert(clock.right_sum.stream == 3);
+
+ sc_clock_update(&clock, 10, 20);
+ assert(clock.count == 2);
+ assert(clock.left_sum.system == 2);
+ assert(clock.left_sum.stream == 3);
+ assert(clock.right_sum.system == 10);
+ assert(clock.right_sum.stream == 20);
+
+ sc_clock_update(&clock, 40, 80);
+ assert(clock.count == 3);
+ assert(clock.left_sum.system == 2);
+ assert(clock.left_sum.stream == 3);
+ assert(clock.right_sum.system == 50);
+ assert(clock.right_sum.stream == 100);
+
+ sc_clock_update(&clock, 400, 800);
+ assert(clock.count == 4);
+ assert(clock.left_sum.system == 12);
+ assert(clock.left_sum.stream == 23);
+ assert(clock.right_sum.system == 440);
+ assert(clock.right_sum.stream == 880);
+}
+
+void test_large_rolling_sum(void) {
+ const unsigned half_range = SC_CLOCK_RANGE / 2;
+
+ struct sc_clock clock1;
+ sc_clock_init(&clock1);
+ for (unsigned i = 0; i < 5 * half_range; ++i) {
+ sc_clock_update(&clock1, i, 2 * i + 1);
+ }
+
+ struct sc_clock clock2;
+ sc_clock_init(&clock2);
+ for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) {
+ sc_clock_update(&clock2, i, 2 * i + 1);
+ }
+
+ assert(clock1.count == SC_CLOCK_RANGE);
+ assert(clock2.count == SC_CLOCK_RANGE);
+
+ // The values before the last SC_CLOCK_RANGE points in clock1 should have
+ // no impact
+ assert(clock1.left_sum.system == clock2.left_sum.system);
+ assert(clock1.left_sum.stream == clock2.left_sum.stream);
+ assert(clock1.right_sum.system == clock2.right_sum.system);
+ assert(clock1.right_sum.stream == clock2.right_sum.stream);
+}
+
+int main(int argc, char *argv[]) {
+ (void) argc;
+ (void) argv;
+
+ test_small_rolling_sum();
+ test_large_rolling_sum();
+ return 0;
+};
diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c
index fcbafc62..d8b2b4ec 100644
--- a/app/tests/test_queue.c
+++ b/app/tests/test_queue.c
@@ -10,28 +10,28 @@ struct foo {
};
static void test_queue(void) {
- struct my_queue QUEUE(struct foo) queue;
- queue_init(&queue);
+ struct my_queue SC_QUEUE(struct foo) queue;
+ sc_queue_init(&queue);
- assert(queue_is_empty(&queue));
+ assert(sc_queue_is_empty(&queue));
struct foo v1 = { .value = 42 };
struct foo v2 = { .value = 27 };
- queue_push(&queue, next, &v1);
- queue_push(&queue, next, &v2);
+ sc_queue_push(&queue, next, &v1);
+ sc_queue_push(&queue, next, &v2);
struct foo *foo;
- assert(!queue_is_empty(&queue));
- queue_take(&queue, next, &foo);
+ assert(!sc_queue_is_empty(&queue));
+ sc_queue_take(&queue, next, &foo);
assert(foo->value == 42);
- assert(!queue_is_empty(&queue));
- queue_take(&queue, next, &foo);
+ assert(!sc_queue_is_empty(&queue));
+ sc_queue_take(&queue, next, &foo);
assert(foo->value == 27);
- assert(queue_is_empty(&queue));
+ assert(sc_queue_is_empty(&queue));
}
int main(int argc, char *argv[]) {
diff --git a/app/tests/test_str.c b/app/tests/test_str.c
new file mode 100644
index 00000000..2b030885
--- /dev/null
+++ b/app/tests/test_str.c
@@ -0,0 +1,360 @@
+#include "common.h"
+
+#include
+#include
+#include
+#include
+
+#include "util/str.h"
+
+static void test_strncpy_simple(void) {
+ char s[] = "xxxxxxxxxx";
+ size_t w = sc_strncpy(s, "abcdef", sizeof(s));
+
+ // returns strlen of copied string
+ assert(w == 6);
+
+ // is nul-terminated
+ assert(s[6] == '\0');
+
+ // does not write useless bytes
+ assert(s[7] == 'x');
+
+ // copies the content as expected
+ assert(!strcmp("abcdef", s));
+}
+
+static void test_strncpy_just_fit(void) {
+ char s[] = "xxxxxx";
+ size_t w = sc_strncpy(s, "abcdef", sizeof(s));
+
+ // returns strlen of copied string
+ assert(w == 6);
+
+ // is nul-terminated
+ assert(s[6] == '\0');
+
+ // copies the content as expected
+ assert(!strcmp("abcdef", s));
+}
+
+static void test_strncpy_truncated(void) {
+ char s[] = "xxx";
+ size_t w = sc_strncpy(s, "abcdef", sizeof(s));
+
+ // returns 'n' (sizeof(s))
+ assert(w == 4);
+
+ // is nul-terminated
+ assert(s[3] == '\0');
+
+ // copies the content as expected
+ assert(!strncmp("abcdef", s, 3));
+}
+
+static void test_join_simple(void) {
+ const char *const tokens[] = { "abc", "de", "fghi", NULL };
+ char s[] = "xxxxxxxxxxxxxx";
+ size_t w = sc_str_join(s, tokens, ' ', sizeof(s));
+
+ // returns strlen of concatenation
+ assert(w == 11);
+
+ // is nul-terminated
+ assert(s[11] == '\0');
+
+ // does not write useless bytes
+ assert(s[12] == 'x');
+
+ // copies the content as expected
+ assert(!strcmp("abc de fghi", s));
+}
+
+static void test_join_just_fit(void) {
+ const char *const tokens[] = { "abc", "de", "fghi", NULL };
+ char s[] = "xxxxxxxxxxx";
+ size_t w = sc_str_join(s, tokens, ' ', sizeof(s));
+
+ // returns strlen of concatenation
+ assert(w == 11);
+
+ // is nul-terminated
+ assert(s[11] == '\0');
+
+ // copies the content as expected
+ assert(!strcmp("abc de fghi", s));
+}
+
+static void test_join_truncated_in_token(void) {
+ const char *const tokens[] = { "abc", "de", "fghi", NULL };
+ char s[] = "xxxxx";
+ size_t w = sc_str_join(s, tokens, ' ', sizeof(s));
+
+ // returns 'n' (sizeof(s))
+ assert(w == 6);
+
+ // is nul-terminated
+ assert(s[5] == '\0');
+
+ // copies the content as expected
+ assert(!strcmp("abc d", s));
+}
+
+static void test_join_truncated_before_sep(void) {
+ const char *const tokens[] = { "abc", "de", "fghi", NULL };
+ char s[] = "xxxxxx";
+ size_t w = sc_str_join(s, tokens, ' ', sizeof(s));
+
+ // returns 'n' (sizeof(s))
+ assert(w == 7);
+
+ // is nul-terminated
+ assert(s[6] == '\0');
+
+ // copies the content as expected
+ assert(!strcmp("abc de", s));
+}
+
+static void test_join_truncated_after_sep(void) {
+ const char *const tokens[] = { "abc", "de", "fghi", NULL };
+ char s[] = "xxxxxxx";
+ size_t w = sc_str_join(s, tokens, ' ', sizeof(s));
+
+ // returns 'n' (sizeof(s))
+ assert(w == 8);
+
+ // is nul-terminated
+ assert(s[7] == '\0');
+
+ // copies the content as expected
+ assert(!strcmp("abc de ", s));
+}
+
+static void test_quote(void) {
+ const char *s = "abcde";
+ char *out = sc_str_quote(s);
+
+ // add '"' at the beginning and the end
+ assert(!strcmp("\"abcde\"", out));
+
+ free(out);
+}
+
+static void test_utf8_truncate(void) {
+ const char *s = "aÉbÔc";
+ assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
+
+ size_t count;
+
+ count = sc_str_utf8_truncation_index(s, 1);
+ assert(count == 1);
+
+ count = sc_str_utf8_truncation_index(s, 2);
+ assert(count == 1); // É is 2 bytes-wide
+
+ count = sc_str_utf8_truncation_index(s, 3);
+ assert(count == 3);
+
+ count = sc_str_utf8_truncation_index(s, 4);
+ assert(count == 4);
+
+ count = sc_str_utf8_truncation_index(s, 5);
+ assert(count == 4); // Ô is 2 bytes-wide
+
+ count = sc_str_utf8_truncation_index(s, 6);
+ assert(count == 6);
+
+ count = sc_str_utf8_truncation_index(s, 7);
+ assert(count == 7);
+
+ count = sc_str_utf8_truncation_index(s, 8);
+ assert(count == 7); // no more chars
+}
+
+static void test_parse_integer(void) {
+ long value;
+ bool ok = sc_str_parse_integer("1234", &value);
+ assert(ok);
+ assert(value == 1234);
+
+ ok = sc_str_parse_integer("-1234", &value);
+ assert(ok);
+ assert(value == -1234);
+
+ ok = sc_str_parse_integer("1234k", &value);
+ assert(!ok);
+
+ ok = sc_str_parse_integer("123456789876543212345678987654321", &value);
+ assert(!ok); // out-of-range
+}
+
+static void test_parse_integers(void) {
+ long values[5];
+
+ size_t count = sc_str_parse_integers("1234", ':', 5, values);
+ assert(count == 1);
+ assert(values[0] == 1234);
+
+ count = sc_str_parse_integers("1234:5678", ':', 5, values);
+ assert(count == 2);
+ assert(values[0] == 1234);
+ assert(values[1] == 5678);
+
+ count = sc_str_parse_integers("1234:5678", ':', 2, values);
+ assert(count == 2);
+ assert(values[0] == 1234);
+ assert(values[1] == 5678);
+
+ count = sc_str_parse_integers("1234:-5678", ':', 2, values);
+ assert(count == 2);
+ assert(values[0] == 1234);
+ assert(values[1] == -5678);
+
+ count = sc_str_parse_integers("1:2:3:4:5", ':', 5, values);
+ assert(count == 5);
+ assert(values[0] == 1);
+ assert(values[1] == 2);
+ assert(values[2] == 3);
+ assert(values[3] == 4);
+ assert(values[4] == 5);
+
+ count = sc_str_parse_integers("1234:5678", ':', 1, values);
+ assert(count == 0); // max_items == 1
+
+ count = sc_str_parse_integers("1:2:3:4:5", ':', 3, values);
+ assert(count == 0); // max_items == 3
+
+ count = sc_str_parse_integers(":1234", ':', 5, values);
+ assert(count == 0); // invalid
+
+ count = sc_str_parse_integers("1234:", ':', 5, values);
+ assert(count == 0); // invalid
+
+ count = sc_str_parse_integers("1234:", ':', 1, values);
+ assert(count == 0); // invalid, even when max_items == 1
+
+ count = sc_str_parse_integers("1234::5678", ':', 5, values);
+ assert(count == 0); // invalid
+}
+
+static void test_parse_integer_with_suffix(void) {
+ long value;
+ bool ok = sc_str_parse_integer_with_suffix("1234", &value);
+ assert(ok);
+ assert(value == 1234);
+
+ ok = sc_str_parse_integer_with_suffix("-1234", &value);
+ assert(ok);
+ assert(value == -1234);
+
+ ok = sc_str_parse_integer_with_suffix("1234k", &value);
+ assert(ok);
+ assert(value == 1234000);
+
+ ok = sc_str_parse_integer_with_suffix("1234m", &value);
+ assert(ok);
+ assert(value == 1234000000);
+
+ ok = sc_str_parse_integer_with_suffix("-1234k", &value);
+ assert(ok);
+ assert(value == -1234000);
+
+ ok = sc_str_parse_integer_with_suffix("-1234m", &value);
+ assert(ok);
+ assert(value == -1234000000);
+
+ ok = sc_str_parse_integer_with_suffix("123456789876543212345678987654321", &value);
+ assert(!ok); // out-of-range
+
+ char buf[32];
+
+ sprintf(buf, "%ldk", LONG_MAX / 2000);
+ ok = sc_str_parse_integer_with_suffix(buf, &value);
+ assert(ok);
+ assert(value == LONG_MAX / 2000 * 1000);
+
+ sprintf(buf, "%ldm", LONG_MAX / 2000);
+ ok = sc_str_parse_integer_with_suffix(buf, &value);
+ assert(!ok);
+
+ sprintf(buf, "%ldk", LONG_MIN / 2000);
+ ok = sc_str_parse_integer_with_suffix(buf, &value);
+ assert(ok);
+ assert(value == LONG_MIN / 2000 * 1000);
+
+ sprintf(buf, "%ldm", LONG_MIN / 2000);
+ ok = sc_str_parse_integer_with_suffix(buf, &value);
+ assert(!ok);
+}
+
+static void test_strlist_contains(void) {
+ assert(sc_str_list_contains("a,bc,def", ',', "bc"));
+ assert(!sc_str_list_contains("a,bc,def", ',', "b"));
+ assert(sc_str_list_contains("", ',', ""));
+ assert(sc_str_list_contains("abc,", ',', ""));
+ assert(sc_str_list_contains(",abc", ',', ""));
+ assert(sc_str_list_contains("abc,,def", ',', ""));
+ assert(!sc_str_list_contains("abc", ',', ""));
+ assert(sc_str_list_contains(",,|x", '|', ",,"));
+ assert(sc_str_list_contains("xyz", '\0', "xyz"));
+}
+
+static void test_wrap_lines(void) {
+ const char *s = "This is a text to test line wrapping. The lines must be "
+ "wrapped at a space or a line break.\n"
+ "\n"
+ "This rectangle must remains a rectangle because it is "
+ "drawn in lines having lengths lower than the specified "
+ "number of columns:\n"
+ " +----+\n"
+ " | |\n"
+ " +----+\n";
+
+ // |---- 1 1 2 2|
+ // |0 5 0 5 0 3| <-- 24 columns
+ const char *expected = " This is a text to\n"
+ " test line wrapping.\n"
+ " The lines must be\n"
+ " wrapped at a space\n"
+ " or a line break.\n"
+ " \n"
+ " This rectangle must\n"
+ " remains a rectangle\n"
+ " because it is drawn\n"
+ " in lines having\n"
+ " lengths lower than\n"
+ " the specified number\n"
+ " of columns:\n"
+ " +----+\n"
+ " | |\n"
+ " +----+\n";
+
+ char *formatted = sc_str_wrap_lines(s, 24, 4);
+ assert(formatted);
+
+ assert(!strcmp(formatted, expected));
+
+ free(formatted);
+}
+
+int main(int argc, char *argv[]) {
+ (void) argc;
+ (void) argv;
+
+ test_strncpy_simple();
+ test_strncpy_just_fit();
+ test_strncpy_truncated();
+ test_join_simple();
+ test_join_just_fit();
+ test_join_truncated_in_token();
+ test_join_truncated_before_sep();
+ test_join_truncated_after_sep();
+ test_quote();
+ test_utf8_truncate();
+ test_parse_integer();
+ test_parse_integers();
+ test_parse_integer_with_suffix();
+ test_strlist_contains();
+ test_wrap_lines();
+ return 0;
+}
diff --git a/app/tests/test_strbuf.c b/app/tests/test_strbuf.c
new file mode 100644
index 00000000..97417677
--- /dev/null
+++ b/app/tests/test_strbuf.c
@@ -0,0 +1,47 @@
+#include "common.h"
+
+#include
+#include
+#include
+
+#include "util/strbuf.h"
+
+static void test_strbuf_simple(void) {
+ struct sc_strbuf buf;
+ bool ok = sc_strbuf_init(&buf, 10);
+ assert(ok);
+
+ ok = sc_strbuf_append_staticstr(&buf, "Hello");
+ assert(ok);
+
+ ok = sc_strbuf_append_char(&buf, ' ');
+ assert(ok);
+
+ ok = sc_strbuf_append_staticstr(&buf, "world");
+ assert(ok);
+
+ ok = sc_strbuf_append_staticstr(&buf, "!\n");
+ assert(ok);
+
+ ok = sc_strbuf_append_staticstr(&buf, "This is a test");
+ assert(ok);
+
+ ok = sc_strbuf_append_n(&buf, '.', 3);
+ assert(ok);
+
+ assert(!strcmp(buf.s, "Hello world!\nThis is a test..."));
+
+ sc_strbuf_shrink(&buf);
+ assert(buf.len == buf.cap);
+ assert(!strcmp(buf.s, "Hello world!\nThis is a test..."));
+
+ free(buf.s);
+}
+
+int main(int argc, char *argv[]) {
+ (void) argc;
+ (void) argv;
+
+ test_strbuf_simple();
+ return 0;
+}
diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c
deleted file mode 100644
index dfd99658..00000000
--- a/app/tests/test_strutil.c
+++ /dev/null
@@ -1,321 +0,0 @@
-#include "common.h"
-
-#include
-#include
-#include
-#include
-
-#include "util/str_util.h"
-
-static void test_xstrncpy_simple(void) {
- char s[] = "xxxxxxxxxx";
- size_t w = xstrncpy(s, "abcdef", sizeof(s));
-
- // returns strlen of copied string
- assert(w == 6);
-
- // is nul-terminated
- assert(s[6] == '\0');
-
- // does not write useless bytes
- assert(s[7] == 'x');
-
- // copies the content as expected
- assert(!strcmp("abcdef", s));
-}
-
-static void test_xstrncpy_just_fit(void) {
- char s[] = "xxxxxx";
- size_t w = xstrncpy(s, "abcdef", sizeof(s));
-
- // returns strlen of copied string
- assert(w == 6);
-
- // is nul-terminated
- assert(s[6] == '\0');
-
- // copies the content as expected
- assert(!strcmp("abcdef", s));
-}
-
-static void test_xstrncpy_truncated(void) {
- char s[] = "xxx";
- size_t w = xstrncpy(s, "abcdef", sizeof(s));
-
- // returns 'n' (sizeof(s))
- assert(w == 4);
-
- // is nul-terminated
- assert(s[3] == '\0');
-
- // copies the content as expected
- assert(!strncmp("abcdef", s, 3));
-}
-
-static void test_xstrjoin_simple(void) {
- const char *const tokens[] = { "abc", "de", "fghi", NULL };
- char s[] = "xxxxxxxxxxxxxx";
- size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
-
- // returns strlen of concatenation
- assert(w == 11);
-
- // is nul-terminated
- assert(s[11] == '\0');
-
- // does not write useless bytes
- assert(s[12] == 'x');
-
- // copies the content as expected
- assert(!strcmp("abc de fghi", s));
-}
-
-static void test_xstrjoin_just_fit(void) {
- const char *const tokens[] = { "abc", "de", "fghi", NULL };
- char s[] = "xxxxxxxxxxx";
- size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
-
- // returns strlen of concatenation
- assert(w == 11);
-
- // is nul-terminated
- assert(s[11] == '\0');
-
- // copies the content as expected
- assert(!strcmp("abc de fghi", s));
-}
-
-static void test_xstrjoin_truncated_in_token(void) {
- const char *const tokens[] = { "abc", "de", "fghi", NULL };
- char s[] = "xxxxx";
- size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
-
- // returns 'n' (sizeof(s))
- assert(w == 6);
-
- // is nul-terminated
- assert(s[5] == '\0');
-
- // copies the content as expected
- assert(!strcmp("abc d", s));
-}
-
-static void test_xstrjoin_truncated_before_sep(void) {
- const char *const tokens[] = { "abc", "de", "fghi", NULL };
- char s[] = "xxxxxx";
- size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
-
- // returns 'n' (sizeof(s))
- assert(w == 7);
-
- // is nul-terminated
- assert(s[6] == '\0');
-
- // copies the content as expected
- assert(!strcmp("abc de", s));
-}
-
-static void test_xstrjoin_truncated_after_sep(void) {
- const char *const tokens[] = { "abc", "de", "fghi", NULL };
- char s[] = "xxxxxxx";
- size_t w = xstrjoin(s, tokens, ' ', sizeof(s));
-
- // returns 'n' (sizeof(s))
- assert(w == 8);
-
- // is nul-terminated
- assert(s[7] == '\0');
-
- // copies the content as expected
- assert(!strcmp("abc de ", s));
-}
-
-static void test_strquote(void) {
- const char *s = "abcde";
- char *out = strquote(s);
-
- // add '"' at the beginning and the end
- assert(!strcmp("\"abcde\"", out));
-
- free(out);
-}
-
-static void test_utf8_truncate(void) {
- const char *s = "aÉbÔc";
- assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
-
- size_t count;
-
- count = utf8_truncation_index(s, 1);
- assert(count == 1);
-
- count = utf8_truncation_index(s, 2);
- assert(count == 1); // É is 2 bytes-wide
-
- count = utf8_truncation_index(s, 3);
- assert(count == 3);
-
- count = utf8_truncation_index(s, 4);
- assert(count == 4);
-
- count = utf8_truncation_index(s, 5);
- assert(count == 4); // Ô is 2 bytes-wide
-
- count = utf8_truncation_index(s, 6);
- assert(count == 6);
-
- count = utf8_truncation_index(s, 7);
- assert(count == 7);
-
- count = utf8_truncation_index(s, 8);
- assert(count == 7); // no more chars
-}
-
-static void test_parse_integer(void) {
- long value;
- bool ok = parse_integer("1234", &value);
- assert(ok);
- assert(value == 1234);
-
- ok = parse_integer("-1234", &value);
- assert(ok);
- assert(value == -1234);
-
- ok = parse_integer("1234k", &value);
- assert(!ok);
-
- ok = parse_integer("123456789876543212345678987654321", &value);
- assert(!ok); // out-of-range
-}
-
-static void test_parse_integers(void) {
- long values[5];
-
- size_t count = parse_integers("1234", ':', 5, values);
- assert(count == 1);
- assert(values[0] == 1234);
-
- count = parse_integers("1234:5678", ':', 5, values);
- assert(count == 2);
- assert(values[0] == 1234);
- assert(values[1] == 5678);
-
- count = parse_integers("1234:5678", ':', 2, values);
- assert(count == 2);
- assert(values[0] == 1234);
- assert(values[1] == 5678);
-
- count = parse_integers("1234:-5678", ':', 2, values);
- assert(count == 2);
- assert(values[0] == 1234);
- assert(values[1] == -5678);
-
- count = parse_integers("1:2:3:4:5", ':', 5, values);
- assert(count == 5);
- assert(values[0] == 1);
- assert(values[1] == 2);
- assert(values[2] == 3);
- assert(values[3] == 4);
- assert(values[4] == 5);
-
- count = parse_integers("1234:5678", ':', 1, values);
- assert(count == 0); // max_items == 1
-
- count = parse_integers("1:2:3:4:5", ':', 3, values);
- assert(count == 0); // max_items == 3
-
- count = parse_integers(":1234", ':', 5, values);
- assert(count == 0); // invalid
-
- count = parse_integers("1234:", ':', 5, values);
- assert(count == 0); // invalid
-
- count = parse_integers("1234:", ':', 1, values);
- assert(count == 0); // invalid, even when max_items == 1
-
- count = parse_integers("1234::5678", ':', 5, values);
- assert(count == 0); // invalid
-}
-
-static void test_parse_integer_with_suffix(void) {
- long value;
- bool ok = parse_integer_with_suffix("1234", &value);
- assert(ok);
- assert(value == 1234);
-
- ok = parse_integer_with_suffix("-1234", &value);
- assert(ok);
- assert(value == -1234);
-
- ok = parse_integer_with_suffix("1234k", &value);
- assert(ok);
- assert(value == 1234000);
-
- ok = parse_integer_with_suffix("1234m", &value);
- assert(ok);
- assert(value == 1234000000);
-
- ok = parse_integer_with_suffix("-1234k", &value);
- assert(ok);
- assert(value == -1234000);
-
- ok = parse_integer_with_suffix("-1234m", &value);
- assert(ok);
- assert(value == -1234000000);
-
- ok = parse_integer_with_suffix("123456789876543212345678987654321", &value);
- assert(!ok); // out-of-range
-
- char buf[32];
-
- sprintf(buf, "%ldk", LONG_MAX / 2000);
- ok = parse_integer_with_suffix(buf, &value);
- assert(ok);
- assert(value == LONG_MAX / 2000 * 1000);
-
- sprintf(buf, "%ldm", LONG_MAX / 2000);
- ok = parse_integer_with_suffix(buf, &value);
- assert(!ok);
-
- sprintf(buf, "%ldk", LONG_MIN / 2000);
- ok = parse_integer_with_suffix(buf, &value);
- assert(ok);
- assert(value == LONG_MIN / 2000 * 1000);
-
- sprintf(buf, "%ldm", LONG_MIN / 2000);
- ok = parse_integer_with_suffix(buf, &value);
- assert(!ok);
-}
-
-static void test_strlist_contains(void) {
- assert(strlist_contains("a,bc,def", ',', "bc"));
- assert(!strlist_contains("a,bc,def", ',', "b"));
- assert(strlist_contains("", ',', ""));
- assert(strlist_contains("abc,", ',', ""));
- assert(strlist_contains(",abc", ',', ""));
- assert(strlist_contains("abc,,def", ',', ""));
- assert(!strlist_contains("abc", ',', ""));
- assert(strlist_contains(",,|x", '|', ",,"));
- assert(strlist_contains("xyz", '\0', "xyz"));
-}
-
-int main(int argc, char *argv[]) {
- (void) argc;
- (void) argv;
-
- test_xstrncpy_simple();
- test_xstrncpy_just_fit();
- test_xstrncpy_truncated();
- test_xstrjoin_simple();
- test_xstrjoin_just_fit();
- test_xstrjoin_truncated_in_token();
- test_xstrjoin_truncated_before_sep();
- test_xstrjoin_truncated_after_sep();
- test_strquote();
- test_utf8_truncate();
- test_parse_integer();
- test_parse_integers();
- test_parse_integer_with_suffix();
- test_strlist_contains();
- return 0;
-}
diff --git a/build.gradle b/build.gradle
index c977c398..6ed7e4c6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath 'com.android.tools.build:gradle:7.0.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/config/android-checkstyle.gradle b/config/android-checkstyle.gradle
index f998530e..29c67b19 100644
--- a/config/android-checkstyle.gradle
+++ b/config/android-checkstyle.gradle
@@ -2,7 +2,7 @@ apply plugin: 'checkstyle'
check.dependsOn 'checkstyle'
checkstyle {
- toolVersion = '6.19'
+ toolVersion = '9.0.1'
}
task checkstyle(type: Checkstyle) {
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
index 812d060b..edda3919 100644
--- a/config/checkstyle/checkstyle.xml
+++ b/config/checkstyle/checkstyle.xml
@@ -37,6 +37,14 @@ page at http://checkstyle.sourceforge.net/config.html -->
+
+
+
+
+
+
+
+
@@ -72,13 +80,6 @@ page at http://checkstyle.sourceforge.net/config.html -->
-
-
-
-
-
-
-
@@ -152,26 +153,6 @@ page at http://checkstyle.sourceforge.net/config.html -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/cross_win32.txt b/cross_win32.txt
index 0d8a843a..4db17be7 100644
--- a/cross_win32.txt
+++ b/cross_win32.txt
@@ -17,4 +17,4 @@ endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev'
-prebuilt_sdl2 = 'SDL2-2.0.14/i686-w64-mingw32'
+prebuilt_sdl2 = 'SDL2-2.0.16/i686-w64-mingw32'
diff --git a/cross_win64.txt b/cross_win64.txt
index 6a39c391..d03f0272 100644
--- a/cross_win64.txt
+++ b/cross_win64.txt
@@ -17,4 +17,4 @@ endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev'
-prebuilt_sdl2 = 'SDL2-2.0.14/x86_64-w64-mingw32'
+prebuilt_sdl2 = 'SDL2-2.0.16/x86_64-w64-mingw32'
diff --git a/data/icon.png b/data/icon.png
new file mode 100644
index 00000000..b96a1aff
Binary files /dev/null and b/data/icon.png differ
diff --git a/data/icon.svg b/data/icon.svg
new file mode 100644
index 00000000..0ab92c2a
--- /dev/null
+++ b/data/icon.svg
@@ -0,0 +1,16 @@
+
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a4b44297..29e41345 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/install_release.sh b/install_release.sh
index 9158bdd4..e12b4469 100755
--- a/install_release.sh
+++ b/install_release.sh
@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
-PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18
-PREBUILT_SERVER_SHA256=641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3
+PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20
+PREBUILT_SERVER_SHA256=b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
diff --git a/meson.build b/meson.build
index 2d76f1e9..7a814212 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
project('scrcpy', 'c',
- version: '1.18',
+ version: '1.20',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',
diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile
index d75d0a5c..dced047c 100644
--- a/prebuilt-deps/Makefile
+++ b/prebuilt-deps/Makefile
@@ -30,11 +30,11 @@ prepare-ffmpeg-dev-win64:
ffmpeg-4.3.1-win64-dev
prepare-sdl2:
- @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.14-mingw.tar.gz \
- 405eaff3eb18f2e08fe669ef9e63bc9a8710b7d343756f238619761e9b60407d \
- SDL2-2.0.14
+ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.16-mingw.tar.gz \
+ 2bfe48628aa9635c12eac7d421907e291525de1d0b04b3bca4a5bd6e6c881a6f \
+ SDL2-2.0.16
prepare-adb:
- @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.2-windows.zip \
- d560cb8ded83ae04763b94632673481f14843a5969256569623cfeac82db4ba5 \
+ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \
+ 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \
platform-tools
diff --git a/release.mk b/release.mk
index 2a026135..05ddbe46 100644
--- a/release.mk
+++ b/release.mk
@@ -94,6 +94,7 @@ dist-win32: build-server build-win32
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
+ cp data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@@ -102,7 +103,7 @@ dist-win32: build-server build-win32
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
- cp prebuilt-deps/SDL2-2.0.14/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
+ cp prebuilt-deps/SDL2-2.0.16/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
@@ -110,6 +111,7 @@ dist-win64: build-server build-win64
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
+ cp data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
@@ -118,7 +120,7 @@ dist-win64: build-server build-win64
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
- cp prebuilt-deps/SDL2-2.0.14/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
+ cp prebuilt-deps/SDL2-2.0.16/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
diff --git a/run b/run
index 628c5c7e..fda3ea57 100755
--- a/run
+++ b/run
@@ -20,4 +20,6 @@ then
exit 1
fi
-SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@"
+SCRCPY_ICON_PATH="data/icon.png" \
+SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \
+"$BUILDDIR/app/scrcpy" "$@"
diff --git a/server/build.gradle b/server/build.gradle
index f088ba9d..52781a29 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -1,13 +1,13 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 30
+ compileSdkVersion 31
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
- targetSdkVersion 30
- versionCode 11800
- versionName "1.18"
+ targetSdkVersion 31
+ versionCode 12000
+ versionName "1.20"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -20,7 +20,7 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- testImplementation 'junit:junit:4.13'
+ testImplementation 'junit:junit:4.13.1'
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"
diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh
index 302d3aaa..61b42103 100755
--- a/server/build_without_gradle.sh
+++ b/server/build_without_gradle.sh
@@ -12,15 +12,17 @@
set -e
SCRCPY_DEBUG=false
-SCRCPY_VERSION_NAME=1.18
+SCRCPY_VERSION_NAME=1.20
-PLATFORM=${ANDROID_PLATFORM:-30}
-BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0}
+PLATFORM_VERSION=31
+PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION}
+BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0}
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
+ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS"
@@ -47,23 +49,40 @@ cd "$SERVER_DIR/src/main/aidl"
echo "Compiling java sources..."
cd ../java
-javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \
- -cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \
+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
echo "Dexing..."
cd "$CLASSES_DIR"
-"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
- --output "$BUILD_DIR/classes.dex" \
- android/view/*.class \
- android/content/*.class \
- com/genymobile/scrcpy/*.class \
- com/genymobile/scrcpy/wrappers/*.class
-echo "Archiving..."
-cd "$BUILD_DIR"
-jar cvf "$SERVER_BINARY" classes.dex
-rm -rf classes.dex classes
+if [[ $PLATFORM_VERSION -lt 31 ]]
+then
+ # use dx
+ "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
+ --output "$BUILD_DIR/classes.dex" \
+ android/view/*.class \
+ android/content/*.class \
+ com/genymobile/scrcpy/*.class \
+ com/genymobile/scrcpy/wrappers/*.class
+
+ echo "Archiving..."
+ cd "$BUILD_DIR"
+ jar cvf "$SERVER_BINARY" classes.dex
+ rm -rf classes.dex classes
+else
+ # use d8
+ "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \
+ --output "$BUILD_DIR/classes.zip" \
+ android/view/*.class \
+ android/content/*.class \
+ com/genymobile/scrcpy/*.class \
+ com/genymobile/scrcpy/wrappers/*.class
+
+ cd "$BUILD_DIR"
+ mv classes.zip "$SERVER_BINARY"
+ rm -rf classes
+fi
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java
index 92986241..45882bb9 100644
--- a/server/src/main/java/com/genymobile/scrcpy/Controller.java
+++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java
@@ -241,7 +241,7 @@ public class Controller {
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0,
- InputDevice.SOURCE_TOUCHSCREEN, 0);
+ InputDevice.SOURCE_MOUSE, 0);
return device.injectEvent(event);
}
diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
index 2f7109c5..f98c53d0 100644
--- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
+++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
@@ -56,17 +56,13 @@ public class ScreenEncoder implements Device.RotationListener {
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
-
- try {
- internalStreamScreen(device, fd);
- } catch (NullPointerException e) {
- // Retry with workarounds enabled:
- //
- //
- Ln.d("Applying workarounds to avoid NullPointerException");
+ if (Build.BRAND.equalsIgnoreCase("meizu")) {
+ //
+ //
Workarounds.fillAppInfo();
- internalStreamScreen(device, fd);
}
+
+ internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java
index 5b1e5f5e..7a19e6e5 100644
--- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java
+++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java
@@ -11,6 +11,7 @@ public class StatusBarManager {
private final IInterface manager;
private Method expandNotificationsPanelMethod;
+ private boolean expandNotificationPanelMethodCustomVersion;
private Method expandSettingsPanelMethod;
private boolean expandSettingsPanelMethodNewVersion = true;
private Method collapsePanelsMethod;
@@ -21,7 +22,13 @@ public class StatusBarManager {
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
if (expandNotificationsPanelMethod == null) {
- expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
+ try {
+ expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
+ } catch (NoSuchMethodException e) {
+ // Custom version for custom vendor ROM:
+ expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class);
+ expandNotificationPanelMethodCustomVersion = true;
+ }
}
return expandNotificationsPanelMethod;
}
@@ -50,7 +57,11 @@ public class StatusBarManager {
public void expandNotificationsPanel() {
try {
Method method = getExpandNotificationsPanelMethod();
- method.invoke(manager);
+ if (expandNotificationPanelMethodCustomVersion) {
+ method.invoke(manager, 0);
+ } else {
+ method.invoke(manager);
+ }
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java
index da568486..7f3d3f61 100644
--- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java
+++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java
@@ -2,7 +2,6 @@ package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
-
import org.junit.Assert;
import org.junit.Test;