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) + +scrcpy [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_. ![screenshot](assets/screenshot-debian-600.jpg) 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. Packaging status +### 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. + +![screenshot](assets/screenshot-debian-600.jpg) + +Ö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 + +Packaging status + +### Ö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) + +scrcpy 本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 ![screenshot](assets/screenshot-debian-600.jpg) -它专注于: +本应用专注于: - - **轻量** (原生,仅显示设备屏幕) - - **性能** (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._ Packaging status +### 概要 + + - 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] 键通常是指 WindowsCmd 键。 [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;