diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 29a2bf01..1c04da7f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,5 +21,9 @@ assignees: '' A clear and concise description of what the bug is. On errors, please provide the output of the console (and `adb logcat` if relevant). -Format them between code blocks (delimited by ```). + +``` +Please paste terminal output in a code block. +``` + Please do not post screenshots of your terminal, just post the content as text instead. diff --git a/.gitignore b/.gitignore index 59bc840d..2829d835 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ build/ /dist/ +/build-*/ +/build_*/ +/release-*/ .idea/ .gradle/ +/x/ +local.properties diff --git a/BUILD.md b/BUILD.md index 8b845583..87078b71 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,11 +2,43 @@ Here are the instructions to build _scrcpy_ (client and server). -You may want to build only the client: the server binary, which will be pushed -to the Android device, does not depend on your system and architecture. In that -case, use the [prebuilt server] (so you will not need Java or the Android SDK). -[prebuilt server]: #prebuilt-server +## Simple + +If you just want to install the latest release from `master`, follow this +simplified process. + +First, you need to install the required packages: + +```bash +# 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 +``` + +Then clone the repo and execute the installation script +([source](install_release.sh)): + +```bash +git clone https://github.com/Genymobile/scrcpy +cd scrcpy +./install_release.sh +``` + +When a new release is out, update the repo and reinstall: + +```bash +git pull +./install_release.sh +``` + +To uninstall: + +```bash +sudo ninja -Cbuild-auto uninstall +``` + ## Branches @@ -59,12 +91,11 @@ Install the required packages from your package manager. sudo apt install ffmpeg libsdl2-2.0-0 adb # client build dependencies -sudo apt install gcc git pkg-config meson ninja-build \ - libavcodec-dev libavformat-dev libavutil-dev \ - libsdl2-dev +sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev # server build dependencies -sudo apt install openjdk-8-jdk +sudo apt install openjdk-11-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install @@ -106,13 +137,13 @@ sudo apt install mingw-w64 mingw-w64-tools You also need the JDK to build the server: ```bash -sudo apt install openjdk-8-jdk +sudo apt install openjdk-11-jdk ``` Then generate the releases: ```bash -make -f Makefile.CrossWindows +./release.sh ``` It will generate win32 and win64 releases into `dist/`. @@ -173,12 +204,12 @@ brew install pkg-config meson ``` Additionally, if you want to build the server, install Java 8 from Caskroom, and -make it avaliable from the `PATH`: +make it available from the `PATH`: ```bash -brew tap caskroom/versions -brew cask install java8 -export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)" +brew tap homebrew/cask-versions +brew install adoptopenjdk/openjdk/adoptopenjdk11 +export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)" export PATH="$JAVA_HOME/bin:$PATH" ``` @@ -189,29 +220,44 @@ See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker). ## Common steps -If you want to build the server, install the [Android SDK] (_Android Studio_), -and set `ANDROID_HOME` to its directory. For example: - -[Android SDK]: https://developer.android.com/studio/index.html - -```bash -export ANDROID_HOME=~/android/sdk -``` - -If you don't want to build the server, use the [prebuilt server]. - -Clone the project: +**As a non-root user**, clone the project: ```bash git clone https://github.com/Genymobile/scrcpy cd scrcpy ``` + +### Build + +You may want to build only the client: the server binary, which will be pushed +to the Android device, does not depend on your system and architecture. In that +case, use the [prebuilt server] (so you will not need Java or the Android SDK). + +[prebuilt server]: #option-2-use-prebuilt-server + + +#### Option 1: Build everything from sources + +Install the [Android SDK] (_Android Studio_), and set `ANDROID_SDK_ROOT` to its +directory. For example: + +[Android SDK]: https://developer.android.com/studio/index.html + +```bash +# Linux +export ANDROID_SDK_ROOT=~/Android/Sdk +# Mac +export ANDROID_SDK_ROOT=~/Library/Android/sdk +# Windows +set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk +``` + Then, build: ```bash meson x --buildtype release --strip -Db_lto=true -ninja -Cx +ninja -Cx # DO NOT RUN AS ROOT ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja @@ -220,9 +266,27 @@ install` must be run as root)._ [ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a -### Run +#### Option 2: Use prebuilt server -To run without installing: + - [`scrcpy-server-v1.18`][direct-scrcpy-server] + _(SHA-256: 641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3)_ + +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 + +Download the prebuilt server somewhere, and specify its path during the Meson +configuration: + +```bash +meson x --buildtype release --strip -Db_lto=true \ + -Dprebuilt_server=/path/to/scrcpy-server +ninja -Cx # DO NOT RUN AS ROOT +``` + +The server only works with a matching client version (this server works with the +`master` branch). + + +### Run without installing: ```bash ./run x [options] @@ -237,32 +301,16 @@ After a successful build, you can install _scrcpy_ on the system: sudo ninja -Cx install # without sudo on Windows ``` -This installs two files: +This installs three files: - `/usr/local/bin/scrcpy` - `/usr/local/share/scrcpy/scrcpy-server` - -Just remove them to "uninstall" the application. + - `/usr/local/share/man/man1/scrcpy.1` You can then [run](README.md#run) _scrcpy_. - -## Prebuilt server - - - [`scrcpy-server-v1.12.1`][direct-scrcpy-server] - _(SHA-256: 63e569c8a1d0c1df31d48c4214871c479a601782945fed50c1e61167d78266ea)_ - -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-server-v1.12.1 - -Download the prebuilt server somewhere, and specify its path during the Meson -configuration: +### Uninstall ```bash -meson x --buildtype release --strip -Db_lto=true \ - -Dprebuilt_server=/path/to/scrcpy-server -ninja -Cx -sudo ninja -Cx install +sudo ninja -Cx uninstall # without sudo on Windows ``` - -The server only works with a matching client version (this server works with the -`master` branch). diff --git a/DEVELOP.md b/DEVELOP.md index 0258782f..d11f139e 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -211,7 +211,7 @@ There are two [frames][video_buffer] simultaneously in memory: - the **rendering** frame, rendered in a texture from the main thread. When a new decoded frame is available, the decoder _swaps_ the decoding and -rendering frame (with proper synchronization). Thus, it immediatly starts +rendering frame (with proper synchronization). Thus, it immediately starts to decode a new frame while the main thread renders the last one. If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw @@ -282,6 +282,15 @@ meson x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` +If your device runs Android 8 or below, set the `server_debugger_method` to +`old` in addition: + +```bash +meson x -Dserver_debugger=true -Dserver_debugger_method=old +# or, if x is already configured +meson configure x -Dserver_debugger=true -Dserver_debugger_method=old +``` + Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. diff --git a/FAQ.it.md b/FAQ.it.md new file mode 100644 index 00000000..5c5830ce --- /dev/null +++ b/FAQ.it.md @@ -0,0 +1,217 @@ +_Apri le [FAQ](FAQ.md) originali e sempre aggiornate._ + +# Domande Frequenti (FAQ) + +Questi sono i problemi più comuni riportati e i loro stati. + + +## Problemi di `adb` + +`scrcpy` esegue comandi `adb` per inizializzare la connessione con il dispositivo. Se `adb` fallisce, scrcpy non funzionerà. + +In questo caso sarà stampato questo errore: + +> ERROR: "adb push" returned with value 1 + +Questo solitamente non è un bug di _scrcpy_, ma un problema del tuo ambiente. + +Per trovare la causa, esegui: + +```bash +adb devices +``` + +### `adb` not found (`adb` non trovato) + +È necessario che `adb` sia accessibile dal tuo `PATH`. + +In Windows, la cartella corrente è nel tuo `PATH` e `adb.exe` è incluso nella release, perciò dovrebbe già essere pronto all'uso. + + +### Device unauthorized (Dispositivo non autorizzato) + +Controlla [stackoverflow][device-unauthorized] (in inglese). + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized + + +### Device not detected (Dispositivo non rilevato) + +> adb: error: failed to get feature set: no devices/emulators found + +Controlla di aver abilitato correttamente il [debug con adb][enable-adb] (link in inglese). + +Se il tuo dispositivo non è rilevato, potresti avere bisogno dei [driver][drivers] (link in inglese) (in Windows). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling +[drivers]: https://developer.android.com/studio/run/oem-usb.html + + +### Più dispositivi connessi + +Se più dispositivi sono connessi, riscontrerai questo errore: + +> adb: error: failed to get feature set: more than one device/emulator + +l'identificatore del tuo dispositivo deve essere fornito: + +```bash +scrcpy -s 01234567890abcdef +``` + +Notare che se il tuo dispositivo è connesso mediante TCP/IP, riscontrerai questo messaggio: + +> adb: error: more than one device/emulator +> ERROR: "adb reverse" returned with value 1 +> WARN: 'adb reverse' failed, fallback to 'adb forward' + +Questo è un problema atteso (a causa di un bug di una vecchia versione di Android, vedi [#5] (link in inglese)), ma in quel caso scrcpy ripiega su un metodo differente, il quale dovrebbe funzionare. + +[#5]: https://github.com/Genymobile/scrcpy/issues/5 + + +### Conflitti tra versioni di adb + +> adb server version (41) doesn't match this client (39); killing... + +L'errore compare quando usi più versioni di `adb` simultaneamente. Devi trovare il programma che sta utilizzando una versione differente di `adb` e utilizzare la stessa versione dappertutto. + +Puoi sovrascrivere i binari di `adb` nell'altro programma, oppure chiedere a _scrcpy_ di usare un binario specifico di `adb`, impostando la variabile d'ambiente `ADB`: + +```bash +set ADB=/path/to/your/adb +scrcpy +``` + + +### Device disconnected (Dispositivo disconnesso) + +Se _scrcpy_ si interrompe con l'avviso "Device disconnected", allora la connessione `adb` è stata chiusa. + +Prova con un altro cavo USB o inseriscilo in un'altra porta USB. Vedi [#281] (in inglese) e [#283] (in inglese). + +[#281]: https://github.com/Genymobile/scrcpy/issues/281 +[#283]: https://github.com/Genymobile/scrcpy/issues/283 + + + +## Problemi di controllo + +### Mouse e tastiera non funzionano + +Su alcuni dispositivi potresti dover abilitare un opzione che permette l'[input simulato][simulating input] (link in inglese). Nelle opzioni sviluppatore, abilita: + +> **Debug USB (Impostazioni di sicurezza)** +> _Permetti la concessione dei permessi e la simulazione degli input mediante il debug USB_ + + +[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +### I caratteri speciali non funzionano + +Iniettare del testo in input è [limitato ai caratteri ASCII][text-input] (link in inglese). Un trucco permette di iniettare dei [caratteri accentati][accented-characters] (link in inglese), ma questo è tutto. Vedi [#37] (link in inglese). + +[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode +[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters +[#37]: https://github.com/Genymobile/scrcpy/issues/37 + + +## Problemi del client + +### La qualità è bassa + +Se la definizione della finestra del tuo client è minore di quella del tuo dispositivo, allora potresti avere una bassa qualità di visualizzazione, specialmente individuabile nei testi (vedi [#40] (link in inglese)). + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + +Per migliorare la qualità di ridimensionamento (downscaling), il filtro trilineare è applicato automaticamente se il renderizzatore è OpenGL e se supporta la creazione di mipmap. + +In Windows, potresti voler forzare OpenGL: + +``` +scrcpy --render-driver=opengl +``` + +Potresti anche dover configurare il [comportamento di ridimensionamento][scaling behavior] (link in inglese): + +> `scrcpy.exe` > Propietà > Compatibilità > Modifica impostazioni DPI elevati > Esegui l'override del comportamento di ridimensionamento DPI elevati > Ridimensionamento eseguito per: _Applicazione_. + +[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 + + + +### Crash del compositore KWin + +In Plasma Desktop, il compositore è disabilitato mentre _scrcpy_ è in esecuzione. + +Come soluzione alternativa, [disattiva la "composizione dei blocchi"][kwin] (link in inglese). + + +[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 + + +## Crash + +### Eccezione + +Ci potrebbero essere molte ragioni. Una causa comune è che il codificatore hardware del tuo dispositivo non riesce a codificare alla definizione selezionata: + +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> android.media.MediaCodec$CodecException: Error 0xfffffc0e +> ... +> Exit due to uncaughtException in main thread: +> ERROR: Could not open video stream +> INFO: Initial texture: 1080x2336 +> ``` + +o + +> ``` +> ERROR: Exception on thread Thread[main,5,main] +> java.lang.IllegalStateException +> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) +> ``` + +Prova con una definizione inferiore: + +``` +scrcpy -m 1920 +scrcpy -m 1024 +scrcpy -m 800 +``` + +Potresti anche provare un altro [codificatore](README.it.md#codificatore). + + +## Linea di comando in Windows + +Alcuni utenti Windows non sono familiari con la riga di comando. Qui è descritto come aprire un terminale ed eseguire `scrcpy` con gli argomenti: + + 1. Premi Windows+r, questo apre una finestra di dialogo. + 2. Scrivi `cmd` e premi Enter, questo apre un terminale. + 3. Vai nella tua cartella di _scrcpy_ scrivendo (adatta il percorso): + + ```bat + cd C:\Users\user\Downloads\scrcpy-win64-xxx + ``` + + e premi Enter + 4. Scrivi il tuo comando. Per esempio: + + ```bat + scrcpy --record file.mkv + ``` + +Se pianifichi di utilizzare sempre gli stessi argomenti, crea un file `myscrcpy.bat` (abilita mostra [estensioni nomi file][show file extensions] per evitare di far confusione) contenente il tuo comando nella cartella di `scrcpy`. Per esempio: + +```bat +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +Poi fai doppio click su quel file. + +Potresti anche modificare (una copia di) `scrcpy-console.bat` o `scrcpy-noconsole.vbs` per aggiungere alcuni argomenti. + +[show file extensions]: https://www.techpedia.it/14-windows/windows-10/171-visualizzare-le-estensioni-nomi-file-con-windows-10 diff --git a/FAQ.ko.md b/FAQ.ko.md index 6cc1a1d9..c9e06e24 100644 --- a/FAQ.ko.md +++ b/FAQ.ko.md @@ -3,16 +3,16 @@ 다음은 자주 제보되는 문제들과 그들의 현황입니다. -### Window 운영체제에서, 디바이스가 발견되지 않습니다. +### Windows 운영체제에서, 디바이스가 발견되지 않습니다. 가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. 다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: adb devices -Window는 당신의 디바이스를 감지하기 위해 [drivers]가 필요할 수도 있습니다. +Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다. -[drivers]: https://developer.android.com/studio/run/oem-usb.html +[드라이버]: https://developer.android.com/studio/run/oem-usb.html ### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. diff --git a/FAQ.md b/FAQ.md index d68a8011..c1e39a39 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,5 +1,7 @@ # Frequently Asked Questions +[Read in another language](#translations) + Here are the common reported problems and their status. @@ -37,8 +39,13 @@ Check [stackoverflow][device-unauthorized]. ### Device not detected +> adb: error: failed to get feature set: no devices/emulators found + +Check that you correctly enabled [adb debugging][enable-adb]. + If your device is not detected, you may need some [drivers] (on Windows). +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html @@ -109,16 +116,6 @@ In developer options, enable: [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -### Mouse clicks at wrong location - -On MacOS, with HiDPI support and multiple screens, input location are wrongly -scaled. See [#15]. - -[#15]: https://github.com/Genymobile/scrcpy/issues/15 - -Open _scrcpy_ directly on the monitor you use it. - - ### Special characters do not work Injecting text input is [limited to ASCII characters][text-input]. A trick @@ -134,18 +131,27 @@ that's all. See [#37]. ### The quality is low -On Windows, you may need to configure the [scaling behavior]. +If the definition of your client window is smaller than that of your device +screen, then you might get poor quality, especially visible on text (see [#40]). + +[#40]: https://github.com/Genymobile/scrcpy/issues/40 + +To improve downscaling quality, trilinear filtering is enabled automatically +if the renderer is OpenGL and if it supports mipmapping. + +On Windows, you might want to force OpenGL: + +``` +scrcpy --render-driver=opengl +``` + +You may also need to configure the [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 -If the definition of your client window is far smaller than that of your device -screen, then you'll get poor quality. This is especially visible on text. See -[#40]. - -[#40]: https://github.com/Genymobile/scrcpy/issues/40 ### KWin compositor crashes @@ -188,3 +194,49 @@ scrcpy -m 1920 scrcpy -m 1024 scrcpy -m 800 ``` + +You could also try another [encoder](README.md#encoder). + + +## Command line on Windows + +Some Windows users are not familiar with the command line. Here is how to open a +terminal and run `scrcpy` with arguments: + + 1. Press Windows+r, this opens a dialog box. + 2. Type `cmd` and press Enter, this opens a terminal. + 3. Go to your _scrcpy_ directory, by typing (adapt the path): + + ```bat + cd C:\Users\user\Downloads\scrcpy-win64-xxx + ``` + + and press Enter + 4. Type your command. For example: + + ```bat + scrcpy --record file.mkv + ``` + +If you plan to always use the same arguments, create a file `myscrcpy.bat` +(enable [show file extensions] to avoid confusion) in the `scrcpy` directory, +containing your command. For example: + +```bat +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +Then just double-click on that file. + +You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` +to add some arguments. + +[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ + + +## Translations + +This FAQ is available in other languages: + + - [Italiano (Italiano, `it`) - v1.17](FAQ.it.md) + - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) diff --git a/LICENSE b/LICENSE index bc4bb77d..b320f699 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + 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. diff --git a/README.id.md b/README.id.md new file mode 100644 index 00000000..b4b16735 --- /dev/null +++ b/README.id.md @@ -0,0 +1,696 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.16) + +Aplikasi ini menyediakan tampilan dan kontrol perangkat Android yang terhubung pada USB (atau [melalui TCP/IP][article-tcpip]). Ini tidak membutuhkan akses _root_ apa pun. Ini bekerja pada _GNU/Linux_, _Windows_ and _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Ini berfokus pada: + + - **keringanan** (asli, hanya menampilkan layar perangkat) + - **kinerja** (30~60fps) + - **kualitas** (1920×1080 atau lebih) + - **latensi** rendah ([35~70ms][lowlatency]) + - **waktu startup rendah** (~1 detik untuk menampilkan gambar pertama) + - **tidak mengganggu** (tidak ada yang terpasang di perangkat) + + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Persyaratan +Perangkat Android membutuhkan setidaknya API 21 (Android 5.0). + +Pastikan Anda [mengaktifkan debugging adb][enable-adb] pada perangkat Anda. + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +Di beberapa perangkat, Anda juga perlu mengaktifkan [opsi tambahan][control] untuk mengontrolnya menggunakan keyboard dan mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Dapatkan aplikasinya + +### Linux + +Di Debian (_testing_ dan _sid_ untuk saat ini) dan Ubuntu (20.04): + +``` +apt install scrcpy +``` + +Paket [Snap] tersedia: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Untuk Fedora, paket [COPR] tersedia: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Untuk Arch Linux, paket [AUR] tersedia: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Untuk Gentoo, tersedia [Ebuild]: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Anda juga bisa [membangun aplikasi secara manual][BUILD] (jangan khawatir, tidak terlalu sulit). + + +### Windows + +Untuk Windows, untuk kesederhanaan, arsip prebuilt dengan semua dependensi (termasuk `adb`) tersedia : + + - [README](README.md#windows) + +Ini juga tersedia di [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # jika Anda belum memilikinya +``` + +Dan di [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # jika Anda belum memilikinya +``` + +[Scoop]: https://scoop.sh + +Anda juga dapat [membangun aplikasi secara manual][BUILD]. + + +### macOS + +Aplikasi ini tersedia di [Homebrew]. Instal saja: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` +Anda membutuhkan `adb`, dapat diakses dari `PATH` Anda. Jika Anda belum memilikinya: + +```bash +brew cask install android-platform-tools +``` + +Anda juga dapat [membangun aplikasi secara manual][BUILD]. + + +## Menjalankan + +Pasang perangkat Android, dan jalankan: + +```bash +scrcpy +``` + +Ini menerima argumen baris perintah, didaftarkan oleh: + +```bash +scrcpy --help +``` + +## Fitur + +### Menangkap konfigurasi + +#### Mengurangi ukuran + +Kadang-kadang, berguna untuk mencerminkan perangkat Android dengan definisi yang lebih rendah untuk meningkatkan kinerja. + +Untuk membatasi lebar dan tinggi ke beberapa nilai (mis. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versi pendek +``` + +Dimensi lain dihitung agar rasio aspek perangkat dipertahankan. +Dengan begitu, perangkat 1920×1080 akan dicerminkan pada 1024×576. + +#### Ubah kecepatan bit + +Kecepatan bit default adalah 8 Mbps. Untuk mengubah bitrate video (mis. Menjadi 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versi pendek +``` + +#### Batasi frekuensi gambar + +Kecepatan bingkai pengambilan dapat dibatasi: + +```bash +scrcpy --max-fps 15 +``` + +Ini secara resmi didukung sejak Android 10, tetapi dapat berfungsi pada versi sebelumnya. + +#### Memotong + +Layar perangkat dapat dipotong untuk mencerminkan hanya sebagian dari layar. + +Ini berguna misalnya untuk mencerminkan hanya satu mata dari Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 Mengimbangi (0,0) +``` + +Jika `--max-size` juga ditentukan, pengubahan ukuran diterapkan setelah pemotongan. + + +#### Kunci orientasi video + +Untuk mengunci orientasi pencerminan: + +```bash +scrcpy --lock-video-orientation 0 # orientasi alami +scrcpy --lock-video-orientation 1 # 90° berlawanan arah jarum jam +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° searah jarum jam +``` + +Ini mempengaruhi orientasi perekaman. + + +### Rekaman + +Anda dapat merekam layar saat melakukan mirroring: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Untuk menonaktifkan pencerminan saat merekam: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# berhenti merekam dengan Ctrl+C +``` + +"Skipped frames" are recorded, even if they are not displayed in real time (for +performance reasons). Frames are _timestamped_ on the device, so [packet delay +variation] does not impact the recorded file. + +"Frame yang dilewati" direkam, meskipun tidak ditampilkan secara real time (untuk alasan performa). Bingkai *diberi stempel waktu* pada perangkat, jadi [variasi penundaan paket] tidak memengaruhi file yang direkam. + +[variasi penundaan paket]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Koneksi + +#### Wireless + +_Scrcpy_ menggunakan `adb` untuk berkomunikasi dengan perangkat, dan` adb` dapat [terhubung] ke perangkat melalui TCP / IP: + +1. Hubungkan perangkat ke Wi-Fi yang sama dengan komputer Anda. +2. Dapatkan alamat IP perangkat Anda (dalam Pengaturan → Tentang ponsel → Status). +3. Aktifkan adb melalui TCP / IP pada perangkat Anda: `adb tcpip 5555`. +4. Cabut perangkat Anda. +5. Hubungkan ke perangkat Anda: `adb connect DEVICE_IP: 5555` (*ganti* *`DEVICE_IP`*). +6. Jalankan `scrcpy` seperti biasa. + +Mungkin berguna untuk menurunkan kecepatan bit dan definisi: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versi pendek +``` + +[terhubung]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi-perangkat + +Jika beberapa perangkat dicantumkan di `adb devices`, Anda harus menentukan _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versi pendek +``` + +If the device is connected over TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versi pendek +``` + +Anda dapat memulai beberapa contoh _scrcpy_ untuk beberapa perangkat. + +#### Mulai otomatis pada koneksi perangkat + +Anda bisa menggunakan [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Koneksi via SSH tunnel + +Untuk menyambung ke perangkat jarak jauh, dimungkinkan untuk menghubungkan klien `adb` lokal ke server `adb` jarak jauh (asalkan mereka menggunakan versi yang sama dari _adb_ protocol): + +```bash +adb kill-server # matikan server adb lokal di 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 komputer_jarak_jauh_anda +# jaga agar tetap terbuka +``` + +Dari terminal lain: + +```bash +scrcpy +``` + +Untuk menghindari mengaktifkan penerusan port jarak jauh, Anda dapat memaksa sambungan maju sebagai gantinya (perhatikan `-L`, bukan` -R`): + +```bash +adb kill-server # matikan server adb lokal di 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 komputer_jarak_jauh_anda +# jaga agar tetap terbuka +``` + +Dari terminal lain: + +```bash +scrcpy --force-adb-forward +``` + +Seperti koneksi nirkabel, mungkin berguna untuk mengurangi kualitas: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Konfigurasi Jendela + +#### Judul + +Secara default, judul jendela adalah model perangkat. Itu bisa diubah: + +```bash +scrcpy --window-title 'Perangkat Saya' +``` + +#### Posisi dan ukuran + +Posisi dan ukuran jendela awal dapat ditentukan: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Jendela tanpa batas + +Untuk menonaktifkan dekorasi jendela: + +```bash +scrcpy --window-borderless +``` + +#### Selalu di atas + +Untuk menjaga jendela scrcpy selalu di atas: + +```bash +scrcpy --always-on-top +``` + +#### Layar penuh + +Aplikasi dapat dimulai langsung dalam layar penuh:: + +```bash +scrcpy --fullscreen +scrcpy -f # versi pendek +``` + +Layar penuh kemudian dapat diubah secara dinamis dengan MOD+f. + +#### Rotasi + +Jendela mungkin diputar: + +```bash +scrcpy --rotation 1 +``` + +Nilai yang mungkin adalah: + - `0`: tidak ada rotasi + - `1`: 90 derajat berlawanan arah jarum jam + - `2`: 180 derajat + - `3`: 90 derajat searah jarum jam + +Rotasi juga dapat diubah secara dinamis dengan MOD+ +_(kiri)_ and MOD+ _(kanan)_. + +Perhatikan bahwa _scrcpy_ mengelola 3 rotasi berbeda:: + - MOD+r meminta perangkat untuk beralih antara potret dan lanskap (aplikasi yang berjalan saat ini mungkin menolak, jika mendukung orientasi yang diminta). + - `--lock-video-orientation` mengubah orientasi pencerminan (orientasi video yang dikirim dari perangkat ke komputer). Ini mempengaruhi rekaman. + - `--rotation` (atau MOD+/MOD+) + memutar hanya konten jendela. Ini hanya mempengaruhi tampilan, bukan rekaman. + + +### Opsi pencerminan lainnya + +#### Hanya-baca + +Untuk menonaktifkan kontrol (semua yang dapat berinteraksi dengan perangkat: tombol input, peristiwa mouse, seret & lepas file): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Layar + +Jika beberapa tampilan tersedia, Anda dapat memilih tampilan untuk cermin: + +```bash +scrcpy --display 1 +``` + +Daftar id tampilan dapat diambil dengan:: + +``` +adb shell dumpsys display # cari "mDisplayId=" di keluaran +``` + +Tampilan sekunder hanya dapat dikontrol jika perangkat menjalankan setidaknya Android 10 (jika tidak maka akan dicerminkan dalam hanya-baca). + + +#### Tetap terjaga + +Untuk mencegah perangkat tidur setelah beberapa penundaan saat perangkat dicolokkan: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +Keadaan awal dipulihkan ketika scrcpy ditutup. + + +#### Matikan layar + +Dimungkinkan untuk mematikan layar perangkat saat pencerminan mulai dengan opsi baris perintah: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Atau dengan menekan MOD+o kapan saja. + +Untuk menyalakannya kembali, tekan MOD+Shift+o. + +Di Android, tombol `POWER` selalu menyalakan layar. Untuk kenyamanan, jika `POWER` dikirim melalui scrcpy (melalui klik kanan atauMOD+p), itu akan memaksa untuk mematikan layar setelah penundaan kecil (atas dasar upaya terbaik). +Tombol fisik `POWER` masih akan menyebabkan layar dihidupkan. + +Ini juga berguna untuk mencegah perangkat tidur: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + +#### Render frame kedaluwarsa + +Secara default, untuk meminimalkan latensi, _scrcpy_ selalu menampilkan frame yang terakhir didekodekan tersedia, dan menghapus frame sebelumnya. + +Untuk memaksa rendering semua frame (dengan kemungkinan peningkatan latensi), gunakan: + +```bash +scrcpy --render-expired-frames +``` + +#### Tunjukkan sentuhan + +Untuk presentasi, mungkin berguna untuk menunjukkan sentuhan fisik (pada perangkat fisik). + +Android menyediakan fitur ini di _Opsi Pengembang_. + +_Scrcpy_ menyediakan opsi untuk mengaktifkan fitur ini saat mulai dan mengembalikan nilai awal saat keluar: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Perhatikan bahwa ini hanya menunjukkan sentuhan _fisik_ (dengan jari di perangkat). + + +#### Nonaktifkan screensaver + +Secara default, scrcpy tidak mencegah screensaver berjalan di komputer. + +Untuk menonaktifkannya: + +```bash +scrcpy --disable-screensaver +``` + + +### Kontrol masukan + +#### Putar layar perangkat + +Tekan MOD+r untuk beralih antara mode potret dan lanskap. + +Perhatikan bahwa itu berputar hanya jika aplikasi di latar depan mendukung orientasi yang diminta. + +#### Salin-tempel + +Setiap kali papan klip Android berubah, secara otomatis disinkronkan ke papan klip komputer. + +Apa saja Ctrl pintasan diteruskan ke perangkat. Khususnya: + - Ctrl+c biasanya salinan + - Ctrl+x biasanya memotong + - Ctrl+v biasanya menempel (setelah sinkronisasi papan klip komputer-ke-perangkat) + +Ini biasanya berfungsi seperti yang Anda harapkan. + +Perilaku sebenarnya tergantung pada aplikasi yang aktif. Sebagai contoh, +_Termux_ mengirim SIGINT ke Ctrl+c sebagai gantinya, dan _K-9 Mail_ membuat pesan baru. + +Untuk menyalin, memotong dan menempel dalam kasus seperti itu (tetapi hanya didukung di Android> = 7): + - MOD+c injeksi `COPY` _(salin)_ + - MOD+x injeksi `CUT` _(potong)_ + - MOD+v injeksi `PASTE` (setelah sinkronisasi papan klip komputer-ke-perangkat) + +Tambahan, MOD+Shift+v memungkinkan untuk memasukkan teks papan klip komputer sebagai urutan peristiwa penting. Ini berguna ketika komponen tidak menerima penempelan teks (misalnya di _Termux_), tetapi dapat merusak konten non-ASCII. + +**PERINGATAN:** Menempelkan papan klip komputer ke perangkat (baik melalui +Ctrl+v or MOD+v) menyalin konten ke clipboard perangkat. Akibatnya, aplikasi Android apa pun dapat membaca kontennya. Anda harus menghindari menempelkan konten sensitif (seperti kata sandi) seperti itu. + + +#### Cubit untuk memperbesar/memperkecil + +Untuk mensimulasikan "cubit-untuk-memperbesar/memperkecil": Ctrl+_klik-dan-pindah_. + +Lebih tepatnya, tahan Ctrl sambil menekan tombol klik kiri. Hingga tombol klik kiri dilepaskan, semua gerakan mouse berskala dan memutar konten (jika didukung oleh aplikasi) relatif ke tengah layar. + +Secara konkret, scrcpy menghasilkan kejadian sentuh tambahan dari "jari virtual" di lokasi yang dibalik melalui bagian tengah layar. + + +#### Preferensi injeksi teks + +Ada dua jenis [peristiwa][textevents] dihasilkan saat mengetik teks: +- _peristiwa penting_, menandakan bahwa tombol ditekan atau dilepaskan; +- _peristiwa teks_, menandakan bahwa teks telah dimasukkan. + +Secara default, huruf dimasukkan menggunakan peristiwa kunci, sehingga keyboard berperilaku seperti yang diharapkan dalam game (biasanya untuk tombol WASD). + +Tapi ini mungkin [menyebabkan masalah][prefertext]. Jika Anda mengalami masalah seperti itu, Anda dapat menghindarinya dengan: + +```bash +scrcpy --prefer-text +``` + +(tapi ini akan merusak perilaku keyboard dalam game) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Ulangi kunci + +Secara default, menahan tombol akan menghasilkan peristiwa kunci yang berulang. Ini dapat menyebabkan masalah kinerja di beberapa game, di mana acara ini tidak berguna. + +Untuk menghindari penerusan peristiwa penting yang berulang: + +```bash +scrcpy --no-key-repeat +``` + + +### Seret/jatuhkan file + +#### Pasang APK + +Untuk menginstal APK, seret & lepas file APK (diakhiri dengan `.apk`) ke jendela _scrcpy_. + +Tidak ada umpan balik visual, log dicetak ke konsol. + + +#### Dorong file ke perangkat + +Untuk mendorong file ke `/sdcard/` di perangkat, seret & jatuhkan file (non-APK) ke jendela _scrcpy_. + +Tidak ada umpan balik visual, log dicetak ke konsol. + +Direktori target dapat diubah saat mulai: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### Penerusan audio + +Audio tidak diteruskan oleh _scrcpy_. Gunakan [sndcpy]. + +Lihat juga [Masalah #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[Masalah #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Pintasan + +Dalam daftar berikut, MOD adalah pengubah pintasan. Secara default, ini (kiri) Alt atau (kiri) Super. + +Ini dapat diubah menggunakan `--shortcut-mod`. Kunci yang memungkinkan adalah `lctrl`,`rctrl`, `lalt`,` ralt`, `lsuper` dan` rsuper`. Sebagai contoh: + +```bash +# gunakan RCtrl untuk jalan pintas +scrcpy --shortcut-mod=rctrl + +# gunakan baik LCtrl+LAlt atau LSuper untuk jalan pintas +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] biasanya adalah Windows atau Cmd key._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Aksi | Pintasan + | ------------------------------------------------------|:----------------------------- + | Alihkan mode layar penuh | MOD+f + | Putar layar kiri | MOD+ _(kiri)_ + | Putar layar kanan | MOD+ _(kanan)_ + | Ubah ukuran jendela menjadi 1:1 (piksel-sempurna) | MOD+g + | Ubah ukuran jendela menjadi hapus batas hitam | MOD+w \| _klik-dua-kali¹_ + | Klik `HOME` | MOD+h \| _Klik-tengah_ + | Klik `BACK` | MOD+b \| _Klik-kanan²_ + | Klik `APP_SWITCH` | MOD+s + | Klik `MENU` (buka kunci layar) | MOD+m + | Klik `VOLUME_UP` | MOD+ _(naik)_ + | Klik `VOLUME_DOWN` | MOD+ _(turun)_ + | Klik `POWER` | MOD+p + | Menyalakan | _Klik-kanan²_ + | Matikan layar perangkat (tetap mirroring) | MOD+o + | Hidupkan layar perangkat | MOD+Shift+o + | Putar layar perangkat | MOD+r + | Luaskan panel notifikasi | MOD+n + | Ciutkan panel notifikasi | MOD+Shift+n + | Menyalin ke papan klip³ | MOD+c + | Potong ke papan klip³ | MOD+x + | Sinkronkan papan klip dan tempel³ | MOD+v + | Masukkan teks papan klip komputer | MOD+Shift+v + | Mengaktifkan/menonaktifkan penghitung FPS (di stdout) | MOD+i + | Cubit-untuk-memperbesar/memperkecil | Ctrl+_klik-dan-pindah_ + +_¹Klik-dua-kali pada batas hitam untuk menghapusnya._ +_²Klik-kanan akan menghidupkan layar jika mati, tekan BACK jika tidak._ +_³Hanya di Android >= 7._ + +Semua Ctrl+_key_ pintasan diteruskan ke perangkat, demikian adanya +ditangani oleh aplikasi aktif. + + +## Jalur kustom + +Untuk menggunakan biner _adb_ tertentu, konfigurasikan jalurnya di variabel lingkungan `ADB`: + + ADB=/path/to/adb scrcpy + +Untuk mengganti jalur file `scrcpy-server`, konfigurasikan jalurnya di +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Mengapa _scrcpy_? + +Seorang kolega menantang saya untuk menemukan nama yang tidak dapat diucapkan seperti [gnirehtet]. + +[`strcpy`] menyalin sebuah **str**ing; `scrcpy` menyalin sebuah **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Bagaimana Cara membangun? + +Lihat [BUILD]. + +[BUILD]: BUILD.md + + +## Masalah umum + +Lihat [FAQ](FAQ.md). + + +## Pengembang + +Baca [halaman pengembang]. + +[halaman pengembang]: DEVELOP.md + + +## Lisensi + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-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. + +## Artikel + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + diff --git a/README.it.md b/README.it.md new file mode 100644 index 00000000..37416f1d --- /dev/null +++ b/README.it.md @@ -0,0 +1,742 @@ +_Apri il [README](README.md) originale e sempre aggiornato._ + +# scrcpy (v1.17) + +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_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Si concentra su: + + - **leggerezza** (nativo, mostra solo lo schermo del dispositivo) + - **prestazioni** (30~60fps) + - **qualità** (1920×1080 o superiore) + - **bassa latenza** ([35~70ms][lowlatency]) + - **tempo di avvio basso** (~ 1secondo per visualizzare la prima immagine) + - **non invadenza** (nulla viene lasciato installato sul dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisiti + +Il dispositivo Android richiede almeno le API 21 (Android 5.0). + +Assiucurati di aver [attivato il debug usb][enable-adb] sul(/i) tuo(i) dispositivo(/i). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +In alcuni dispositivi, devi anche abilitare [un'opzione aggiuntiva][control] per controllarli con tastiera e mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + +## Ottieni l'app + +Packaging status + +### Sommario + + - Linux: `apt install scrcpy` + - Windows: [download](README.md#windows) + - macOS: `brew install scrcpy` + +Compila dai sorgenti: [BUILD] (in inglese) ([procedimento semplificato][BUILD_simple] (in inglese)) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + + +### Linux + +Su Debian (_testing_ e _sid_ per ora) e Ubuntu (20.04): + +``` +apt install scrcpy +``` + +È disponibile anche un pacchetto [Snap]: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://it.wikipedia.org/wiki/Snappy_(gestore_pacchetti) + +Per Fedora, è disponibile un pacchetto [COPR]: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Per Arch Linux, è disponibile un pacchetto [AUR]: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Per Gentoo, è disponibile una [Ebuild]: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Puoi anche [compilare l'app manualmente][BUILD] (in inglese) ([procedimento semplificato][BUILD_simple] (in inglese)). + + +### Windows + +Per Windows, per semplicità è disponibile un archivio precompilato con tutte le dipendenze (incluso `adb`): + + - [README](README.md#windows) (Link al README originale per l'ultima versione) + +È anche disponibile in [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # se non lo hai già +``` + +E in [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # se non lo hai già +``` + +[Scoop]: https://scoop.sh + +Puoi anche [compilare l'app manualmente][BUILD] (in inglese). + + +### macOS + +L'applicazione è disponibile in [Homebrew]. Basta installarlo: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Serve che `adb` sia accessibile dal tuo `PATH`. Se non lo hai già: + +```bash +brew install android-platform-tools +``` + +È anche disponibile in [MacPorts], che imposta adb per te: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + +Puoi anche [compilare l'app manualmente][BUILD] (in inglese). + + +## Esecuzione + +Collega un dispositivo Android ed esegui: + +```bash +scrcpy +``` + +Scrcpy accetta argomenti da riga di comando, essi sono listati con: + +```bash +scrcpy --help +``` + +## Funzionalità + +### Configurazione di acquisizione + +#### Riduci dimensione + +Qualche volta è utile trasmettere un dispositvo Android ad una definizione inferiore per aumentare le prestazioni. + +Per limitare sia larghezza che altezza ad un certo valore (ad es. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versione breve +``` + +L'altra dimensione è calcolata in modo tale che il rapporto di forma del dispositivo sia preservato. +In questo esempio un dispositivo in 1920x1080 viene trasmesso a 1024x576. + + +#### Cambia bit-rate (velocità di trasmissione) + +Il bit-rate predefinito è 8 Mbps. Per cambiare il bitrate video (ad es. a 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versione breve +``` + +#### Limitare il frame rate (frequenza di fotogrammi) + +Il frame rate di acquisizione può essere limitato: + +```bash +scrcpy --max-fps 15 +``` + +Questo è supportato ufficialmente a partire da Android 10, ma potrebbe funzionare in versioni precedenti. + +#### Ritaglio + +Lo schermo del dispositivo può essere ritagliato per visualizzare solo parte di esso. + +Questo può essere utile, per esempio, per trasmettere solo un occhio dell'Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) +``` + +Se anche `--max-size` è specificata, il ridimensionamento è applicato dopo il ritaglio. + + +#### Blocca orientamento del video + + +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 +``` + +Questo influisce sull'orientamento della registrazione. + + +La [finestra può anche essere ruotata](#rotazione) indipendentemente. + + +#### Codificatore + +Alcuni dispositivi hanno più di un codificatore e alcuni di questi possono provocare problemi o crash. È possibile selezionare un encoder diverso: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Per elencare i codificatori disponibili puoi immettere un nome di codificatore non valido e l'errore mostrerà i codificatori disponibili: + +```bash +scrcpy --encoder _ +``` + +### Registrazione + +È possibile registrare lo schermo durante la trasmissione: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Per disabilitare la trasmissione durante la registrazione: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrompere la registrazione con Ctrl+C +``` + +I "fotogrammi saltati" sono registrati nonostante non siano mostrati in tempo reale (per motivi di prestazioni). I fotogrammi sono _datati_ sul dispositivo, così una [variazione di latenza dei pacchetti][packet delay variation] non impatta il file registrato. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Connessione + +#### Wireless + + +_Scrcpy_ usa `adb` per comunicare col dispositivo e `adb` può [connettersi][connect] al dispositivo mediante TCP/IP: + +1. Connetti il dispositivo alla stessa rete Wi-Fi del tuo computer. +2. Trova l'indirizzo IP del tuo dispositivo in Impostazioni → Informazioni sul telefono → Stato, oppure eseguendo questo comando: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Abilita adb via TCP/IP sul tuo dispositivo: `adb tcpip 5555`. +4. Scollega il tuo dispositivo. +5. Connetti il tuo dispositivo: `adb connect IP_DISPOSITVO:5555` _(rimpiazza `IP_DISPOSITIVO`)_. +6. Esegui `scrcpy` come al solito. + +Potrebbe essere utile diminuire il bit-rate e la definizione + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versione breve +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Multi dispositivo + +Se in `adb devices` sono listati più dispositivi, è necessario specificare il _seriale_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versione breve +``` + +Se il dispositivo è collegato mediante TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versione breve +``` + +Puoi avviare più istanze di _scrcpy_ per diversi dispositivi. + + +#### Avvio automativo alla connessione del dispositivo + +Potresti usare [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Tunnel SSH + +Per connettersi a un dispositivo remoto è possibile collegare un client `adb` locale ad un server `adb` remoto (assunto che entrambi stiano usando la stessa versione del protocollo _adb_): + +```bash +adb kill-server # termina il server adb locale su 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# tieni questo aperto +``` + +Da un altro terminale: + +```bash +scrcpy +``` + +Per evitare l'abilitazione dell'apertura porte remota potresti invece forzare una "forward connection" (notare il `-L` invece di `-R`) + +```bash +adb kill-server # termina il server adb locale su 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# tieni questo aperto +``` + +Da un altro terminale: + +```bash +scrcpy --force-adb-forward +``` + + +Come per le connessioni wireless potrebbe essere utile ridurre la qualità: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configurazione della finestra + +#### Titolo + +Il titolo della finestra è il modello del dispositivo per impostazione predefinita. Esso può essere cambiato: + +```bash +scrcpy --window-title 'My device' +``` + +#### Posizione e dimensione + +La posizione e la dimensione iniziale della finestra può essere specificata: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Senza bordi + +Per disabilitare le decorazioni della finestra: + +```bash +scrcpy --window-borderless +``` + +#### Sempre in primo piano + +Per tenere scrcpy sempre in primo piano: + +```bash +scrcpy --always-on-top +``` + +#### Schermo intero + +L'app può essere avviata direttamente a schermo intero: + +```bash +scrcpy --fullscreen +scrcpy -f # versione breve +``` + +Lo schermo intero può anche essere attivato/disattivato con MOD+f. + +#### Rotazione + +La finestra può essere ruotata: + +```bash +scrcpy --rotation 1 +``` + +I valori possibili sono: + - `0`: nessuna rotazione + - `1`: 90 gradi antiorari + - `2`: 180 gradi + - `3`: 90 gradi orari + +La rotazione può anche essere cambiata dinamicamente con MOD+ +_(sinistra)_ e MOD+ _(destra)_. + +Notare che _scrcpy_ gestisce 3 diversi tipi di rotazione: + - MOD+r richiede al dispositvo di cambiare tra orientamento verticale (portrait) e orizzontale (landscape) (l'app in uso potrebbe rifiutarsi se non supporta l'orientamento richiesto). + - [`--lock-video-orientation`](#blocca-orientamento-del-video) cambia l'orientamento della trasmissione (l'orientamento del video inviato dal dispositivo al computer). Questo influenza la registrazione. + - `--rotation` (o MOD+/MOD+) ruota solo il contenuto della finestra. Questo influenza solo la visualizzazione, non la registrazione. + + +### Altre opzioni di trasmissione + +#### "Sola lettura" + +Per disabilitare i controlli (tutto ciò che può interagire col dispositivo: tasti di input, eventi del mouse, trascina e rilascia (drag&drop) file): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Schermo + +Se sono disponibili più schermi, è possibile selezionare lo schermo da trasmettere: + +```bash +scrcpy --display 1 +``` + +La lista degli id schermo può essere ricavata da: + +```bash +adb shell dumpsys display # cerca "mDisplayId=" nell'output +``` + +Lo schermo secondario potrebbe essere possibile controllarlo solo se il dispositivo esegue almeno Android 10 (in caso contrario è trasmesso in modalità sola lettura). + + +#### Mantenere sbloccato + +Per evitare che il dispositivo si blocchi dopo un po' che il dispositivo è collegato: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +Lo stato iniziale è ripristinato quando scrcpy viene chiuso. + + +#### Spegnere lo schermo + +È possibile spegnere lo schermo del dispositivo durante la trasmissione con un'opzione da riga di comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Oppure premendo MOD+o in qualsiasi momento. + +Per riaccenderlo premere MOD+Shift+o. + +In Android il pulsante `POWER` (tasto di accensione) accende sempre lo schermo. Per comodità, se `POWER` è inviato via scrcpy (con click destro o con MOD+p), si forza il dispositivo a spegnere lo schermo dopo un piccolo ritardo (appena possibile). +Il pulsante fisico `POWER` continuerà ad accendere lo schermo normalmente. + +Può anche essere utile evitare il blocco del dispositivo: + +```bash +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). + +Android fornisce questa funzionalità nelle _Opzioni sviluppatore_. + +_Scrcpy_ fornisce un'opzione per abilitare questa funzionalità all'avvio e ripristinare il valore iniziale alla chiusura: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Notare che mostra solo i tocchi _fisici_ (con le dita sul dispositivo). + + +#### Disabilitare il salvaschermo + +In maniera predefinita scrcpy non previene l'attivazione del salvaschermo del computer. + +Per disabilitarlo: + +```bash +scrcpy --disable-screensaver +``` + + +### Input di controlli + +#### Rotazione dello schermo del dispostivo + +Premere MOD+r per cambiare tra le modalità verticale (portrait) e orizzontale (landscape). + +Notare che la rotazione avviene solo se l'applicazione in primo piano supporta l'orientamento richiesto. + +#### Copia-incolla + +Quando gli appunti di Android cambiano, essi vengono automaticamente sincronizzati con gli appunti del computer. + +Qualsiasi scorciatoia Ctrl viene inoltrata al dispositivo. In particolare: + - Ctrl+c copia + - Ctrl+x taglia + - Ctrl+v incolla (dopo la sincronizzazione degli appunti da computer a dispositivo) + +Questo solitamente funziona nella maniera più comune. + +Il comportamento reale, però, dipende dall'applicazione attiva. Per esempio _Termux_ invia SIGINT con Ctrl+c, e _K-9 Mail_ compone un nuovo messaggio. + +Per copiare, tagliare e incollare in questi casi (ma è solo supportato in Android >= 7): + - MOD+c inietta `COPY` + - MOD+x inietta `CUT` + - MOD+v inietta `PASTE` (dopo la sincronizzazione degli appunti da computer a dispositivo) + +In aggiunta, MOD+Shift+v permette l'iniezione del testo degli appunti del computer come una sequenza di eventi pressione dei tasti. Questo è utile quando il componente non accetta l'incollaggio di testo (per esempio in _Termux_), ma questo può rompere il contenuto non ASCII. + +**AVVISO:** Incollare gli appunti del computer nel dispositivo (sia con Ctrl+v che con MOD+v) copia il contenuto negli appunti del dispositivo. Come conseguenza, qualsiasi applicazione Android potrebbe leggere il suo contenuto. Dovresti evitare di incollare contenuti sensibili (come password) in questa maniera. + +Alcuni dispositivi non si comportano come aspettato quando si modificano gli appunti del dispositivo a livello di codice. L'opzione `--legacy-paste` è fornita per cambiare il comportamento di Ctrl+v and MOD+v in modo tale che anch'essi iniettino il testo gli appunti del computer come una sequenza di eventi pressione dei tasti (nella stessa maniera di MOD+Shift+v). + +#### Pizzica per zoomare (pinch-to-zoom) + +Per simulare il "pizzica per zoomare": Ctrl+_click e trascina_. + +Più precisamente, tieni premuto Ctrl mentre premi il pulsante sinistro. Finchè il pulsante non sarà rilasciato, tutti i movimenti del mouse ridimensioneranno e ruoteranno il contenuto (se supportato dall'applicazione) relativamente al centro dello schermo. + +Concretamente scrcpy genera degli eventi di tocco addizionali di un "dito virtuale" nella posizione simmetricamente opposta rispetto al centro dello schermo. + + +#### Preferenze di iniezione del testo + +Ci sono due tipi di [eventi][textevents] generati quando si scrive testo: + - _eventi di pressione_, segnalano che tasto è stato premuto o rilasciato; + - _eventi di testo_, segnalano che del testo è stato inserito. + +In maniera predefinita le lettere sono "iniettate" usando gli eventi di pressione, in maniera tale che la tastiera si comporti come aspettato nei giochi (come accade solitamente per i tasti WASD). + +Questo, però, può [causare problemi][prefertext]. Se incontri un problema del genere, puoi evitarlo con: + +```bash +scrcpy --prefer-text +``` + +(ma questo romperà il normale funzionamento della tastiera nei giochi) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Ripetizione di tasti + +In maniera predefinita tenere premuto un tasto genera una ripetizione degli eventi di pressione di tale tasto. Questo può creare problemi di performance in alcuni giochi, dove questi eventi sono inutilizzati. + +Per prevenire l'inoltro ripetuto degli eventi di pressione: + +```bash +scrcpy --no-key-repeat +``` + +#### Click destro e click centrale + +In maniera predefinita, click destro aziona BACK (indietro) e il click centrale aziona HOME. Per disabilitare queste scorciatoie e, invece, inviare i click al dispositivo: + +```bash +scrcpy --forward-all-clicks +``` + + +### Rilascio di file + +#### Installare APK + +Per installare un APK, trascina e rilascia un file APK (finisce con `.apk`) nella finestra di _scrcpy_. + +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_. + +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/ +``` + + +### Inoltro dell'audio + +L'audio non è inoltrato da _scrcpy_. Usa [sndcpy]. + +Vedi anche la [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Scociatoie + +Nella lista seguente, MOD è il modificatore delle scorciatoie. In maniera predefinita è Alt (sinistro) o Super (sinistro). + +Può essere cambiato usando `--shortcut-mod`. I tasti possibili sono `lctrl`, `rctrl`, `lalt`, `ralt`, `lsuper` and `rsuper` (`l` significa sinistro e `r` significa destro). Per esempio: + +```bash +# usa ctrl destro per le scorciatoie +scrcpy --shortcut-mod=rctrl + +# use sia "ctrl sinistro"+"alt sinistro" che "super sinistro" per le scorciatoie +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] è il pulsante Windows o Cmd._ + +[Super]: https://it.wikipedia.org/wiki/Tasto_Windows + + + | Azione | Scorciatoia + | ------------------------------------------- |:----------------------------- + | Schermo intero | MOD+f + | 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¹_ + | 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 `MENU` (sblocca lo schermo) | MOD+m + | Premi il tasto `VOLUME_UP` | MOD+ _(su)_ + | Premi il tasto `VOLUME_DOWN` | MOD+ _(giù)_ + | Premi il tasto `POWER` | MOD+p + | Accendi | _Click destro²_ + | 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 + | 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._ + +Tutte le scorciatoie Ctrl+_tasto_ sono inoltrate al dispositivo, così sono gestite dall'applicazione attiva. + +## Path personalizzati + +Per utilizzare dei binari _adb_ specifici, configura il suo path nella variabile d'ambente `ADB`: + +```bash +ADB=/percorso/per/adb scrcpy +``` + +Per sovrascrivere il percorso del file `scrcpy-server`, configura il percorso in `SCRCPY_SERVER_PATH`. + +## Perchè _scrcpy_? + +Un collega mi ha sfidato a trovare un nome tanto impronunciabile quanto [gnirehtet]. + +[`strcpy`] copia una **str**ing (stringa); `scrcpy` copia uno **scr**een (schermo). + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + +## Come compilare? + +Vedi [BUILD] (in inglese). + + +## Problemi comuni + +Vedi le [FAQ](FAQ.it.md). + + +## Sviluppatori + +Leggi la [pagina per sviluppatori]. + +[pagina per sviluppatori]: DEVELOP.md + + +## Licenza (in inglese) + + 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. + +## Articoli (in inglese) + +- [Introducendo scrcpy][article-intro] +- [Scrcpy ora funziona wireless][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.jp.md b/README.jp.md new file mode 100644 index 00000000..e42c528e --- /dev/null +++ b/README.jp.md @@ -0,0 +1,725 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +# scrcpy (v1.17) + +このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。 + +![screenshot](assets/screenshot-debian-600.jpg) + +以下に焦点を当てています: + + - **軽量** (ネイティブ、デバイス画面表示のみ) + - **パフォーマンス** (30~60fps) + - **クオリティ** (1920x1080以上) + - **低遅延** ([35~70ms][lowlatency]) + - **短い起動時間** (初回画像を1秒以内に表示) + - **非侵入型** (デバイスに何もインストールされていない状態になる) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 必要要件 + +AndroidデバイスはAPI21(Android 5.0)以上。 + +Androidデバイスで[adbデバッグが有効][enable-adb]であること。 + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +一部のAndroidデバイスでは、キーボードとマウスを使用して制御する[追加オプション][control]を有効にする必要がある。 + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## アプリの取得 + +Packaging status + +### Linux + +Debian (_testing_ と _sid_) とUbuntu(20.04): + +``` +apt install scrcpy +``` + +[Snap]パッケージが利用可能: [`scrcpy`][snap-link] + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Fedora用[COPR]パッケージが利用可能: [`scrcpy`][copr-link] + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Arch Linux用[AUR]パッケージが利用可能: [`scrcpy`][aur-link] + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Gentoo用[Ebuild]が利用可能: [`scrcpy`][ebuild-link] + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +[自分でビルド][BUILD]も可能(心配しないでください、それほど難しくはありません。) + + +### Windows + +Windowsでは簡単に、(`adb`を含む)すべての依存関係を構築済みのアーカイブを利用可能です。 + + - [README](README.md#windows) + +[Chocolatey]でも利用可能です: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # まだ入手していない場合 +``` + +[Scoop]でも利用可能です: + +```bash +scoop install scrcpy +scoop install adb # まだ入手していない場合 +``` + +[Scoop]: https://scoop.sh + +また、[アプリケーションをビルド][BUILD]することも可能です。 + +### macOS + +アプリケーションは[Homebrew]で利用可能です。ただインストールするだけです。 + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +`PATH`から`adb`へのアクセスが必要です。もしまだ持っていない場合: + +```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 +brew cask install android-platform-tools +``` + +また、[アプリケーションをビルド][BUILD]することも可能です。 + + +## 実行 + +Androidデバイスを接続し、実行: + +```bash +scrcpy +``` + +次のコマンドでリストされるコマンドライン引数も受け付けます: + +```bash +scrcpy --help +``` + +## 機能 + +### キャプチャ構成 + +#### サイズ削減 + +Androidデバイスを低解像度でミラーリングする場合、パフォーマンス向上に便利な場合があります。 + +幅と高さをある値(例:1024)に制限するには: + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 短縮版 +``` + +一方のサイズはデバイスのアスペクト比が維持されるように計算されます。この方法では、1920x1080のデバイスでは1024x576にミラーリングされます。 + + +#### ビットレート変更 + +ビットレートの初期値は8Mbpsです。ビットレートを変更するには(例:2Mbpsに変更): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 短縮版 +``` + +#### フレームレート制限 + +キャプチャするフレームレートを制限できます: + +```bash +scrcpy --max-fps 15 +``` + +この機能はAndroid 10からオフィシャルサポートとなっていますが、以前のバージョンでも動作する可能性があります。 + +#### トリミング + +デバイスの画面は、画面の一部のみをミラーリングするようにトリミングできます。 + +これは、例えばOculus Goの片方の目をミラーリングする場合に便利です。: + +```bash +scrcpy --crop 1224:1440:0:0 # オフセット位置(0,0)で1224x1440 +``` + +もし`--max-size`も指定されている場合、トリミング後にサイズ変更が適用されます。 + +#### ビデオの向きをロックする + +ミラーリングの向きをロックするには: + +```bash +scrcpy --lock-video-orientation 0 # 自然な向き +scrcpy --lock-video-orientation 1 # 90°反時計回り +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90°時計回り +``` + +この設定は録画の向きに影響します。 + +[ウィンドウは独立して回転することもできます](#回転)。 + + +#### エンコーダ + +いくつかのデバイスでは一つ以上のエンコーダを持ちます。それらのいくつかは、問題やクラッシュを引き起こします。別のエンコーダを選択することが可能です: + + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +利用可能なエンコーダをリストするために、無効なエンコーダ名を渡すことができます。エラー表示で利用可能なエンコーダを提供します。 + +```bash +scrcpy --encoder _ +``` + +### 録画 + +ミラーリング中に画面の録画をすることが可能です: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +録画中にミラーリングを無効にするには: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# Ctrl+Cで録画を中断する +``` + +"スキップされたフレーム"は(パフォーマンス上の理由で)リアルタイムで表示されなくても録画されます。 + +フレームはデバイス上で _タイムスタンプされる_ ため [パケット遅延のバリエーション] は録画されたファイルに影響を与えません。 + +[パケット遅延のバリエーション]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### 接続 + +#### ワイヤレス + +_Scrcpy_ はデバイスとの通信に`adb`を使用します。そして`adb`はTCP/IPを介しデバイスに[接続]することができます: + +1. あなたのコンピュータと同じWi-Fiに接続します。 +2. あなたのIPアドレスを取得します。設定 → 端末情報 → ステータス情報、もしくは、このコマンドを実行します: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. あなたのデバイスでTCP/IPを介したadbを有効にします: `adb tcpip 5555` +4. あなたのデバイスの接続を外します。 +5. あなたのデバイスに接続します: + `adb connect DEVICE_IP:5555` _(`DEVICE_IP`は置き換える)_ +6. 通常通り`scrcpy`を実行します。 + +この方法はビットレートと解像度を減らすのにおそらく有用です: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 短縮版 +``` + +[接続]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### マルチデバイス + +もし`adb devices`でいくつかのデバイスがリストされる場合、 _シリアルナンバー_ を指定する必要があります: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # 短縮版 +``` + +デバイスがTCP/IPを介して接続されている場合: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # 短縮版 +``` + +複数のデバイスに対して、複数の _scrcpy_ インスタンスを開始することができます。 + +#### デバイス接続での自動起動 + +[AutoAdb]を使用可能です: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### SSHトンネル + +リモートデバイスに接続するため、ローカル`adb`クライアントからリモート`adb`サーバーへ接続することが可能です(同じバージョンの _adb_ プロトコルを使用している場合): + +```bash +adb kill-server # 5037ポートのローカルadbサーバーを終了する +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# オープンしたままにする +``` + +他の端末から: + +```bash +scrcpy +``` + +リモートポート転送の有効化を回避するためには、代わりに転送接続を強制することができます(`-R`の代わりに`-L`を使用することに注意): + +```bash +adb kill-server # 5037ポートのローカルadbサーバーを終了する +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# オープンしたままにする +``` + +他の端末から: + +```bash +scrcpy --force-adb-forward +``` + + +ワイヤレス接続と同様に、クオリティを下げると便利な場合があります: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### ウィンドウ構成 + +#### タイトル + +ウィンドウのタイトルはデバイスモデルが初期値です。これは変更できます: + +```bash +scrcpy --window-title 'My device' +``` + +#### 位置とサイズ + +ウィンドウの位置とサイズの初期値を指定できます: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### ボーダーレス + +ウィンドウの装飾を無効化するには: + +```bash +scrcpy --window-borderless +``` + +#### 常に画面のトップ + +scrcpyの画面を常にトップにするには: + +```bash +scrcpy --always-on-top +``` + +#### フルスクリーン + +アプリケーションを直接フルスクリーンで開始できます: + +```bash +scrcpy --fullscreen +scrcpy -f # 短縮版 +``` + +フルスクリーンは、次のコマンドで動的に切り替えることができます MOD+f + + +#### 回転 + +ウィンドウは回転することができます: + +```bash +scrcpy --rotation 1 +``` + +設定可能な値: + - `0`: 回転なし + - `1`: 90° 反時計回り + - `2`: 180° + - `3`: 90° 時計回り + +回転は次のコマンドで動的に変更することができます。 MOD+_(左)_ 、 MOD+_(右)_ + +_scrcpy_ は3つの回転を管理することに注意: + - MOD+rはデバイスに縦向きと横向きの切り替えを要求する(現在実行中のアプリで要求している向きをサポートしていない場合、拒否することがある) + - [`--lock-video-orientation`](#ビデオの向きをロックする)は、ミラーリングする向きを変更する(デバイスからPCへ送信される向き)。録画に影響します。 + - `--rotation` (もしくはMOD+/MOD+)は、ウィンドウのコンテンツのみを回転します。これは表示にのみに影響し、録画には影響しません。 + +### 他のミラーリングオプション + +#### Read-only リードオンリー + +制御を無効にするには(デバイスと対話する全てのもの:入力キー、マウスイベント、ファイルのドラッグ&ドロップ): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### ディスプレイ + +いくつか利用可能なディスプレイがある場合、ミラーリングするディスプレイを選択できます: + +```bash +scrcpy --display 1 +``` + +ディスプレイIDのリストは次の方法で取得できます: + +``` +adb shell dumpsys display # search "mDisplayId=" in the output +``` + +セカンダリディスプレイは、デバイスが少なくともAndroid 10の場合にコントロール可能です。(それ以外ではリードオンリーでミラーリングされます) + + +#### 起動状態にする + +デバイス接続時、少し遅れてからデバイスのスリープを防ぐには: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +scrcpyが閉じられた時、初期状態に復元されます。 + +#### 画面OFF + +コマンドラインオプションを使用することで、ミラーリングの開始時にデバイスの画面をOFFにすることができます: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +もしくは、MOD+oを押すことでいつでもできます。 + +元に戻すには、MOD+Shift+oを押します。 + +Androidでは、`POWER`ボタンはいつでも画面を表示します。便宜上、`POWER`がscrcpyを介して(右クリックもしくはMOD+pを介して)送信される場合、(ベストエフォートベースで)少し遅れて、強制的に画面を非表示にします。ただし、物理的な`POWER`ボタンを押した場合は、画面は表示されます。 + +このオプションはデバイスがスリープしないようにすることにも役立ちます: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### 期限切れフレームをレンダリングする + +初期状態では、待ち時間を最小限にするために、_scrcpy_ は最後にデコードされたフレームをレンダリングし、前のフレームを削除します。 + +全フレームのレンダリングを強制するには(待ち時間が長くなる可能性があります): + +```bash +scrcpy --render-expired-frames +``` + +#### タッチを表示 + +プレゼンテーションの場合(物理デバイス上で)物理的なタッチを表示すると便利な場合があります。 + +Androidはこの機能を _開発者オプション_ で提供します。 + +_Scrcpy_ は開始時にこの機能を有効にし、終了時に初期値を復元するオプションを提供します: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +(デバイス上で指を使った) _物理的な_ タッチのみ表示されることに注意してください。 + + +#### スクリーンセーバー無効 + +初期状態では、scrcpyはコンピュータ上でスクリーンセーバーが実行される事を妨げません。 + +これを無効にするには: + +```bash +scrcpy --disable-screensaver +``` + + +### 入力制御 + +#### デバイス画面の回転 + +MOD+rを押すことで、縦向きと横向きを切り替えます。 + +フォアグラウンドのアプリケーションが要求された向きをサポートしている場合のみ回転することに注意してください。 + +#### コピー-ペースト + +Androidのクリップボードが変更される度に、コンピュータのクリップボードに自動的に同期されます。 + +Ctrlのショートカットは全てデバイスに転送されます。特に: + - Ctrl+c 通常はコピーします + - Ctrl+x 通常はカットします + - Ctrl+v 通常はペーストします(コンピュータとデバイスのクリップボードが同期された後) + +通常は期待通りに動作します。 + +しかしながら、実際の動作はアクティブなアプリケーションに依存します。例えば、_Termux_ は代わりにCtrl+cでSIGINTを送信します、そして、_K-9 Mail_ は新しいメッセージを作成します。 + +このようなケースでコピー、カットそしてペーストをするには(Android 7以上でのサポートのみですが): + - MOD+c `COPY`を挿入 + - MOD+x `CUT`を挿入 + - MOD+v `PASTE`を挿入(コンピュータとデバイスのクリップボードが同期された後) + +加えて、MOD+Shift+vはコンピュータのクリップボードテキストにキーイベントのシーケンスとして挿入することを許可します。これはコンポーネントがテキストのペーストを許可しない場合(例えば _Termux_)に有用ですが、非ASCIIコンテンツを壊す可能性があります。 + +**警告:** デバイスにコンピュータのクリップボードを(Ctrl+vまたはMOD+vを介して)ペーストすることは、デバイスのクリップボードにコンテンツをコピーします。結果としてどのAndoridアプリケーションもそのコンテンツを読み取ることができます。機密性の高いコンテンツ(例えばパスワードなど)をこの方法でペーストすることは避けてください。 + +プログラムでデバイスのクリップボードを設定した場合、一部のデバイスは期待どおりに動作しません。`--legacy-paste`オプションは、コンピュータのクリップボードテキストをキーイベントのシーケンスとして挿入するため(MOD+Shift+vと同じ方法)、Ctrl+vMOD+vの動作の変更を提供します。 + +#### ピンチしてズームする + +"ピンチしてズームする"をシミュレートするには: Ctrl+_クリック&移動_ + +より正確にするには、左クリックボタンを押している間、Ctrlを押したままにします。左クリックボタンを離すまで、全てのマウスの動きは、(アプリでサポートされている場合)画面の中心を基準として、コンテンツを拡大縮小および回転します。 + +具体的には、scrcpyは画面の中央を反転した位置にある"バーチャルフィンガー"から追加のタッチイベントを生成します。 + + +#### テキストインジェクション環境設定 + +テキストをタイプした時に生成される2種類の[イベント][textevents]があります: + - _key events_ はキーを押したときと離したことを通知します。 + - _text events_ はテキストが入力されたことを通知します。 + +初期状態で、文字はキーイベントで挿入されるため、キーボードはゲームで期待通りに動作します(通常はWASDキー)。 + +しかし、これは[問題を引き起こす][prefertext]かもしれません。もしこのような問題が発生した場合は、この方法で回避できます: + +```bash +scrcpy --prefer-text +``` + +(しかしこの方法はゲームのキーボードの動作を壊します) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### キーの繰り返し + +初期状態では、キーの押しっぱなしは繰り返しのキーイベントを生成します。これらのイベントが使われない場合でも、この方法は一部のゲームでパフォーマンスの問題を引き起す可能性があります。 + +繰り返しのキーイベントの転送を回避するためには: + +```bash +scrcpy --no-key-repeat +``` + + +#### 右クリックと真ん中クリック + +初期状態では、右クリックはバックの動作(もしくはパワーオン)を起こし、真ん中クリックではホーム画面へ戻ります。このショートカットを無効にし、代わりにデバイスへクリックを転送するには: + +```bash +scrcpy --forward-all-clicks +``` + + +### ファイルのドロップ + +#### APKのインストール + +APKをインストールするには、(`.apk`で終わる)APKファイルを _scrcpy_ の画面にドラッグ&ドロップします。 + +見た目のフィードバックはありません。コンソールにログが出力されます。 + + +#### デバイスにファイルを送る + +デバイスの`/sdcard/`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。 + +見た目のフィードバックはありません。コンソールにログが出力されます。 + +転送先ディレクトリを起動時に変更することができます: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### 音声転送 + +音声は _scrcpy_ では転送されません。[sndcpy]を使用します。 + +[issue #14]も参照ください。 + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## ショートカット + +次のリストでは、MODでショートカット変更します。初期状態では、(left)Altまたは(left)Superです。 + +これは`--shortcut-mod`で変更することができます。可能なキーは`lctrl`、`rctrl`、`lalt`、 `ralt`、 `lsuper`そして`rsuper`です。例えば: + +```bash +# RCtrlをショートカットとして使用します +scrcpy --shortcut-mod=rctrl + +# ショートカットにLCtrl+LAltまたはLSuperのいずれかを使用します +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super]は通常WindowsもしくはCmdキーです。_ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | アクション | ショートカット + | ------------------------------------------- |:----------------------------- + | フルスクリーンモードへの切り替え | MOD+f + | ディスプレイを左に回転 | MOD+ _(左)_ + | ディスプレイを右に回転 | MOD+ _(右)_ + | ウィンドウサイズを変更して1:1に変更(ピクセルパーフェクト) | MOD+g + | ウィンドウサイズを変更して黒い境界線を削除 | MOD+w \| _ダブルクリック¹_ + | `HOME`をクリック | MOD+h \| _真ん中クリック_ + | `BACK`をクリック | MOD+b \| _右クリック²_ + | `APP_SWITCH`をクリック | MOD+s + | `MENU` (画面のアンロック)をクリック | MOD+m + | `VOLUME_UP`をクリック | MOD+ _(上)_ + | `VOLUME_DOWN`をクリック | MOD+ _(下)_ + | `POWER`をクリック | MOD+p + | 電源オン | _右クリック²_ + | デバイス画面をオフにする(ミラーリングしたまま) | MOD+o + | デバイス画面をオンにする | MOD+Shift+o + | デバイス画面を回転する | MOD+r + | 通知パネルを展開する | MOD+n + | 通知パネルを折りたたむ | MOD+Shift+n + | クリップボードへのコピー³ | MOD+c + | クリップボードへのカット³ | MOD+x + | クリップボードの同期とペースト³ | MOD+v + | コンピュータのクリップボードテキストの挿入 | MOD+Shift+v + | FPSカウンタ有効/無効(標準入出力上) | MOD+i + | ピンチしてズームする | Ctrl+_クリック&移動_ + +_¹黒い境界線を削除するため、境界線上でダブルクリック_ +_²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._ +_³Android 7以上のみ._ + +全てのCtrl+_キー_ ショートカットはデバイスに転送されます、そのためアクティブなアプリケーションによって処理されます。 + + +## カスタムパス + +特定の _adb_ バイナリを使用する場合、そのパスを環境変数`ADB`で構成します: + + ADB=/path/to/adb scrcpy + +`scrcpy-server`ファイルのパスを上書きするには、`SCRCPY_SERVER_PATH`でそのパスを構成します。 + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## なぜ _scrcpy_? + +同僚が私に、[gnirehtet]のように発音できない名前を見つけるように要求しました。 + +[`strcpy`]は**str**ingをコピーします。`scrcpy`は**scr**eenをコピーします。 + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## ビルド方法は? + +[BUILD]を参照してください。 + +[BUILD]: BUILD.md + + +## よくある質問 + +[FAQ](FAQ.md)を参照してください。 + + +## 開発者 + +[開発者のページ]を読んでください。 + +[開発者のページ]: DEVELOP.md + + +## ライセンス + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-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. + +## 記事 + +- [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.ko.md b/README.ko.md index b232accd..31e38c6f 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,3 +1,5 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + # scrcpy (v1.11) This document will be updated frequently along with the original Readme file @@ -66,9 +68,7 @@ Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link]. 윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : 해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. - - [`scrcpy-win`][direct-win] - -[direct-win]: https://github.com/Genymobile/scrcpy/blob/master/README.md#windows + - [README](README.md#windows) [어플을 직접 설치][BUILD] 할 수도 있습니다. @@ -112,7 +112,7 @@ scrcpy --help ### 캡쳐 환경 설정 -###사이즈 재정의 +### 사이즈 재정의 가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. @@ -136,7 +136,7 @@ scrcpy --bit-rate 2M scrcpy -b 2M # 축약 버전 ``` -###프레임 비율 제한 +### 프레임 비율 제한 안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: @@ -475,7 +475,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상 ## 라이선스 Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + 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. diff --git a/README.md b/README.md index ae947cd8..c83cafeb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# scrcpy (v1.12.1) +# scrcpy (v1.18) + +[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. @@ -34,10 +36,23 @@ control it using keyboard and mouse. ## Get the app +Packaging status + +### Summary + + - Linux: `apt install scrcpy` + - Windows: [download][direct-win64] + - macOS: `brew install scrcpy` + +Build from sources: [BUILD] ([simplified process][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + ### Linux -In Debian (_testing_ and _sid_ for now): +On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): ``` apt install scrcpy @@ -49,6 +64,11 @@ A [Snap] package is available: [`scrcpy`][snap-link]. [snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) +For Fedora, a [COPR] package is available: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link]. [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository @@ -59,9 +79,8 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link]. [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy -You could also [build the app manually][BUILD] (don't worry, it's not that -hard). - +You could also [build the app manually][BUILD] ([simplified +process][BUILD_simple]). ### Windows @@ -69,10 +88,10 @@ hard). For Windows, for simplicity, a prebuilt archive with all the dependencies (including `adb`) is available: - - [`scrcpy-win64-v1.12.1.zip`][direct-win64] - _(SHA-256: 57d34b6d16cfd9fe169bc37c4df58ebd256d05c1ea3febc63d9cb0a027ab47c9)_ + - [`scrcpy-win64-v1.18.zip`][direct-win64] + _(SHA-256: 37212f5087fe6f3e258f1d44fa5c02207496b30e1d7ec442cbcf8358910a5c63)_ -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.12.1/scrcpy-win64-v1.12.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-win64-v1.18.zip It is also available in [Chocolatey]: @@ -108,9 +127,18 @@ brew install scrcpy You need `adb`, accessible from your `PATH`. If you don't have it yet: ```bash -brew cask install android-platform-tools +brew install android-platform-tools ``` +It's also available in [MacPorts], which sets up adb for you: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + You can also [build the app manually][BUILD]. @@ -159,12 +187,14 @@ scrcpy -b 2M # short version #### Limit frame rate -On devices with Android >= 10, the capture frame rate can be limited: +The capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` +This is officially supported since Android 10, but may work on earlier versions. + #### Crop The device screen may be cropped to mirror only part of the screen. @@ -178,7 +208,43 @@ scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) If `--max-size` is also specified, resizing is applied after cropping. -### Recording +#### Lock video orientation + + +To lock the orientation of the mirroring: + +```bash +scrcpy --lock-video-orientation # initial (current) orientation +scrcpy --lock-video-orientation=0 # natural orientation +scrcpy --lock-video-orientation=1 # 90° counterclockwise +scrcpy --lock-video-orientation=2 # 180° +scrcpy --lock-video-orientation=3 # 90° clockwise +``` + +This affects recording orientation. + +The [window may also be rotated](#rotation) independently. + + +#### Encoder + +Some devices have more than one encoder, and some of them may cause issues or +crash. It is possible to select a different encoder: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +To list the available encoders, you could pass an invalid encoder name, the +error will give the available encoders: + +```bash +scrcpy --encoder _ +``` + +### Capture + +#### Recording It is possible to record the screen while mirroring: @@ -193,7 +259,6 @@ To disable mirroring while recording: scrcpy --no-display --record file.mp4 scrcpy -Nr file.mkv # interrupt recording with Ctrl+C -# Ctrl+C does not terminate properly on Windows, so disconnect the device ``` "Skipped frames" are recorded, even if they are not displayed in real time (for @@ -203,6 +268,59 @@ variation] does not impact the recorded file. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation +#### v4l2loopback + +On Linux, it is possible to send the video stream to a v4l2 loopback device, so +that the Android device can be opened like a webcam by any v4l2-capable tool. + +The module `v4l2loopback` must be installed: + +```bash +sudo apt install v4l2loopback-dkms +``` + +To create a v4l2 device: + +```bash +sudo modprobe v4l2loopback +``` + +This will create a new video device in `/dev/videoN`, where `N` is an integer +(more [options](https://github.com/umlaeute/v4l2loopback#options) are available +to create several devices or devices with specific IDs). + +To list the enabled devices: + +```bash +# requires v4l-utils package +v4l2-ctl --list-devices + +# simple but might be sufficient +ls /dev/video* +``` + +To start scrcpy using a v4l2 sink: + +```bash +scrcpy --v4l2-sink=/dev/videoN +scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window +scrcpy --v4l2-sink=/dev/videoN -N # short version +``` + +(replace `N` by the device ID, check with `ls /dev/video*`) + +Once enabled, you can open your video stream with a v4l2-capable tool: + +```bash +ffplay -i /dev/videoN +vlc v4l2:///dev/videoN # VLC might add some buffering delay +``` + +For example, you could capture the video within [OBS]. + +[OBS]: https://obsproject.com/fr + + ### Connection #### Wireless @@ -212,7 +330,13 @@ device over TCP/IP: 1. Plug the device into a USB port on your computer. 2. Connect the device to the same Wi-Fi as your computer. -3. Get your device IP address (in Settings → About phone → Status). +3. Get your device IP address, in Settings → About phone → Status, or by + executing this command: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + 4. Enable adb over TCP/IP on your device: `adb tcpip 5555`. 5. Unplug your device. 6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. @@ -280,6 +404,16 @@ scrcpy -s 192.168.0.1:5555 # short version You can start several instances of _scrcpy_ for several devices. +#### Autostart on device connection + +You could use [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + #### SSH tunnel To connect to a remote device, it is possible to connect a local `adb` client to @@ -298,6 +432,22 @@ From another terminal: scrcpy ``` +To avoid enabling remote port forwarding, you could force a forward connection +instead (notice the `-L` instead of `-R`): + +```bash +adb kill-server # kill the local adb server on 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal: + +```bash +scrcpy --force-adb-forward +``` + + Like for wireless connections, it may be useful to reduce quality: ``` @@ -347,7 +497,35 @@ scrcpy --fullscreen scrcpy -f # short version ``` -Fullscreen can then be toggled dynamically with `Ctrl`+`f`. +Fullscreen can then be toggled dynamically with MOD+f. + +#### Rotation + +The window may be rotated: + +```bash +scrcpy --rotation 1 +``` + +Possibles values are: + - `0`: no rotation + - `1`: 90 degrees counterclockwise + - `2`: 180 degrees + - `3`: 90 degrees clockwise + +The rotation can also be changed dynamically with MOD+ +_(left)_ and MOD+ _(right)_. + +Note that _scrcpy_ manages 3 different rotations: + - MOD+r requests the device to switch between portrait + and landscape (the current running app may refuse, if it does not support the + requested orientation). + - [`--lock-video-orientation`](#lock-video-orientation) changes the mirroring + orientation (the orientation of the video sent from the device to the + computer). This affects the recording. + - `--rotation` (or MOD+/MOD+) + rotates only the window content. This affects only the display, not the + recording. ### Other mirroring options @@ -362,6 +540,37 @@ scrcpy --no-control scrcpy -n ``` +#### Display + +If several displays are available, it is possible to select the display to +mirror: + +```bash +scrcpy --display 1 +``` + +The list of display ids can be retrieved by: + +```bash +adb shell dumpsys display # search "mDisplayId=" in the output +``` + +The secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored in read-only). + + +#### Stay awake + +To prevent the device to sleep after some delay when the device is plugged in: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +The initial state is restored when scrcpy is closed. + + #### Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -372,22 +581,23 @@ scrcpy --turn-screen-off scrcpy -S ``` -Or by pressing `Ctrl`+`o` at any time. +Or by pressing MOD+o at any time. -To turn it back on, press `POWER` (or `Ctrl`+`p`). +To turn it back on, press MOD+Shift+o. -#### Render expired frames +On Android, the `POWER` button always turns the screen on. For convenience, if +`POWER` is sent via scrcpy (via right-click or MOD+p), it +will force to turn the screen off after a small delay (on a best effort basis). +The physical `POWER` button will still cause the screen to be turned on. -By default, to minimize latency, _scrcpy_ always renders the last decoded frame -available, and drops any previous one. - -To force the rendering of all frames (at a cost of a possible increased -latency), use: +It can also be useful to prevent the device from sleeping: ```bash -scrcpy --render-expired-frames +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw ``` + #### Show touches For presentations, it may be useful to show physical touches (on the physical @@ -395,7 +605,8 @@ device). Android provides this feature in _Developers options_. -_Scrcpy_ provides an option to enable this feature on start and disable on exit: +_Scrcpy_ provides an option to enable this feature on start and restore the +initial value on exit: ```bash scrcpy --show-touches @@ -405,24 +616,78 @@ scrcpy -t Note that it only shows _physical_ touches (with the finger on the device). +#### Disable screensaver + +By default, scrcpy does not prevent the screensaver to run on the computer. + +To disable it: + +```bash +scrcpy --disable-screensaver +``` + + ### Input control #### Rotate device screen -Press `Ctrl`+`r` to switch between portrait and landscape modes. +Press MOD+r to switch between portrait and landscape +modes. Note that it rotates only if the application in foreground supports the requested orientation. #### Copy-paste -It is possible to synchronize clipboards between the computer and the device, in -both directions: +Any time the Android clipboard changes, it is automatically synchronized to the +computer clipboard. + +Any Ctrl shortcut is forwarded to the device. In particular: + - Ctrl+c typically copies + - Ctrl+x typically cuts + - Ctrl+v typically pastes (after computer-to-device + clipboard synchronization) + +This typically works as you expect. + +The actual behavior depends on the active application though. For example, +_Termux_ sends SIGINT on Ctrl+c instead, and _K-9 Mail_ +composes a new message. + +To copy, cut and paste in such cases (but only supported on Android >= 7): + - MOD+c injects `COPY` + - MOD+x injects `CUT` + - MOD+v injects `PASTE` (after computer-to-device + clipboard synchronization) + +In addition, MOD+Shift+v allows to inject the +computer clipboard text as a sequence of key events. This is useful when the +component does not accept text pasting (for example in _Termux_), but it can +break non-ASCII content. + +**WARNING:** Pasting the computer clipboard to the device (either via +Ctrl+v or MOD+v) copies the content +into the device clipboard. As a consequence, any Android application could read +its content. You should avoid to paste sensitive content (like passwords) that +way. + +Some devices do not behave as expected when setting the device clipboard +programmatically. An option `--legacy-paste` is provided to change the behavior +of Ctrl+v and MOD+v so that they +also inject the computer clipboard text as a sequence of key events (the same +way as MOD+Shift+v). + +#### Pinch-to-zoom + +To simulate "pinch-to-zoom": Ctrl+_click-and-move_. + +More precisely, hold Ctrl while pressing the left-click button. Until +the left-click button is released, all mouse movements scale and rotate the +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. - - `Ctrl`+`c` copies the device clipboard to the computer clipboard; - - `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard; - - `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but - breaks non-ASCII characters). #### Text injection preference @@ -446,6 +711,28 @@ scrcpy --prefer-text [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 +#### Key repeat + +By default, holding a key down generates repeated key events. This can cause +performance problems in some games, where these events are useless anyway. + +To avoid forwarding repeated key events: + +```bash +scrcpy --no-key-repeat +``` + + +#### Right-click and middle-click + +By default, right-click triggers BACK (or POWER on) and middle-click triggers +HOME. To disable these shortcuts and forward the clicks to the device instead: + +```bash +scrcpy --forward-all-clicks +``` + + ### File drop #### Install APK @@ -458,54 +745,90 @@ There is no visual feedback, a log is printed to the console. #### Push file to device -To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the -_scrcpy_ window. +To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK) +file to the _scrcpy_ window. There is no visual feedback, a log is printed to the console. The target directory can be changed on start: ```bash -scrcpy --push-target /sdcard/foo/bar/ +scrcpy --push-target=/sdcard/Movies/ ``` ### Audio forwarding -Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only). +Audio is not forwarded by _scrcpy_. Use [sndcpy]. Also see [issue #14]. -[USBaudio]: https://github.com/rom1v/usbaudio +[sndcpy]: https://github.com/rom1v/sndcpy [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 ## Shortcuts - | Action | Shortcut | Shortcut (macOS) - | -------------------------------------- |:----------------------------- |:----------------------------- - | Switch fullscreen mode | `Ctrl`+`f` | `Cmd`+`f` - | Resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | `Cmd`+`g` - | Resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ - | Click on `HOME` | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ - | Click on `BACK` | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ - | Click on `APP_SWITCH` | `Ctrl`+`s` | `Cmd`+`s` - | Click on `MENU` | `Ctrl`+`m` | `Ctrl`+`m` - | Click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ - | Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ - | Click on `POWER` | `Ctrl`+`p` | `Cmd`+`p` - | Power on | _Right-click²_ | _Right-click²_ - | Turn device screen off (keep mirroring)| `Ctrl`+`o` | `Cmd`+`o` - | Rotate device screen | `Ctrl`+`r` | `Cmd`+`r` - | Expand notification panel | `Ctrl`+`n` | `Cmd`+`n` - | Collapse notification panel | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` - | Copy device clipboard to computer | `Ctrl`+`c` | `Cmd`+`c` - | Paste computer clipboard to device | `Ctrl`+`v` | `Cmd`+`v` - | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` +In the following list, MOD is the shortcut modifier. By default, it's +(left) Alt or (left) Super. + +It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`, +`lalt`, `ralt`, `lsuper` and `rsuper`. For example: + +```bash +# use RCtrl for shortcuts +scrcpy --shortcut-mod=rctrl + +# use either LCtrl+LAlt or LSuper for shortcuts +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] is typically the Windows or Cmd key._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Action | Shortcut + | ------------------------------------------- |:----------------------------- + | Switch fullscreen mode | MOD+f + | Rotate display left | MOD+ _(left)_ + | Rotate display right | MOD+ _(right)_ + | Resize window to 1:1 (pixel-perfect) | MOD+g + | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ + | Click on `HOME` | MOD+h \| _Middle-click_ + | Click on `BACK` | MOD+b \| _Right-click²_ + | Click on `APP_SWITCH` | MOD+s \| _4th-click³_ + | Click on `MENU` (unlock screen) | MOD+m + | Click on `VOLUME_UP` | MOD+ _(up)_ + | Click on `VOLUME_DOWN` | MOD+ _(down)_ + | Click on `POWER` | MOD+p + | Power on | _Right-click²_ + | Turn device screen off (keep mirroring) | MOD+o + | 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³_ + | Collapse panels | MOD+Shift+n + | Copy to clipboard⁴ | MOD+c + | Cut to clipboard⁴ | MOD+x + | Synchronize clipboards and paste⁴ | MOD+v + | Inject computer clipboard text | MOD+Shift+v + | Enable/disable FPS counter (on stdout) | MOD+i + | Pinch-to-zoom | Ctrl+_click-and-move_ _¹Double-click on black borders to remove them._ -_²Right-click turns the screen on if it was off, presses BACK otherwise._ +_²Right-click turns the screen on if it was off, presses BACK otherwise._ +_³4th and 5th mouse buttons, if your mouse has them._ +_⁴Only on Android >= 7._ + +Shortcuts with repeated keys are executted by releasing and pressing the key a +second time. For example, to execute "Expand settings panel": + + 1. Press and keep pressing MOD. + 2. Then double-press n. + 3. Finally, release MOD. + +All Ctrl+_key_ shortcuts are forwarded to the device, so they are +handled by the active application. ## Custom paths @@ -513,7 +836,9 @@ _²Right-click turns the screen on if it was off, presses BACK otherwise._ To use a specific _adb_ binary, configure its path in the environment variable `ADB`: - ADB=/path/to/adb scrcpy +```bash +ADB=/path/to/adb scrcpy +``` To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. @@ -535,8 +860,6 @@ A colleague challenged me to find a name as unpronounceable as [gnirehtet]. See [BUILD]. -[BUILD]: BUILD.md - ## Common issues @@ -553,7 +876,7 @@ Read the [developers page]. ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2020 Romain Vimont + 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. @@ -574,3 +897,18 @@ Read the [developers page]. [article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ [article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ + +## Translations + +This README is available in other languages: + +- [Indonesian (Indonesia, `id`) - v1.16](README.id.md) +- [Italiano (Italiano, `it`) - v1.17](README.it.md) +- [日本語 (Japanese, `jp`) - v1.17](README.jp.md) +- [한국어 (Korean, `ko`) - v1.11](README.ko.md) +- [português brasileiro (Brazilian Portuguese, `pt-BR`) - v1.17](README.pt-br.md) +- [Español (Spanish, `sp`) - v1.17](README.sp.md) +- [简体中文 (Simplified Chinese, `zh-Hans`) - v1.17](README.zh-Hans.md) +- [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) + +Only this README file is guaranteed to be up-to-date. diff --git a/README.pt-br.md b/README.pt-br.md new file mode 100644 index 00000000..3549f0fb --- /dev/null +++ b/README.pt-br.md @@ -0,0 +1,792 @@ +_Apenas o [README](README.md) original é garantido estar atualizado._ + +# scrcpy (v1.17) + +Esta aplicação fornece exibição e controle de dispositivos Android conectados via +USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso _root_. +Funciona em _GNU/Linux_, _Windows_ e _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Foco em: + + - **leveza** (nativo, mostra apenas a tela do dispositivo) + - **performance** (30~60fps) + - **qualidade** (1920×1080 ou acima) + - **baixa latência** ([35~70ms][lowlatency]) + - **baixo tempo de inicialização** (~1 segundo para mostrar a primeira imagem) + - **não intrusivo** (nada é deixado instalado no dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisitos + +O dispositivo Android requer pelo menos a API 21 (Android 5.0). + +Tenha certeza de ter [ativado a depuração adb][enable-adb] no(s) seu(s) dispositivo(s). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +Em alguns dispositivos, você também precisa ativar [uma opção adicional][control] para +controlá-lo usando teclado e mouse. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Obter o app + +Packaging status + +### Linux + +No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04): + +``` +apt install scrcpy +``` + +Um pacote [Snap] está disponível: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Para Fedora, um pacote [COPR] está disponível: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +Você também pode [compilar o app manualmente][BUILD] (não se preocupe, não é tão +difícil). + + + +### Windows + +Para Windows, por simplicidade, um arquivo pré-compilado com todas as dependências +(incluindo `adb`) está disponível: + + - [README](README.md#windows) + +Também está disponível em [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # se você ainda não o tem +``` + +E no [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # se você ainda não o tem +``` + +[Scoop]: https://scoop.sh + +Você também pode [compilar o app manualmente][BUILD]. + + +### macOS + +A aplicação está disponível em [Homebrew]. Apenas instale-a: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem: + +```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 +brew cask install android-platform-tools +``` + +Você também pode [compilar o app manualmente][BUILD]. + + +## Executar + +Conecte um dispositivo Android e execute: + +```bash +scrcpy +``` + +Também aceita argumentos de linha de comando, listados por: + +```bash +scrcpy --help +``` + +## Funcionalidades + +### Configuração de captura + +#### Reduzir tamanho + +Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para +aumentar a performance. + +Para limitar ambos (largura e altura) para algum valor (ex: 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versão curta +``` + +A outra dimensão é calculada para que a proporção do dispositivo seja preservada. +Dessa forma, um dispositivo de 1920x1080 será espelhado em 1024x576. + + +#### Mudar bit-rate + +O bit-rate padrão é 8 Mbps. Para mudar o bit-rate do vídeo (ex: para 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versão curta +``` + +#### Limitar frame rate + +O frame rate de captura pode ser limitado: + +```bash +scrcpy --max-fps 15 +``` + +Isso é oficialmente suportado desde o Android 10, mas pode funcionar em versões anteriores. + +#### Cortar + +A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela. + +Isso é útil por exemplo, para espelhar apenas um olho do Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0) +``` + +Se `--max-size` também for especificado, o redimensionamento é aplicado após o corte. + + +#### Travar orientação do vídeo + + +Para travar a orientação do espelhamento: + +```bash +scrcpy --lock-video-orientation 0 # orientação natural +scrcpy --lock-video-orientation 1 # 90° sentido anti-horário +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° sentido horário +``` + +Isso afeta a orientação de gravação. + +A [janela também pode ser rotacionada](#rotação) independentemente. + + +#### Encoder + +Alguns dispositivos têm mais de um encoder, e alguns deles podem causar problemas ou +travar. É possível selecionar um encoder diferente: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Para listar os encoders disponíveis, você pode passar um nome de encoder inválido, o +erro dará os encoders disponíveis: + +```bash +scrcpy --encoder _ +``` + +### Gravando + +É possível gravar a tela enquanto ocorre o espelhamento: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Para desativar o espelhamento durante a gravação: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrompa a gravação com Ctrl+C +``` + +"Frames pulados" são gravados, mesmo que não sejam exibidos em tempo real (por +motivos de performance). Frames têm seu _horário carimbado_ no dispositivo, então [variação de atraso nos +pacotes][packet delay variation] não impacta o arquivo gravado. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Conexão + +#### Sem fio + +_Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se][connect] a um +dispositivo via TCP/IP: + +1. Conecte o dispositivo no mesmo Wi-Fi do seu computador. +2. Pegue o endereço IP do seu dispositivo, em Configurações → Sobre o telefone → Status, ou + executando este comando: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`. +4. Desconecte seu dispositivo. +5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua `DEVICE_IP`)_. +6. Execute `scrcpy` como de costume. + +Pode ser útil diminuir o bit-rate e a resolução: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versão curta +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Múltiplos dispositivos + +Se vários dispositivos são listados em `adb devices`, você deve especificar o _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versão curta +``` + +Se o dispositivo está conectado via TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versão curta +``` + +Você pode iniciar várias instâncias do _scrcpy_ para vários dispositivos. + +#### Iniciar automaticamente quando dispositivo é conectado + +Você pode usar [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Túnel SSH + +Para conectar-se a um dispositivo remoto, é possível conectar um cliente `adb` local a +um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo +_adb_): + +```bash +adb kill-server # encerra o servidor adb local em 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# mantenha isso aberto +``` + +De outro terminal: + +```bash +scrcpy +``` + +Para evitar ativar o encaminhamento de porta remota, você pode forçar uma conexão +de encaminhamento (note o `-L` em vez de `-R`): + +```bash +adb kill-server # encerra o servidor adb local em 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# mantenha isso aberto +``` + +De outro terminal: + +```bash +scrcpy --force-adb-forward +``` + + +Igual a conexões sem fio, pode ser útil reduzir a qualidade: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configuração de janela + +#### Título + +Por padrão, o título da janela é o modelo do dispositivo. Isso pode ser mudado: + +```bash +scrcpy --window-title 'Meu dispositivo' +``` + +#### Posição e tamanho + +A posição e tamanho iniciais da janela podem ser especificados: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Sem bordas + +Para desativar decorações de janela: + +```bash +scrcpy --window-borderless +``` + +#### Sempre no topo + +Para manter a janela do scrcpy sempre no topo: + +```bash +scrcpy --always-on-top +``` + +#### Tela cheia + +A aplicação pode ser iniciada diretamente em tela cheia: + +```bash +scrcpy --fullscreen +scrcpy -f # versão curta +``` + +Tela cheia pode ser alternada dinamicamente com MOD+f. + +#### Rotação + +A janela pode ser rotacionada: + +```bash +scrcpy --rotation 1 +``` + +Valores possíveis são: + - `0`: sem rotação + - `1`: 90 graus sentido anti-horário + - `2`: 180 graus + - `3`: 90 graus sentido horário + +A rotação também pode ser mudada dinamicamente com MOD+ +_(esquerda)_ e MOD+ _(direita)_. + +Note que _scrcpy_ controla 3 rotações diferentes: + - MOD+r requisita ao dispositivo para mudar entre retrato + e paisagem (a aplicação em execução pode se recusar, se ela não suporta a + orientação requisitada). + - [`--lock-video-orientation`](#travar-orientação-do-vídeo) muda a orientação de + espelhamento (a orientação do vídeo enviado pelo dispositivo para o + computador). Isso afeta a gravação. + - `--rotation` (ou MOD+/MOD+) + rotaciona apenas o conteúdo da janela. Isso afeta apenas a exibição, não a + gravação. + + +### Outras opções de espelhamento + +#### Apenas leitura + +Para desativar controles (tudo que possa interagir com o dispositivo: teclas de entrada, +eventos de mouse, arrastar e soltar arquivos): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### Display + +Se vários displays estão disponíveis, é possível selecionar o display para +espelhar: + +```bash +scrcpy --display 1 +``` + +A lista de IDs dos displays pode ser obtida por: + +``` +adb shell dumpsys display # busca "mDisplayId=" na saída +``` + +O display secundário pode apenas ser controlado se o dispositivo roda pelo menos Android +10 (caso contrário é espelhado como apenas leitura). + + +#### Permanecer ativo + +Para evitar que o dispositivo seja suspenso após um delay quando o dispositivo é conectado: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +O estado inicial é restaurado quando o scrcpy é fechado. + + +#### Desligar tela + +É possível desligar a tela do dispositivo durante o início do espelhamento com uma +opção de linha de comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +Ou apertando MOD+o a qualquer momento. + +Para ligar novamente, pressione MOD+Shift+o. + +No Android, o botão de `POWER` sempre liga a tela. Por conveniência, se +`POWER` é enviado via scrcpy (via clique-direito ou MOD+p), ele +forçará a desligar a tela após um delay pequeno (numa base de melhor esforço). +O botão `POWER` físico ainda causará a tela ser ligada. + +Também pode ser útil evitar que o dispositivo seja suspenso: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### 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 +físico). + +Android fornece esta funcionalidade nas _Opções do desenvolvedor_. + +_Scrcpy_ fornece esta opção de ativar esta funcionalidade no início e restaurar o +valor inicial no encerramento: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +Note que isto mostra apenas toques _físicos_ (com o dedo no dispositivo). + + +#### Desativar descanso de tela + +Por padrão, scrcpy não evita que o descanso de tela rode no computador. + +Para desativá-lo: + +```bash +scrcpy --disable-screensaver +``` + + +### Controle de entrada + +#### Rotacionar a tela do dispositivo + +Pressione MOD+r para mudar entre os modos retrato e +paisagem. + +Note que só será rotacionado se a aplicação em primeiro plano suportar a +orientação requisitada. + +#### Copiar-colar + +Sempre que a área de transferência do Android muda, é automaticamente sincronizada com a +área de transferência do computador. + +Qualquer atalho com Ctrl é encaminhado para o dispositivo. Em particular: + - Ctrl+c tipicamente copia + - Ctrl+x tipicamente recorta + - Ctrl+v tipicamente cola (após a sincronização de área de transferência + computador-para-dispositivo) + +Isso tipicamente funciona como esperado. + +O comportamento de fato depende da aplicação ativa, no entanto. Por exemplo, +_Termux_ envia SIGINT com Ctrl+c, e _K-9 Mail_ +compõe uma nova mensagem. + +Para copiar, recortar e colar em tais casos (mas apenas suportado no Android >= 7): + - MOD+c injeta `COPY` + - MOD+x injeta `CUT` + - MOD+v injeta `PASTE` (após a sincronização de área de transferência + computador-para-dispositivo) + +Em adição, MOD+Shift+v permite injetar o +texto da área de transferência do computador como uma sequência de eventos de tecla. Isso é útil quando o +componente não aceita colar texto (por exemplo no _Termux_), mas pode +quebrar conteúdo não-ASCII. + +**ADVERTÊNCIA:** Colar a área de transferência do computador para o dispositivo (tanto via +Ctrl+v quanto MOD+v) copia o conteúdo +para a área de transferência do dispositivo. Como consequência, qualquer aplicação Android pode ler +o seu conteúdo. Você deve evitar colar conteúdo sensível (como senhas) dessa +forma. + +Alguns dispositivos não se comportam como esperado quando a área de transferência é definida +programaticamente. Uma opção `--legacy-paste` é fornecida para mudar o comportamento +de Ctrl+v e MOD+v para que eles +também injetem o texto da área de transferência do computador como uma sequência de eventos de tecla (da mesma +forma que MOD+Shift+v). + +#### Pinçar para dar zoom + +Para simular "pinçar para dar zoom": Ctrl+_clicar-e-mover_. + +Mais precisamente, segure Ctrl enquanto pressiona o botão de clique-esquerdo. Até que +o botão de clique-esquerdo seja liberado, todos os movimentos do mouse ampliar e rotacionam o +conteúdo (se suportado pelo app) relativo ao centro da tela. + +Concretamente, scrcpy gera eventos adicionais de toque de um "dedo virtual" em +uma posição invertida em relação ao centro da tela. + + +#### Preferência de injeção de texto + +Existem dois tipos de [eventos][textevents] gerados ao digitar um texto: + - _eventos de tecla_, sinalizando que a tecla foi pressionada ou solta; + - _eventos de texto_, sinalizando que o texto foi inserido. + +Por padrão, letras são injetadas usando eventos de tecla, assim o teclado comporta-se +como esperado em jogos (normalmente para teclas WASD). + +Mas isso pode [causar problemas][prefertext]. Se você encontrar tal problema, você +pode evitá-lo com: + +```bash +scrcpy --prefer-text +``` + +(mas isso vai quebrar o comportamento do teclado em jogos) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Repetir tecla + +Por padrão, segurar uma tecla gera eventos de tecla repetidos. Isso pode causar +problemas de performance em alguns jogos, onde esses eventos são inúteis de qualquer forma. + +Para evitar o encaminhamento eventos de tecla repetidos: + +```bash +scrcpy --no-key-repeat +``` + + +#### Clique-direito e clique-do-meio + +Por padrão, clique-direito dispara BACK (ou POWER) e clique-do-meio dispara +HOME. Para desabilitar esses atalhos e encaminhar os cliques para o dispositivo: + +```bash +scrcpy --forward-all-clicks +``` + + +### Soltar arquivo + +#### Instalar APK + +Para instalar um APK, arraste e solte o arquivo APK (com extensão `.apk`) na janela +_scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + + +#### Enviar arquivo para dispositivo + +Para enviar um arquivo para `/sdcard/` no dispositivo, arraste e solte um arquivo (não-APK) para a +janela do _scrcpy_. + +Não existe feedback visual, um log é imprimido no console. + +O diretório alvo pode ser mudado ao iniciar: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### Encaminhamento de áudio + +Áudio não é encaminhado pelo _scrcpy_. Use [sndcpy]. + +Também veja [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Atalhos + +Na lista a seguir, MOD é o modificador de atalho. Por padrão, é +Alt (esquerdo) ou Super (esquerdo). + +Ele pode ser mudado usando `--shortcut-mod`. Possíveis teclas são `lctrl`, `rctrl`, +`lalt`, `ralt`, `lsuper` e `rsuper`. Por exemplo: + +```bash +# usar RCtrl para atalhos +scrcpy --shortcut-mod=rctrl + +# usar tanto LCtrl+LAlt quanto LSuper para atalhos +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] é tipicamente a tecla Windows ou Cmd._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Ação | Atalho + | ------------------------------------------- |:----------------------------- + | 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¹_ + | 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 `VOLUME_UP` | MOD+ _(cima)_ + | Clicar em `VOLUME_DOWN` | MOD+ _(baixo)_ + | Clicar em `POWER` | MOD+p + | Ligar | _Clique-direito²_ + | 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 + | 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_ + +_¹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._ + +Todos os atalhos Ctrl+_tecla_ são encaminhados para o dispositivo, para que eles sejam +tratados pela aplicação ativa. + + +## Caminhos personalizados + +Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente +`ADB`: + + ADB=/caminho/para/adb scrcpy + +Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em +`SCRCPY_SERVER_PATH`. + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## Por quê _scrcpy_? + +Um colega me desafiou a encontrar um nome tão impronunciável quanto [gnirehtet]. + +[`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## Como compilar? + +Veja [BUILD]. + +[BUILD]: BUILD.md + + +## Problemas comuns + +Veja o [FAQ](FAQ.md). + + +## Desenvolvedores + +Leia a [página dos desenvolvedores][developers page]. + +[developers page]: DEVELOP.md + + +## Licença + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-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. + +## Artigos + +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/README.sp.md b/README.sp.md new file mode 100644 index 00000000..6f76a7be --- /dev/null +++ b/README.sp.md @@ -0,0 +1,743 @@ +Solo se garantiza que el archivo [README](README.md) original esté actualizado. + +# scrcpy (v1.17) + +Esta aplicación proporciona imagen y control de un dispositivo Android conectado +por USB (o [por TCP/IP][article-tcpip]). No requiere acceso _root_. +Compatible con _GNU/Linux_, _Windows_ y _macOS_. + +![screenshot](assets/screenshot-debian-600.jpg) + +Sus características principales son: + + - **ligero** (nativo, solo muestra la imagen del dispositivo) + - **desempeño** (30~60fps) + - **calidad** (1920×1080 o superior) + - **baja latencia** ([35~70ms][lowlatency]) + - **corto tiempo de inicio** (~1 segundo para mostrar la primera imagen) + - **no intrusivo** (no se deja nada instalado en el dispositivo) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## Requisitos + +El dispositivo Android requiere como mínimo API 21 (Android 5.0). + +Asegurate de [habilitar el adb debugging][enable-adb] en tu(s) dispositivo(s). + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +En algunos dispositivos, también necesitas habilitar [una opción adicional][control] para controlarlo con el teclado y ratón. + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## Consigue la app + +Packaging status + +### Resumen + + - Linux: `apt install scrcpy` + - Windows: [download](README.md#windows) + - macOS: `brew install scrcpy` + +Construir desde la fuente: [BUILD] ([proceso simplificado][BUILD_simple]) + +[BUILD]: BUILD.md +[BUILD_simple]: BUILD.md#simple + + +### Linux + +En Debian (_test_ y _sid_ por ahora) y Ubuntu (20.04): + +``` +apt install scrcpy +``` + +Hay un paquete [Snap]: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +Para Fedora, hay un paquete [COPR]: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +Para Arch Linux, hay un paquete [AUR]: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +Para Gentoo, hay un paquete [Ebuild]: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +También puedes [construir la aplicación manualmente][BUILD] ([proceso simplificado][BUILD_simple]). + + +### Windows + +Para Windows, por simplicidad, hay un pre-compilado con todas las dependencias +(incluyendo `adb`): + + - [README](README.md#windows) + +También está disponible en [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # si aún no está instalado +``` + +Y en [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # si aún no está instalado +``` + +[Scoop]: https://scoop.sh + +También puedes [construir la aplicación manualmente][BUILD]. + + +### macOS + +La aplicación está disponible en [Homebrew]. Solo instalala: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +Necesitarás `adb`, accesible desde `PATH`. Si aún no lo tienes: + +```bash +brew install android-platform-tools +``` + +También está disponible en [MacPorts], que configurará el adb automáticamente: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + + +También puedes [construir la aplicación manualmente][BUILD]. + + +## Ejecutar + +Enchufa el dispositivo Android, y ejecuta: + +```bash +scrcpy +``` + +Acepta argumentos desde la línea de comandos, listados en: + +```bash +scrcpy --help +``` + +## Características + +### Capturar configuración + +#### Reducir la definición + +A veces es útil reducir la definición de la imagen del dispositivo Android para aumentar el desempeño. + +Para limitar el ancho y la altura a un valor específico (ej. 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # versión breve +``` + +La otra dimensión es calculada para conservar el aspect ratio del dispositivo. +De esta forma, un dispositivo en 1920×1080 será transmitido a 1024×576. + + +#### Cambiar el bit-rate + +El bit-rate por defecto es 8 Mbps. Para cambiar el bit-rate del video (ej. a 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # versión breve +``` + +#### Limitar los fps + +El fps puede ser limitado: + +```bash +scrcpy --max-fps 15 +``` + +Es oficialmente soportado desde Android 10, pero puede funcionar en versiones anteriores. + +#### Recortar + +La imagen del dispositivo puede ser recortada para transmitir solo una parte de la pantalla. + +Por ejemplo, puede ser útil para transmitir la imagen de un solo ojo del Oculus Go: + +```bash +scrcpy --crop 1224:1440:0:0 # 1224x1440 con coordenadas de origen en (0,0) +``` + +Si `--max-size` también está especificado, el cambio de tamaño es aplicado después de cortar. + + +#### Fijar la rotación del video + + +Para fijar la rotación de la transmisión: + +```bash +scrcpy --lock-video-orientation 0 # orientación normal +scrcpy --lock-video-orientation 1 # 90° contrarreloj +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 90° sentido de las agujas del reloj +``` + +Esto afecta la rotación de la grabación. + +La [ventana también puede ser rotada](#rotación) independientemente. + + +#### Codificador + +Algunos dispositivos pueden tener más de una rotación, y algunos pueden causar problemas o errores. Es posible seleccionar un codificador diferente: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +Para listar los codificadores disponibles, puedes pasar un nombre de codificador inválido, el error te dará los codificadores disponibles: + +```bash +scrcpy --encoder _ +``` + +### Grabación + +Es posible grabar la pantalla mientras se transmite: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +Para grabar sin transmitir la pantalla: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# interrumpe la grabación con Ctrl+C +``` + +"Skipped frames" son grabados, incluso si no son mostrados en tiempo real (por razones de desempeño). Los frames tienen _marcas de tiempo_ en el dispositivo, por lo que el "[packet delay +variation]" no impacta el archivo grabado. + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### Conexión + +#### Inalámbrica + +_Scrcpy_ usa `adb` para comunicarse con el dispositivo, y `adb` puede [conectarse] vía TCP/IP: + +1. Conecta el dispositivo al mismo Wi-Fi que tu computadora. +2. Obtén la dirección IP del dispositivo, en Ajustes → Acerca del dispositivo → Estado, o ejecutando este comando: + + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. Habilita adb vía TCP/IP en el dispositivo: `adb tcpip 5555`. +4. Desenchufa el dispositivo. +5. Conéctate a tu dispositivo: `adb connect IP_DEL_DISPOSITIVO:5555` _(reemplaza `IP_DEL_DISPOSITIVO`)_. +6. Ejecuta `scrcpy` con normalidad. + +Podría resultar útil reducir el bit-rate y la definición: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # versión breve +``` + +[conectarse]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### Múltiples dispositivos + +Si hay muchos dispositivos listados en `adb devices`, será necesario especificar el _número de serie_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # versión breve +``` + +Si el dispositivo está conectado por TCP/IP: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # versión breve +``` + +Puedes iniciar múltiples instancias de _scrcpy_ para múltiples dispositivos. + +#### Autoiniciar al detectar dispositivo + +Puedes utilizar [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### Túnel SSH + +Para conectarse a un dispositivo remoto, es posible conectar un cliente local de `adb` a un servidor remoto `adb` (siempre y cuando utilicen la misma versión de protocolos _adb_): + +```bash +adb kill-server # cierra el servidor local adb en 5037 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# conserva este servidor abierto +``` + +Desde otra terminal: + +```bash +scrcpy +``` + +Para evitar habilitar "remote port forwarding", puedes forzar una "forward connection" (nótese el argumento `-L` en vez de `-R`): + +```bash +adb kill-server # cierra el servidor local adb en 5037 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# conserva este servidor abierto +``` + +Desde otra terminal: + +```bash +scrcpy --force-adb-forward +``` + + +Al igual que las conexiones inalámbricas, puede resultar útil reducir la calidad: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### Configuración de la ventana + +#### Título + +Por defecto, el título de la ventana es el modelo del dispositivo. Puede ser modificado: + +```bash +scrcpy --window-title 'My device' +``` + +#### Posición y tamaño + +La posición y tamaño inicial de la ventana puede ser especificado: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### Sin bordes + +Para deshabilitar el diseño de la ventana: + +```bash +scrcpy --window-borderless +``` + +#### Siempre adelante + +Para mantener la ventana de scrcpy siempre adelante: + +```bash +scrcpy --always-on-top +``` + +#### Pantalla completa + +La aplicación puede ser iniciada en pantalla completa: + +```bash +scrcpy --fullscreen +scrcpy -f # versión breve +``` + +Puede entrar y salir de la pantalla completa con la combinación MOD+f. + +#### Rotación + +Se puede rotar la ventana: + +```bash +scrcpy --rotation 1 +``` + +Los valores posibles son: + - `0`: sin rotación + - `1`: 90 grados contrarreloj + - `2`: 180 grados + - `3`: 90 grados en sentido de las agujas del reloj + +La rotación también puede ser modificada con la combinación de teclas MOD+ _(izquierda)_ y MOD+ _(derecha)_. + +Nótese que _scrcpy_ maneja 3 diferentes rotaciones: + - MOD+r solicita al dispositivo cambiar entre vertical y horizontal (la aplicación en uso puede rechazarlo si no soporta la orientación solicitada). + - [`--lock-video-orientation`](#fijar-la-rotación-del-video) cambia la rotación de la transmisión (la orientación del video enviado a la PC). Esto afecta a la grabación. + - `--rotation` (o MOD+/MOD+) rota solo el contenido de la imagen. Esto solo afecta a la imagen mostrada, no a la grabación. + + +### Otras opciones menores + +#### Solo lectura ("Read-only") + +Para deshabilitar los controles (todo lo que interactúe con el dispositivo: eventos del teclado, eventos del mouse, arrastrar y soltar archivos): + +```bash +scrcpy --no-control +scrcpy -n # versión breve +``` + +#### Pantalla + +Si múltiples pantallas están disponibles, es posible elegir cual transmitir: + +```bash +scrcpy --display 1 +``` + +Los ids de las pantallas se pueden obtener con el siguiente comando: + +```bash +adb shell dumpsys display # busque "mDisplayId=" en la respuesta +``` + +La segunda pantalla solo puede ser manejada si el dispositivo cuenta con Android 10 (en caso contrario será transmitida en el modo solo lectura). + + +#### Permanecer activo + +Para evitar que el dispositivo descanse después de un tiempo mientras está conectado: + +```bash +scrcpy --stay-awake +scrcpy -w # versión breve +``` + +La configuración original se restaura al cerrar scrcpy. + + +#### Apagar la pantalla + +Es posible apagar la pantalla mientras se transmite al iniciar con el siguiente comando: + +```bash +scrcpy --turn-screen-off +scrcpy -S # versión breve +``` + +O presionando MOD+o en cualquier momento. + +Para volver a prenderla, presione MOD+Shift+o. + +En Android, el botón de `POWER` siempre prende la pantalla. Por conveniencia, si `POWER` es enviado vía scrcpy (con click-derecho o MOD+p), esto forzará a apagar la pantalla con un poco de atraso (en la mejor de las situaciones). El botón físico `POWER` seguirá prendiendo la pantalla. + +También puede resultar útil para evitar que el dispositivo entre en inactividad: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw # versión breve +``` + + +#### Renderizar frames vencidos + +Por defecto, para minimizar la latencia, _scrcpy_ siempre renderiza el último frame disponible decodificado, e ignora cualquier frame anterior. + +Para forzar el renderizado de todos los frames (a costo de posible aumento de latencia), use: + +```bash +scrcpy --render-expired-frames +``` + +#### Mostrar clicks + +Para presentaciones, puede resultar útil mostrar los clicks físicos (en el dispositivo físicamente). + +Android provee esta opción en _Opciones para desarrolladores_. + +_Scrcpy_ provee una opción para habilitar esta función al iniciar la aplicación y restaurar el valor original al salir: + +```bash +scrcpy --show-touches +scrcpy -t # versión breve +``` + +Nótese que solo muestra los clicks _físicos_ (con el dedo en el dispositivo). + + +#### Desactivar protector de pantalla + +Por defecto, scrcpy no evita que el protector de pantalla se active en la computadora. + +Para deshabilitarlo: + +```bash +scrcpy --disable-screensaver +``` + + +### Control + +#### Rotar pantalla del dispositivo + +Presione MOD+r para cambiar entre posición vertical y horizontal. + +Nótese que solo rotará si la aplicación activa soporta la orientación solicitada. + +#### Copiar y pegar + +Cuando que el portapapeles de Android cambia, automáticamente se sincroniza al portapapeles de la computadora. + +Cualquier shortcut con Ctrl es enviado al dispositivo. En particular: + - Ctrl+c normalmente copia + - Ctrl+x normalmente corta + - Ctrl+v normalmente pega (después de la sincronización de portapapeles entre la computadora y el dispositivo) + +Esto normalmente funciona como es esperado. + +Sin embargo, este comportamiento depende de la aplicación en uso. Por ejemplo, _Termux_ envía SIGINT con Ctrl+c, y _K-9 Mail_ crea un nuevo mensaje. + +Para copiar, cortar y pegar, en tales casos (solo soportado en Android >= 7): + - MOD+c inyecta `COPY` + - MOD+x inyecta `CUT` + - MOD+v inyecta `PASTE` (después de la sincronización de portapapeles entre la computadora y el dispositivo) + +Además, MOD+Shift+v permite inyectar el texto en el portapapeles de la computadora como una secuencia de teclas. Esto es útil cuando el componente no acepta pegado de texto (por ejemplo en _Termux_), pero puede romper caracteres no pertenecientes a ASCII. + +**AVISO:** Pegar de la computadora al dispositivo (tanto con Ctrl+v o MOD+v) copia el contenido al portapapeles del dispositivo. Como consecuencia, cualquier aplicación de Android puede leer su contenido. Debería evitar pegar contenido sensible (como contraseñas) de esta forma. + +Algunos dispositivos no se comportan como es esperado al establecer el portapapeles programáticamente. La opción `--legacy-paste` está disponible para cambiar el comportamiento de Ctrl+v y MOD+v para que también inyecten el texto del portapapeles de la computadora como una secuencia de teclas (de la misma forma que MOD+Shift+v). + +#### Pellizcar para zoom + +Para simular "pinch-to-zoom": Ctrl+_click-y-mover_. + +Más precisamente, mantén Ctrl mientras presionas botón izquierdo. Hasta que no se suelte el botón, todos los movimientos del mouse cambiarán el tamaño y rotación del contenido (si es soportado por la app en uso) respecto al centro de la pantalla. + +Concretamente, scrcpy genera clicks adicionales con un "dedo virtual" en la posición invertida respecto al centro de la pantalla. + + +#### Preferencias de inyección de texto + +Existen dos tipos de [eventos][textevents] generados al escribir texto: + - _key events_, marcando si la tecla es presionada o soltada; + - _text events_, marcando si un texto fue introducido. + +Por defecto, las letras son inyectadas usando _key events_, para que el teclado funcione como es esperado en juegos (típicamente las teclas WASD). + +Pero esto puede [causar problemas][prefertext]. Si encuentras tales problemas, los puedes evitar con: + +```bash +scrcpy --prefer-text +``` + +(Pero esto romperá el comportamiento del teclado en los juegos) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### Repetir tecla + +Por defecto, mantener una tecla presionada genera múltiples _key events_. Esto puede causar problemas de desempeño en algunos juegos, donde estos eventos no tienen sentido de todos modos. + +Para evitar enviar _key events_ repetidos: + +```bash +scrcpy --no-key-repeat +``` + + +#### Botón derecho y botón del medio + +Por defecto, botón derecho ejecuta RETROCEDER (o ENCENDIDO) y botón del medio INICIO. Para inhabilitar estos atajos y enviar los clicks al dispositivo: + +```bash +scrcpy --forward-all-clicks +``` + + +### Arrastrar y soltar archivos + +#### Instalar APKs + +Para instalar un APK, arrastre y suelte el archivo APK (terminado en `.apk`) a la ventana de _scrcpy_. + +No hay respuesta visual, un mensaje se escribirá en la consola. + + +#### Enviar archivos al dispositivo + +Para enviar un archivo a `/sdcard/` en el dispositivo, arrastre y suelte un archivo (no APK) a la ventana de _scrcpy_. + +No hay respuesta visual, un mensaje se escribirá en la consola. + +El directorio de destino puede ser modificado al iniciar: + +```bash +scrcpy --push-target=/sdcard/Download/ +``` + + +### Envío de Audio + +_Scrcpy_ no envía el audio. Use [sndcpy]. + +También lea [issue #14]. + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## Atajos + +En la siguiente lista, MOD es el atajo modificador. Por defecto es Alt (izquierdo) o Super (izquierdo). + +Se puede modificar usando `--shortcut-mod`. Las posibles teclas son `lctrl` (izquierdo), `rctrl` (derecho), `lalt` (izquierdo), `ralt` (derecho), `lsuper` (izquierdo) y `rsuper` (derecho). Por ejemplo: + +```bash +# use RCtrl para los atajos +scrcpy --shortcut-mod=rctrl + +# use tanto LCtrl+LAlt o LSuper para los atajos +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] es generalmente la tecla Windows o Cmd._ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Acción | Atajo + | ------------------------------------------- |:----------------------------- + | Alterne entre pantalla compelta | MOD+f + | Rotar pantalla hacia la izquierda | MOD+ _(izquierda)_ + | Rotar pantalla hacia la derecha | MOD+ _(derecha)_ + | Ajustar ventana a 1:1 ("pixel-perfect") | MOD+g + | Ajustar ventana para quitar los bordes negros| MOD+w \| _Doble click¹_ + | Click en `INICIO` | MOD+h \| _Botón del medio_ + | Click en `RETROCEDER` | MOD+b \| _Botón derecho²_ + | Click en `CAMBIAR APLICACIÓN` | MOD+s + | Click en `MENÚ` (desbloquear pantalla) | MOD+m + | Click en `SUBIR VOLUMEN` | MOD+ _(arriba)_ + | Click en `BAJAR VOLUME` | MOD+ _(abajo)_ + | Click en `ENCENDIDO` | MOD+p + | Encendido | _Botón derecho²_ + | Apagar pantalla (manteniendo la transmisión)| MOD+o + | Encender pantalla | MOD+Shift+o + | Rotar pantalla del dispositivo | MOD+r + | Abrir panel de notificaciones | MOD+n + | Cerrar panel de notificaciones | MOD+Shift+n + | Copiar al portapapeles³ | MOD+c + | Cortar al portapapeles³ | MOD+x + | Synchronizar portapapeles y pegar³ | MOD+v + | inyectar texto del portapapeles de la PC | MOD+Shift+v + | Habilitar/Deshabilitar contador de FPS (en stdout) | MOD+i + | Pellizcar para zoom | Ctrl+_click-y-mover_ + +_¹Doble click en los bordes negros para eliminarlos._ +_²Botón derecho enciende la pantalla si estaba apagada, sino ejecuta RETROCEDER._ +_³Solo en Android >= 7._ + +Todos los atajos Ctrl+_tecla_ son enviados al dispositivo para que sean manejados por la aplicación activa. + + +## Path personalizado + +Para usar un binario de _adb_ en particular, configure el path `ADB` en las variables de entorno: + +```bash +ADB=/path/to/adb scrcpy +``` + +Para sobreescribir el path del archivo `scrcpy-server`, configure el path en `SCRCPY_SERVER_PATH`. + + +## ¿Por qué _scrcpy_? + +Un colega me retó a encontrar un nombre tan impronunciable como [gnirehtet]. + +[`strcpy`] copia un **str**ing; `scrcpy` copia un **scr**een. + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## ¿Cómo construir (BUILD)? + +Véase [BUILD] (en inglés). + + +## Problemas generales + +Vea las [preguntas frecuentes (en inglés)](FAQ.md). + + +## Desarrolladores + +Lea la [hoja de desarrolladores (en inglés)](DEVELOP.md). + + +## Licencia + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-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. + +## Artículos + +- [Introducing scrcpy][article-intro] (en inglés) +- [Scrcpy now works wirelessly][article-tcpip] (en inglés) + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/README.zh-Hans.md b/README.zh-Hans.md new file mode 100644 index 00000000..bdd8023c --- /dev/null +++ b/README.zh-Hans.md @@ -0,0 +1,732 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +只有原版的[README](README.md)会保持最新。 + +本文根据[ed130e05]进行翻译。 + +[ed130e05]: https://github.com/Genymobile/scrcpy/blob/ed130e05d55615d6014d93f15cfcb92ad62b01d8/README.md + +# scrcpy (v1.17) + +本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 + +![screenshot](assets/screenshot-debian-600.jpg) + +它专注于: + + - **轻量** (原生,仅显示设备屏幕) + - **性能** (30~60fps) + - **质量** (分辨率可达 1920×1080 或更高) + - **低延迟** ([35~70ms][lowlatency]) + - **快速启动** (最快 1 秒内即可显示第一帧) + - **无侵入性** (不会在设备上遗留任何程序) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 系统要求 + +安卓设备最低需要支持 API 21 (Android 5.0)。 + +确保设备已[开启 adb 调试][enable-adb]。 + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + +在某些设备上,还需要开启[额外的选项][control]以使用鼠标和键盘进行控制。 + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## 获取本程序 + +Packaging status + +### Linux + +在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上: + +``` +apt install scrcpy +``` + +我们也提供 [Snap] 包: [`scrcpy`][snap-link]。 + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +对 Fedora 我们提供 [COPR] 包: [`scrcpy`][copr-link]。 + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +对 Arch Linux 我们提供 [AUR] 包: [`scrcpy`][aur-link]。 + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +对 Gentoo 我们提供 [Ebuild] 包:[`scrcpy/`][ebuild-link]。 + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +您也可以[自行构建][BUILD] (不必担心,这并不困难)。 + + + +### Windows + +在 Windows 上,简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 + + - [README](README.md#windows) + +也可以使用 [Chocolatey]: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # 如果还没有 adb +``` + +或者 [Scoop]: + +```bash +scoop install scrcpy +scoop install adb # 如果还没有 adb +``` + +[Scoop]: https://scoop.sh + +您也可以[自行构建][BUILD]。 + + +### macOS + +本程序已发布到 [Homebrew]。直接安装即可: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +你还需要在 `PATH` 内有 `adb`。如果还没有: + +```bash +# Homebrew >= 2.6.0 +brew install --cask android-platform-tools + +# Homebrew < 2.6.0 +brew cask install android-platform-tools +``` + +您也可以[自行构建][BUILD]。 + + +## 运行 + +连接安卓设备,然后执行: + +```bash +scrcpy +``` + +本程序支持命令行参数,查看参数列表: + +```bash +scrcpy --help +``` + +## 功能介绍 + +### 捕获设置 + +#### 降低分辨率 + +有时候,可以通过降低镜像的分辨率来提高性能。 + +要同时限制宽度和高度到某个值 (例如 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 简写 +``` + +另一边会被按比例缩小以保持设备的显示比例。这样,1920×1080 分辨率的设备会以 1024×576 的分辨率进行镜像。 + + +#### 修改码率 + +默认码率是 8Mbps。要改变视频的码率 (例如改为 2Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 简写 +``` + +#### 限制帧率 + +要限制捕获的帧率: + +```bash +scrcpy --max-fps 15 +``` + +本功能从 Android 10 开始才被官方支持,但在一些旧版本中也能生效。 + +#### 画面裁剪 + +可以对设备屏幕进行裁剪,只镜像屏幕的一部分。 + +例如可以只镜像 Oculus Go 的一只眼睛。 + +```bash +scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素 +``` + +如果同时指定了 `--max-size`,会先进行裁剪,再进行缩放。 + + +#### 锁定屏幕方向 + + +要锁定镜像画面的方向: + +```bash +scrcpy --lock-video-orientation 0 # 自然方向 +scrcpy --lock-video-orientation 1 # 逆时针旋转 90° +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 顺时针旋转 90° +``` + +只影响录制的方向。 + +[窗口可以独立旋转](#旋转)。 + + +#### 编码器 + +一些设备内置了多种编码器,但是有的编码器会导致问题或崩溃。可以手动选择其它编码器: + +```bash +scrcpy --encoder OMX.qcom.video.encoder.avc +``` + +要列出可用的编码器,可以指定一个不存在的编码器名称,错误信息中会包含所有的编码器: + +```bash +scrcpy --encoder _ +``` + +### 屏幕录制 + +可以在镜像的同时录制视频: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +仅录制,不显示镜像: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# 按 Ctrl+C 停止录制 +``` + +录制时会包含“被跳过的帧”,即使它们由于性能原因没有实时显示。设备会为每一帧打上 _时间戳_ ,所以 [包时延抖动][packet delay variation] 不会影响录制的文件。 + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### 连接 + +#### 无线 + +_Scrcpy_ 使用 `adb` 与设备通信,并且 `adb` 支持通过 TCP/IP [连接]到设备: + +1. 将设备和电脑连接至同一 Wi-Fi。 +2. 打开 设置 → 关于手机 → 状态信息,获取设备的 IP 地址,也可以执行以下的命令: + ```bash + adb shell ip route | awk '{print $9}' + ``` + +3. 启用设备的网络 adb 功能 `adb tcpip 5555`。 +4. 断开设备的 USB 连接。 +5. 连接到您的设备:`adb connect DEVICE_IP:5555` _(将 `DEVICE_IP` 替换为设备 IP)_. +6. 正常运行 `scrcpy`。 + +可能需要降低码率和分辨率: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 简写 +``` + +[连接]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### 多设备 + +如果 `adb devices` 列出了多个设备,您必须指定设备的 _序列号_ : + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # 简写 +``` + +如果设备通过 TCP/IP 连接: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # 简写 +``` + +您可以同时启动多个 _scrcpy_ 实例以同时显示多个设备的画面。 + +#### 在设备连接时自动启动 + +您可以使用 [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### SSH 隧道 + +要远程连接到设备,可以将本地的 adb 客户端连接到远程的 adb 服务端 (需要两端的 _adb_ 协议版本相同): + +```bash +adb kill-server # 关闭本地 5037 端口上的 adb 服务端 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 保持该窗口开启 +``` + +在另一个终端: + +```bash +scrcpy +``` + +若要不使用远程端口转发,可以强制使用正向连接 (注意 `-L` 和 `-R` 的区别): + +```bash +adb kill-server # 关闭本地 5037 端口上的 adb 服务端 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# 保持该窗口开启 +``` + +在另一个终端: + +```bash +scrcpy --force-adb-forward +``` + + +类似无线网络连接,可能需要降低画面质量: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### 窗口设置 + +#### 标题 + +窗口的标题默认为设备型号。可以通过如下命令修改: + +```bash +scrcpy --window-title 'My device' +``` + +#### 位置和大小 + +您可以指定初始的窗口位置和大小: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### 无边框 + +关闭边框: + +```bash +scrcpy --window-borderless +``` + +#### 保持窗口在最前 + +您可以通过如下命令保持窗口在最前面: + +```bash +scrcpy --always-on-top +``` + +#### 全屏 + +您可以通过如下命令直接全屏启动scrcpy: + +```bash +scrcpy --fullscreen +scrcpy -f # 简写 +``` + +全屏状态可以通过 MOD+f 随时切换。 + +#### 旋转 + +可以通过以下命令旋转窗口: + +```bash +scrcpy --rotation 1 +``` + +可选的值有: + - `0`: 无旋转 + - `1`: 逆时针旋转 90° + - `2`: 旋转 180° + - `3`: 顺时针旋转 90° + +也可以使用 MOD+ _(左箭头)_ 和 MOD+ _(右箭头)_ 随时更改。 + +需要注意的是, _scrcpy_ 有三个不同的方向: + - MOD+r 请求设备在竖屏和横屏之间切换 (如果前台应用程序不支持请求的朝向,可能会拒绝该请求)。 + - [`--lock-video-orientation`](#锁定屏幕方向) 改变镜像的朝向 (设备传输到电脑的画面的朝向)。这会影响录制。 + - `--rotation` (或 MOD+/MOD+) 只旋转窗口的内容。这只影响显示,不影响录制。 + + +### 其他镜像设置 + +#### 只读 + +禁用电脑对设备的控制 (如键盘输入、鼠标事件和文件拖放): + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### 显示屏 + +如果设备有多个显示屏,可以选择要镜像的显示屏: + +```bash +scrcpy --display 1 +``` + +可以通过如下命令列出所有显示屏的 id: + +``` +adb shell dumpsys display # 在输出中搜索 “mDisplayId=” +``` + +控制第二显示屏需要设备运行 Android 10 或更高版本 (否则将在只读状态下镜像)。 + + +#### 保持常亮 + +阻止设备在连接时休眠: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +程序关闭时会恢复设备原来的设置。 + + +#### 关闭设备屏幕 + +可以通过以下的命令行参数在关闭设备屏幕的状态下进行镜像: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +或者在任何时候按 MOD+o。 + +要重新打开屏幕,按下 MOD+Shift+o. + +在Android上,`电源` 按钮始终能把屏幕打开。为了方便,对于在 _scrcpy_ 中发出的 `电源` 事件 (通过鼠标右键或 MOD+p),会 (尽最大的努力) 在短暂的延迟后将屏幕关闭。设备上的 `电源` 按钮仍然能打开设备屏幕。 + +还可以同时阻止设备休眠: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### 渲染过期帧 + +默认状态下,为了降低延迟, _scrcpy_ 永远渲染解码成功的最近一帧,并跳过前面任意帧。 + +强制渲染所有帧 (可能导致延迟变高): + +```bash +scrcpy --render-expired-frames +``` + +#### 显示触摸 + +在演示时,可能会需要显示物理触摸点 (在物理设备上的触摸点)。 + +Android 在 _开发者选项_ 中提供了这项功能。 + +_Scrcpy_ 提供一个选项可以在启动时开启这项功能并在退出时恢复初始设置: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +请注意这项功能只能显示 _物理_ 触摸 (用手指在屏幕上的触摸)。 + + +#### 关闭屏保 + +_Scrcpy_ 默认不会阻止电脑上开启的屏幕保护。 + +关闭屏幕保护: + +```bash +scrcpy --disable-screensaver +``` + + +### 输入控制 + +#### 旋转设备屏幕 + +使用 MOD+r 在竖屏和横屏模式之间切换。 + +需要注意的是,只有在前台应用程序支持所要求的模式时,才会进行切换。 + +#### 复制粘贴 + +每次安卓的剪贴板变化时,其内容都会被自动同步到电脑的剪贴板上。 + +所有的 Ctrl 快捷键都会被转发至设备。其中: + - Ctrl+c 通常执行复制 + - Ctrl+x 通常执行剪切 + - Ctrl+v 通常执行粘贴 (在电脑到设备的剪贴板同步完成之后) + +大多数时候这些按键都会执行以上的功能。 + +但实际的行为取决于设备上的前台程序。例如,_Termux_ 会在按下 Ctrl+c 时发送 SIGINT,又如 _K-9 Mail_ 会新建一封邮件。 + +要在这种情况下进行剪切,复制和粘贴 (仅支持 Android >= 7): + - MOD+c 注入 `COPY` (复制) + - MOD+x 注入 `CUT` (剪切) + - MOD+v 注入 `PASTE` (粘贴) (在电脑到设备的剪贴板同步完成之后) + +另外,MOD+Shift+v 会将电脑的剪贴板内容转换为一串按键事件输入到设备。在应用程序不接受粘贴时 (比如 _Termux_),这项功能可以派上一定的用场。不过这项功能可能会导致非 ASCII 编码的内容出现错误。 + +**警告:** 将电脑剪贴板的内容粘贴至设备 (无论是通过 Ctrl+v 还是 MOD+v) 都会将内容复制到设备的剪贴板。如此,任何安卓应用程序都能读取到。您应避免将敏感内容 (如密码) 通过这种方式粘贴。 + +一些设备不支持通过程序设置剪贴板。通过 `--legacy-paste` 选项可以修改 Ctrl+vMOD+v 的工作方式,使它们通过按键事件 (同 MOD+Shift+v) 来注入电脑剪贴板内容。 + +#### 双指缩放 + +模拟“双指缩放”:Ctrl+_按住并移动鼠标_。 + +更准确的说,在按住鼠标左键时按住 Ctrl。直到松开鼠标左键,所有鼠标移动将以屏幕中心为原点,缩放或旋转内容 (如果应用支持)。 + +实际上,_scrcpy_ 会在以屏幕中心对称的位置上生成由“虚拟手指”发出的额外触摸事件。 + + +#### 文字注入偏好 + +打字的时候,系统会产生两种[事件][textevents]: + - _按键事件_ ,代表一个按键被按下或松开。 + - _文本事件_ ,代表一个字符被输入。 + +程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作 (例如 WASD 键)。 + +但这也有可能[造成一些问题][prefertext]。如果您遇到了问题,可以通过以下方式避免: + +```bash +scrcpy --prefer-text +``` + +(这会导致键盘在游戏中工作不正常) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### 按键重复 + +默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这可能会导致性能问题。 + +避免转发重复按键事件: + +```bash +scrcpy --no-key-repeat +``` + + +#### 右键和中键 + +默认状态下,右键会触发返回键 (或电源键),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备: + +```bash +scrcpy --forward-all-clicks +``` + + +### 文件拖放 + +#### 安装APK + +将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。 + +该操作在屏幕上不会出现任何变化,而会在控制台输出一条日志。 + + +#### 将文件推送至设备 + +要推送文件到设备的 `/sdcard/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。 + +该操作没有可见的响应,只会在控制台输出日志。 + +在启动时可以修改目标目录: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### 音频转发 + +_Scrcpy_ 不支持音频。请使用 [sndcpy]. + +另外请阅读 [issue #14]。 + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## 快捷键 + +在以下列表中, MOD 是快捷键的修饰键。 +默认是 (左) Alt 或 (左) Super。 + +您可以使用 `--shortcut-mod` 来修改。可选的按键有 `lctrl`、`rctrl`、`lalt`、`ralt`、`lsuper` 和 `rsuper`。例如: + +```bash +# 使用右 Ctrl 键 +scrcpy --shortcut-mod=rctrl + +# 使用左 Ctrl 键 + 左 Alt 键,或 Super 键 +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[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+_按住并移动鼠标_ | + +_¹双击黑边可以去除黑边_ +_²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ +_³需要安卓版本 Android >= 7。_ + +所有的 Ctrl+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。 + + +## 自定义路径 + +要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`: + + ADB=/path/to/adb scrcpy + +要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。 + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## 为什么叫 _scrcpy_ ? + +一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。 + +[`strcpy`] 复制一个 **str**ing; `scrcpy` 复制一个 **scr**een。 + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## 如何构建? + +请查看[BUILD]。 + +[BUILD]: BUILD.md + + +## 常见问题 + +请查看[FAQ](FAQ.md)。 + + +## 开发者 + +请查看[开发者页面]。 + +[开发者页面]: DEVELOP.md + + +## 许可协议 + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-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. + +## 相关文章 + +- [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-Hant.md b/README.zh-Hant.md new file mode 100644 index 00000000..c0e30254 --- /dev/null +++ b/README.zh-Hant.md @@ -0,0 +1,702 @@ +_Only the original [README](README.md) is guaranteed to be up-to-date._ + +_只有原版的 [README](README.md)是保證最新的。_ + + +本文件翻譯時點: [521f2fe](https://github.com/Genymobile/scrcpy/commit/521f2fe994019065e938aa1a54b56b4f10a4ac4a#diff-04c6e90faac2675aa89e2176d2eec7d8) + + +# scrcpy (v1.15) + +Scrcpy 可以透過 USB、或是 [TCP/IP][article-tcpip] 來顯示或控制 Android 裝置。且 scrcpy 不需要 _root_ 權限。 + +Scrcpy 目前支援 _GNU/Linux_、_Windows_ 和 _macOS_。 + +![screenshot](assets/screenshot-debian-600.jpg) + +特色: + + - **輕量** (只顯示裝置螢幕) + - **效能** (30~60fps) + - **品質** (1920×1080 或更高) + - **低延遲** ([35~70ms][lowlatency]) + - **快速啟動** (~1 秒就可以顯示第一個畫面) + - **非侵入性** (不安裝、留下任何東西在裝置上) + +[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 + + +## 需求 + +Android 裝置必須是 API 21+ (Android 5.0+)。 + +請確認裝置上的 [adb 偵錯 (通常位於開發者模式內)][enable-adb] 已啟用。 + +[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling + + +在部分的裝置上,你也必須啟用[特定的額外選項][control]才能使用鍵盤和滑鼠控制。 + +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 + + +## 下載/獲取軟體 + + +### Linux + +Debian (目前支援 _testing_ 和 _sid_) 和 Ubuntu (20.04): + +``` +apt install scrcpy +``` + +[Snap] 上也可以下載: [`scrcpy`][snap-link]. + +[snap-link]: https://snapstats.org/snaps/scrcpy + +[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) + +在 Fedora 上也可以使用 [COPR] 下載: [`scrcpy`][copr-link]. + +[COPR]: https://fedoraproject.org/wiki/Category:Copr +[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ + +在 Arch Linux 上也可以使用 [AUR] 下載: [`scrcpy`][aur-link]. + +[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository +[aur-link]: https://aur.archlinux.org/packages/scrcpy/ + +在 Gentoo 上也可以使用 [Ebuild] 下載: [`scrcpy/`][ebuild-link]. + +[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild +[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy + +你也可以自己[編譯 _Scrcpy_][BUILD]。別擔心,並沒有想像中的難。 + + + +### Windows + +為了保持簡單,Windows 用戶可以下載一個包含所有必需軟體 (包含 `adb`) 的壓縮包: + + - [README](README.md#windows) + +[Chocolatey] 上也可以下載: + +[Chocolatey]: https://chocolatey.org/ + +```bash +choco install scrcpy +choco install adb # 如果你還沒有安裝的話 +``` + +[Scoop] 上也可以下載: + +```bash +scoop install scrcpy +scoop install adb # 如果你還沒有安裝的話 +``` + +[Scoop]: https://scoop.sh + +你也可以自己[編譯 _Scrcpy_][BUILD]。 + + +### macOS + +_Scrcpy_ 可以在 [Homebrew] 上直接安裝: + +[Homebrew]: https://brew.sh/ + +```bash +brew install scrcpy +``` + +由於執行期間需要可以藉由 `PATH` 存取 `adb` 。如果還沒有安裝 `adb` 可以使用下列方式安裝: + +```bash +brew cask install android-platform-tools +``` + +你也可以自己[編譯 _Scrcpy_][BUILD]。 + + +## 執行 + +將電腦和你的 Android 裝置連線,然後執行: + +```bash +scrcpy +``` + +_Scrcpy_ 可以接受命令列參數。輸入下列指令就可以瀏覽可以使用的命令列參數: + +```bash +scrcpy --help +``` + + +## 功能 + +> 以下說明中,有關快捷鍵的說明可能會出現 MOD 按鈕。相關說明請參見[快捷鍵]內的說明。 + +[快捷鍵]: #快捷鍵 + +### 畫面擷取 + +#### 縮小尺寸 + +使用比較低的解析度來投放 Android 裝置在某些情況可以提升效能。 + +限制寬和高的最大值(例如: 1024): + +```bash +scrcpy --max-size 1024 +scrcpy -m 1024 # 縮短版本 +``` + +比較小的參數會根據螢幕比例重新計算。 +根據上面的範例,1920x1080 會被縮小成 1024x576。 + + +#### 更改 bit-rate + +預設的 bit-rate 是 8 Mbps。如果要更改 bit-rate (例如: 2 Mbps): + +```bash +scrcpy --bit-rate 2M +scrcpy -b 2M # 縮短版本 +``` + +#### 限制 FPS + +限制畫面最高的 FPS: + +```bash +scrcpy --max-fps 15 +``` + +僅在 Android 10 後正式支援,不過也有可能可以在 Android 10 以前的版本使用。 + +#### 裁切 + +裝置的螢幕可以裁切。如此一來,鏡像出來的螢幕就只會是原本的一部份。 + +假如只要鏡像 Oculus Go 的其中一隻眼睛: + +```bash +scrcpy --crop 1224:1440:0:0 # 位於 (0,0),大小1224x1440 +``` + +如果 `--max-size` 也有指定的話,裁切後才會縮放。 + + +#### 鎖定影像方向 + + +如果要鎖定鏡像影像方向: + +```bash +scrcpy --lock-video-orientation 0 # 原本的方向 +scrcpy --lock-video-orientation 1 # 逆轉 90° +scrcpy --lock-video-orientation 2 # 180° +scrcpy --lock-video-orientation 3 # 順轉 90° +``` + +這會影響錄影結果的影像方向。 + + +### 錄影 + +鏡像投放螢幕的同時也可以錄影: + +```bash +scrcpy --record file.mp4 +scrcpy -r file.mkv +``` + +如果只要錄影,不要投放螢幕鏡像的話: + +```bash +scrcpy --no-display --record file.mp4 +scrcpy -Nr file.mkv +# 用 Ctrl+C 停止錄影 +``` + +就算有些幀為了效能而被跳過,它們還是一樣會被錄製。 + +裝置上的每一幀都有時間戳記,所以 [封包延遲 (Packet Delay Variation, PDV)][packet delay variation] 並不會影響錄影的檔案。 + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +### 連線 + +#### 無線 + +_Scrcpy_ 利用 `adb` 和裝置通訊,而 `adb` 可以[透過 TCP/IP 連結][connect]: + +1. 讓電腦和裝置連到同一個 Wi-Fi。 +2. 獲取手機的 IP 位址(設定 → 關於手機 → 狀態). +3. 啟用裝置上的 `adb over TCP/IP`: `adb tcpip 5555`. +4. 拔掉裝置上的線。 +5. 透過 TCP/IP 連接裝置: `adb connect DEVICE_IP:5555` _(把 `DEVICE_IP` 換成裝置的IP位址)_. +6. 和平常一樣執行 `scrcpy`。 + +如果效能太差,可以降低 bit-rate 和解析度: + +```bash +scrcpy --bit-rate 2M --max-size 800 +scrcpy -b2M -m800 # 縮短版本 +``` + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +#### 多裝置 + +如果 `adb devices` 內有多個裝置,則必須附上 _serial_: + +```bash +scrcpy --serial 0123456789abcdef +scrcpy -s 0123456789abcdef # 縮短版本 +``` + +如果裝置是透過 TCP/IP 連線: + +```bash +scrcpy --serial 192.168.0.1:5555 +scrcpy -s 192.168.0.1:5555 # 縮短版本 +``` + +你可以啟用復數個對應不同裝置的 _scrcpy_。 + +#### 裝置連結後自動啟動 + +你可以使用 [AutoAdb]: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb + +#### SSH tunnel + +本地的 `adb` 可以連接到遠端的 `adb` 伺服器(假設兩者使用相同版本的 _adb_ 通訊協定),以連接到遠端裝置: + +```bash +adb kill-server # 停止在 Port 5037 的本地 adb 伺服 +ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer +# 保持開啟 +``` + +從另外一個終端機: + +```bash +scrcpy +``` + +如果要避免啟用 remote port forwarding,你可以強制它使用 forward connection (注意 `-L` 和 `-R` 的差別): + +```bash +adb kill-server # 停止在 Port 5037 的本地 adb 伺服 +ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer +# 保持開啟 +``` + +從另外一個終端機: + +```bash +scrcpy --force-adb-forward +``` + + +和無線連接一樣,有時候降低品質會比較好: + +``` +scrcpy -b2M -m800 --max-fps 15 +``` + +### 視窗調整 + +#### 標題 + +預設標題是裝置的型號,不過可以透過以下方式修改: + +```bash +scrcpy --window-title 'My device' +``` + +#### 位置 & 大小 + +初始的視窗位置和大小也可以指定: + +```bash +scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 +``` + +#### 無邊框 + +如果要停用視窗裝飾: + +```bash +scrcpy --window-borderless +``` + +#### 保持最上層 + +如果要保持 `scrcpy` 的視窗在最上層: + +```bash +scrcpy --always-on-top +``` + +#### 全螢幕 + +這個軟體可以直接在全螢幕模式下起動: + +```bash +scrcpy --fullscreen +scrcpy -f # 縮短版本 +``` + +全螢幕可以使用 MOD+f 開關。 + +#### 旋轉 + +視窗可以旋轉: + +```bash +scrcpy --rotation 1 +``` + +可用的數值: + - `0`: 不旋轉 + - `1`: 90 度**逆**轉 + - `2`: 180 度 + - `3`: 90 度**順**轉 + +旋轉方向也可以使用 MOD+ _(左方向鍵)_ 和 MOD+ _(右方向鍵)_ 調整。 + +_scrcpy_ 有 3 種不同的旋轉: + - MOD+r 要求裝置在垂直、水平之間旋轉 (目前運行中的 App 有可能會因為不支援而拒絕)。 + - `--lock-video-orientation` 修改鏡像的方向 (裝置傳給電腦的影像)。這會影響錄影結果的影像方向。 + - `--rotation` (或是 MOD+ / MOD+) 只旋轉視窗的內容。這只會影響鏡像結果,不會影響錄影結果。 + + +### 其他鏡像選項 + +#### 唯讀 + +停用所有控制,包含鍵盤輸入、滑鼠事件、拖放檔案: + +```bash +scrcpy --no-control +scrcpy -n +``` + +#### 顯示螢幕 + +如果裝置有複數個螢幕,可以指定要鏡像哪個螢幕: + +```bash +scrcpy --display 1 +``` + +可以透過下列指令獲取螢幕 ID: + +``` +adb shell dumpsys display # 找輸出結果中的 "mDisplayId=" +``` + +第二螢幕只有在 Android 10+ 時可以控制。如果不是 Android 10+,螢幕就會在唯讀狀態下投放。 + + +#### 保持清醒 + +如果要避免裝置在連接狀態下進入睡眠: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +_scrcpy_ 關閉後就會回復成原本的設定。 + + +#### 關閉螢幕 + +鏡像開始時,可以要求裝置關閉螢幕: + +```bash +scrcpy --turn-screen-off +scrcpy -S +``` + +或是在任何時候輸入 MOD+o。 + +如果要開啟螢幕,輸入 MOD+Shift+o。 + +在 Android 上,`POWER` 按鈕總是開啟螢幕。 + +為了方便,如果 `POWER` 是透過 scrcpy 轉送 (右鍵 或 MOD+p)的話,螢幕將會在短暫的延遲後關閉。 + +實際在手機上的 `POWER` 還是會開啟螢幕。 + +防止裝置進入睡眠狀態: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw +``` + + +#### 顯示過期的幀 + +為了降低延遲, _scrcpy_ 預設只會顯示最後解碼的幀,並且拋棄所有在這之前的幀。 + +如果要強制顯示所有的幀 (有可能會拉高延遲),輸入: + +```bash +scrcpy --render-expired-frames +``` + +#### 顯示觸控點 + +對於要報告的人來說,顯示裝置上的實際觸控點有時候是有幫助的。 + +Android 在_開發者選項_中有提供這個功能。 + +_Scrcpy_ 可以在啟動時啟用這個功能,並且在停止後恢復成原本的設定: + +```bash +scrcpy --show-touches +scrcpy -t +``` + +這個選項只會顯示**實際觸碰在裝置上的觸碰點**。 + + +### 輸入控制 + + +#### 旋轉裝置螢幕 + +輸入 MOD+r 以在垂直、水平之間切換。 + +如果使用中的程式不支援,則不會切換。 + + +#### 複製/貼上 + +如果 Android 剪貼簿上的內容有任何更動,電腦的剪貼簿也會一起更動。 + +任何與 Ctrl 相關的快捷鍵事件都會轉送到裝置上。特別來說: + - Ctrl+c 通常是複製 + - Ctrl+x 通常是剪下 + - Ctrl+v 通常是貼上 (在電腦的剪貼簿與裝置上的剪貼簿同步之後) + +這些跟你通常預期的行為一樣。 + +但是,實際上的行為是根據目前運行中的應用程式而定。 + +舉例來說, _Termux_ 在收到 Ctrl+c 後,會傳送 SIGINT;而 _K-9 Mail_ 則是建立新訊息。 + +如果在這情況下,要剪下、複製或貼上 (只有在Android 7+時才支援): + - MOD+c 注入 `複製` + - MOD+x 注入 `剪下` + - MOD+v 注入 `貼上` (在電腦的剪貼簿與裝置上的剪貼簿同步之後) + +另外,MOD+Shift+v 則是以一連串的按鍵事件貼上電腦剪貼簿中的內容。當元件不允許文字貼上 (例如 _Termux_) 時,這就很有用。不過,這在非 ASCII 內容上就無法使用。 + +**警告:** 貼上電腦的剪貼簿內容 (無論是從 Ctrl+vMOD+v) 時,會複製剪貼簿中的內容至裝置的剪貼簿上。這會讓所有 Android 程式讀取剪貼簿的內容。請避免貼上任何敏感內容 (像是密碼)。 + + +#### 文字輸入偏好 + +輸入文字時,有兩種[事件][textevents]會被觸發: + - _鍵盤事件 (key events)_,代表有一個按鍵被按下或放開 + - _文字事件 (text events)_,代表有一個文字被輸入 + +預設上,文字是被以鍵盤事件 (key events) 輸入的,所以鍵盤和遊戲內所預期的一樣 (通常是指 WASD)。 + +但是這可能造成[一些問題][prefertext]。如果在這輸入這方面遇到了問題,你可以試試: + +```bash +scrcpy --prefer-text +``` + +(不過遊戲內鍵盤就會不可用) + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +#### 重複輸入 + +通常來說,長時間按住一個按鍵會重複觸發按鍵事件。這會在一些遊戲中造成效能問題,而且這個重複的按鍵事件是沒有意義的。 + +如果不要轉送這些重複的按鍵事件: + +```bash +scrcpy --no-key-repeat +``` + + +### 檔案 + +#### 安裝 APK + +如果要安裝 APK ,拖放一個 APK 檔案 (以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。 + +視窗上不會有任何反饋;結果會顯示在命令列中。 + + +#### 推送檔案至裝置 + +如果要推送檔案到裝置上的 `/sdcard/` ,拖放一個非 APK 檔案 (**不**以 `.apk` 為副檔名) 到 _scrcpy_ 的視窗上。 + +視窗上不會有任何反饋;結果會顯示在命令列中。 + +推送檔案的目標路徑可以在啟動時指定: + +```bash +scrcpy --push-target /sdcard/foo/bar/ +``` + + +### 音訊轉送 + +_scrcpy_ **不**轉送音訊。請使用 [sndcpy]。另外,參見 [issue #14]。 + +[sndcpy]: https://github.com/rom1v/sndcpy +[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 + + +## 快捷鍵 + +在以下的清單中,MOD 是快捷鍵的特殊按鍵。通常來說,這個按鍵是 (左) Alt 或是 (左) Super。 + +這個是可以使用 `--shortcut-mod` 更改的。可以用的選項有: +- `lctrl`: 左邊的 Ctrl +- `rctrl`: 右邊的 Ctrl +- `lalt`: 左邊的 Alt +- `ralt`: 右邊的 Alt +- `lsuper`: 左邊的 Super +- `rsuper`: 右邊的 Super + +```bash +# 以 右邊的 Ctrl 為快捷鍵特殊按鍵 +scrcpy --shortcut-mod=rctrl + +# 以 左邊的 Ctrl 和左邊的 Alt 或是 左邊的 Super 為快捷鍵特殊按鍵 +scrcpy --shortcut-mod=lctrl+lalt,lsuper +``` + +_[Super] 通常是 WindowsCmd 鍵。_ + +[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) + + | Action | Shortcut + | ------------------------------------------------- |:----------------------------- + | 切換至全螢幕 | MOD+f + | 左旋顯示螢幕 | MOD+ _(左)_ + | 右旋顯示螢幕 | MOD+ _(右)_ + | 縮放視窗成 1:1 (pixel-perfect) | MOD+g + | 縮放視窗到沒有黑邊框為止 | MOD+w \| _雙擊¹_ + | 按下 `首頁` 鍵 | MOD+h \| _中鍵_ + | 按下 `返回` 鍵 | MOD+b \| _右鍵²_ + | 按下 `切換 APP` 鍵 | 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 + +_¹在黑邊框上雙擊以移除它們。_ +_²右鍵會返回。如果螢幕是關閉狀態,則會打開螢幕。_ +_³只支援 Android 7+。_ + +所有 Ctrl+_按鍵_ 快捷鍵都會傳送到裝置上,所以它們是由目前運作的應用程式處理的。 + + +## 自訂路徑 + +如果要使用特定的 _adb_ ,將它設定到環境變數中的 `ADB`: + + ADB=/path/to/adb scrcpy + +如果要覆寫 `scrcpy-server` 檔案的路徑,則將路徑設定到環境變數中的 `SCRCPY_SERVER_PATH`。 + +[相關連結][useful] + +[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 + + +## 為何叫 _scrcpy_ ? + +有一個同事要我找一個跟 [gnirehtet] 一樣難念的名字。 + +[`strcpy`] 複製一個字串 (**str**ing);`scrcpy` 複製一個螢幕 (**scr**een)。 + +[gnirehtet]: https://github.com/Genymobile/gnirehtet +[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html + + +## 如何編譯? + +請看[這份文件 (英文)][BUILD]。 + +[BUILD]: BUILD.md + + +## 常見問題 + +請看[這份文件 (英文)][FAQ]。 + +[FAQ]: FAQ.md + + +## 開發者文件 + +請看[這個頁面 (英文)][developers page]. + +[developers page]: DEVELOP.md + + +## Licence + + Copyright (C) 2018 Genymobile + Copyright (C) 2018-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. + +## 相關文章 + +- [Scrcpy 簡介 (英文)][article-intro] +- [Scrcpy 可以無線連線了 (英文)][article-tcpip] + +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/app/meson.build b/app/meson.build index 3bcb9bc1..0663c641 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,16 +1,17 @@ src = [ 'src/main.c', + 'src/adb.c', 'src/cli.c', - 'src/command.c', + 'src/compat.c', 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', - 'src/device.c', 'src/device_msg.c', 'src/event_converter.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/input_manager.c', + 'src/opengl.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', @@ -19,10 +20,30 @@ src = [ 'src/stream.c', 'src/tiny_xpm.c', 'src/video_buffer.c', + 'src/util/log.c', 'src/util/net.c', - 'src/util/str_util.c' + 'src/util/process.c', + 'src/util/str_util.c', + 'src/util/thread.c', ] +if host_machine.system() == 'windows' + src += [ 'src/sys/win/process.c' ] +else + src += [ 'src/sys/unix/process.c' ] +endif + +v4l2_support = host_machine.system() == 'linux' +if v4l2_support + src += [ 'src/v4l2_sink.c' ] +endif + +check_functions = [ + 'strdup' +] + +cc = meson.get_compiler('c') + if not get_option('crossbuild_windows') # native build @@ -33,11 +54,13 @@ if not get_option('crossbuild_windows') dependency('sdl2'), ] + if v4l2_support + dependencies += dependency('libavdevice') + endif + else # cross-compile mingw32 build (from Linux to Windows) - cc = meson.get_compiler('c') - prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' @@ -72,21 +95,18 @@ else endif -cc = meson.get_compiler('c') - if host_machine.system() == 'windows' - src += [ 'src/sys/win/command.c' ] - src += [ 'src/sys/win/net.c' ] dependencies += cc.find_library('ws2_32') -else - src += [ 'src/sys/unix/command.c' ] - src += [ 'src/sys/unix/net.c' ] endif conf = configuration_data() -# expose the build type -conf.set('NDEBUG', get_option('buildtype') != 'debug') +foreach f : check_functions + if cc.has_function(f) + define = 'HAVE_' + f.underscorify().to_upper() + conf.set(define, true) + endif +endforeach # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) @@ -98,43 +118,33 @@ conf.set_quoted('PREFIX', get_option('prefix')) # directory as the executable) conf.set('PORTABLE', get_option('portable')) -# the default client TCP port for the "adb reverse" tunnel +# the default client TCP port range for the "adb reverse" tunnel # overridden by option --port -conf.set('DEFAULT_LOCAL_PORT', '27183') - -# the default max video size for both dimensions, in pixels -# overridden by option --max-size -conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited +conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') +conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # the default video bitrate, in bits/second # overridden by option --bit-rate conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps -# enable High DPI support -conf.set('HIDPI_SUPPORT', get_option('hidpi_support')) - -# disable console on Windows -conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole')) - # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) +# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) +conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') + +# enable V4L2 support (linux only) +conf.set('HAVE_V4L2', v4l2_support) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') -if get_option('windows_noconsole') - link_args = [ '-Wl,--subsystem,windows' ] -else - link_args = [] -endif - executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, - c_args: [], - link_args: link_args) + c_args: []) install_man('scrcpy.1') @@ -155,12 +165,12 @@ if get_option('buildtype') == 'debug' 'src/cli.c', 'src/util/str_util.c', ]], - ['test_control_event_serialize', [ + ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', 'src/util/str_util.c', ]], - ['test_device_event_deserialize', [ + ['test_device_msg_deserialize', [ 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], @@ -177,7 +187,7 @@ if get_option('buildtype') == 'debug' exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies, - c_args: ['-DSDL_MAIN_HANDLED']) + c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) test(t[0], exe) endforeach endif diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 9560df1c..62dc9677 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -25,6 +25,16 @@ Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are Default is 8000000. +.TP +.BI "\-\-codec\-options " key[:type]=value[,...] +Set a list of comma-separated key:type=value options for the device encoder. + +The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. + +The list of possible codec options is available in the Android documentation +.UR https://d.android.com/reference/android/media/MediaFormat +.UE . + .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. @@ -33,6 +43,31 @@ The values are expressed in the device natural orientation (typically, portrait .B \-\-max\-size value is computed on the cropped size. +.TP +.BI "\-\-disable-screensaver" +Disable screensaver while scrcpy is running. + +.TP +.BI "\-\-display " id +Specify the display id to mirror. + +The list of possible display ids can be listed by "adb shell dumpsys display" +(search "mDisplayId=" in the output). + +Default is 0. + +.TP +.BI "\-\-encoder " name +Use a specific MediaCodec encoder (must be a H.264 encoder). + +.TP +.B \-\-force\-adb\-forward +Do not attempt to use "adb reverse" to connect to the device. + +.TP +.B \-\-forward\-all\-clicks +By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead. + .TP .B \-f, \-\-fullscreen Start in fullscreen. @@ -41,9 +76,23 @@ Start in fullscreen. .B \-h, \-\-help Print this help. +.TP +.B \-\-legacy\-paste +Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). + +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. + +Default is "unlocked". + +Passing the option without argument is equivalent to passing "initial". + .TP .BI "\-\-max\-fps " value -Limit the framerate of screen capture (only supported on devices with Android >= 10). +Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). .TP .BI "\-m, \-\-max\-size " value @@ -60,10 +109,18 @@ Disable device control (mirror the device in read\-only). Do not display device (only when screen recording is enabled). .TP -.BI "\-p, \-\-port " port -Set the TCP port the client listens on. +.B \-\-no\-key\-repeat +Do not forward repeated key events when a key is held down. -Default is 27183. +.TP +.B \-\-no\-mipmaps +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. + +.TP +.BI "\-p, \-\-port " port[:port] +Set the TCP port (range) used by the client to listen. + +Default is 27183:27199. .TP .B \-\-prefer\-text @@ -76,7 +133,7 @@ but breaks the expected behavior of alpha keys in games (typically WASD). .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". -Default is "/sdcard/". +Default is "/sdcard/Download/". .TP .BI "\-r, \-\-record " file @@ -92,27 +149,62 @@ option if set, or by the file extension (.mp4 or .mkv). Force recording format (either mp4 or mkv). .TP -.B \-\-render\-expired\-frames -By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency. +.BI "\-\-render\-driver " name +Request SDL to use the given render driver (this is just a hint). + +Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". + +.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER +.UE + +.TP +.BI "\-\-rotation " value +Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. +.TP +.BI "\-\-shortcut\-mod " key[+...]][,...] +Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". + +A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. + +For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". + +Default is "lalt,lsuper" (left-Alt or left-Super). + .TP .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. .TP .B \-t, \-\-show\-touches -Enable "show touches" on start, disable on quit. +Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). +.TP +.BI "\-\-v4l2-sink " /dev/videoN +Output to v4l2loopback device. + +It requires to lock the video orientation (see --lock-video-orientation). + +.TP +.BI "\-V, \-\-verbosity " value +Set the log level ("verbose", "debug", "info", "warn" or "error"). + +Default is "info" for release builds, "debug" for debug builds. + .TP .B \-v, \-\-version Print the version of scrcpy. +.TP +.B \-w, \-\-stay-awake +Keep the device on while scrcpy is running, when the device is plugged in. + .TP .B \-\-window\-borderless Disable window decorations (display borderless window). @@ -125,107 +217,130 @@ Set a custom window title. .BI "\-\-window\-x " value Set the initial window horizontal position. -Default is -1 (automatic).\n +Default is "auto". .TP .BI "\-\-window\-y " value Set the initial window vertical position. -Default is -1 (automatic).\n +Default is "auto". .TP .BI "\-\-window\-width " value Set the initial window width. -Default is 0 (automatic).\n +Default is 0 (automatic). .TP .BI "\-\-window\-height " value Set the initial window height. -Default is 0 (automatic).\n +Default is 0 (automatic). .SH SHORTCUTS -.TP -.B Ctrl+f -switch fullscreen mode +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). .TP -.B Ctrl+g -resize window to 1:1 (pixel\-perfect) +.B MOD+f +Switch fullscreen mode .TP -.B Ctrl+x, Double\-click on black borders -resize window to remove black borders +.B MOD+Left +Rotate display left .TP -.B Ctrl+h, Home, Middle\-click +.B MOD+Right +Rotate display right + +.TP +.B MOD+g +Resize window to 1:1 (pixel\-perfect) + +.TP +.B MOD+w, Double\-click on black borders +Resize window to remove black borders + +.TP +.B MOD+h, Home, Middle\-click Click on HOME .TP -.B Ctrl+b, Ctrl+Backspace, Right\-click (when screen is on) +.B MOD+b, MOD+Backspace, Right\-click (when screen is on) Click on BACK .TP -.B Ctrl+s +.B MOD+s Click on APP_SWITCH .TP -.B Ctrl+m +.B MOD+m Click on MENU .TP -.B Ctrl+Up +.B MOD+Up Click on VOLUME_UP .TP -.B Ctrl+Down +.B MOD+Down Click on VOLUME_DOWN .TP -.B Ctrl+p +.B MOD+p Click on POWER (turn screen on/off) .TP .B Right\-click (when screen is off) -turn screen on +Turn screen on .TP -.B Ctrl+o -turn device screen off (keep mirroring) +.B MOD+o +Turn device screen off (keep mirroring) .TP -.B Ctrl+r -rotate device screen +.B MOD+Shift+o +Turn device screen on .TP -.B Ctrl+n -expand notification panel +.B MOD+r +Rotate device screen .TP -.B Ctrl+Shift+n -collapse notification panel +.B MOD+n +Expand notification panel .TP -.B Ctrl+c -copy device clipboard to computer +.B MOD+Shift+n +Collapse notification panel .TP -.B Ctrl+v -paste computer clipboard to device +.B Mod+c +Copy to clipboard (inject COPY keycode, Android >= 7 only) .TP -.B Ctrl+Shift+v -copy computer clipboard to device +.B Mod+x +Cut to clipboard (inject CUT keycode, Android >= 7 only) .TP -.B Ctrl+i -enable/disable FPS counter (print frames/second in logs) +.B MOD+v +Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) + +.TP +.B MOD+Shift+v +Inject computer clipboard text as a sequence of key events + +.TP +.B MOD+i +Enable/disable FPS counter (print frames/second in logs) + +.TP +.B Ctrl+click-and-move +Pinch-to-zoom from the center of the screen .TP .B Drag & drop APK file -install APK from computer +Install APK from computer .SH Environment variables diff --git a/app/src/command.c b/app/src/adb.c similarity index 73% rename from app/src/command.c rename to app/src/adb.c index 63afccb4..5bb9df30 100644 --- a/app/src/command.c +++ b/app/src/adb.c @@ -1,15 +1,10 @@ -#include "command.h" +#include "adb.h" #include #include #include #include -#include -#include -#include -#include "config.h" -#include "common.h" #include "util/log.h" #include "util/str_util.h" @@ -58,47 +53,89 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) { return idx; } +static void +show_adb_installation_msg() { +#ifndef __WINDOWS__ + static const struct { + const char *binary; + const char *command; + } pkg_managers[] = { + {"apt", "apt install adb"}, + {"apt-get", "apt-get install adb"}, + {"brew", "brew cask install android-platform-tools"}, + {"dnf", "dnf install android-tools"}, + {"emerge", "emerge dev-util/android-tools"}, + {"pacman", "pacman -S android-tools"}, + }; + for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { + if (search_executable(pkg_managers[i].binary)) { + LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); + return; + } + } +#endif + + LOGI("You may download and install 'adb' from " + "https://developer.android.com/studio/releases/platform-tools"); +} + static void show_adb_err_msg(enum process_result err, const char *const argv[]) { - char buf[512]; +#define MAX_COMMAND_STRING_LEN 1024 + char *buf = malloc(MAX_COMMAND_STRING_LEN); + if (!buf) { + LOGE("Failed to execute (could not allocate error message)"); + return; + } + switch (err) { case PROCESS_ERROR_GENERIC: - argv_to_string(argv, buf, sizeof(buf)); + argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); LOGE("Failed to execute: %s", buf); break; case PROCESS_ERROR_MISSING_BINARY: - argv_to_string(argv, buf, sizeof(buf)); + 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: // do nothing break; } + + free(buf); } process_t adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - const char *cmd[len + 4]; int i; process_t process; - cmd[0] = get_adb_command(); + + const char **argv = malloc((len + 4) * sizeof(*argv)); + if (!argv) { + return PROCESS_NONE; + } + + argv[0] = get_adb_command(); if (serial) { - cmd[1] = "-s"; - cmd[2] = serial; + argv[1] = "-s"; + argv[2] = serial; i = 3; } else { i = 1; } - memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); - cmd[len + i] = NULL; - enum process_result r = cmd_execute(cmd, &process); + 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) { - show_adb_err_msg(r, cmd); - return PROCESS_NONE; + show_adb_err_msg(r, argv); + process = PROCESS_NONE; } + + free(argv); return process; } @@ -151,7 +188,7 @@ adb_push(const char *serial, const char *local, const char *remote) { } remote = strquote(remote); if (!remote) { - SDL_free((void *) local); + free((void *) local); return PROCESS_NONE; } #endif @@ -160,8 +197,8 @@ adb_push(const char *serial, const char *local, const char *remote) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - SDL_free((void *) remote); - SDL_free((void *) local); + free((void *) remote); + free((void *) local); #endif return proc; @@ -182,37 +219,8 @@ adb_install(const char *serial, const char *local) { process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); #ifdef __WINDOWS__ - SDL_free((void *) local); + free((void *) local); #endif return proc; } - -bool -process_check_success(process_t proc, const char *name) { - if (proc == PROCESS_NONE) { - LOGE("Could not execute \"%s\"", name); - return false; - } - exit_code_t exit_code; - if (!cmd_simple_wait(proc, &exit_code)) { - if (exit_code != NO_EXIT_CODE) { - LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); - } else { - LOGE("\"%s\" exited unexpectedly", name); - } - return false; - } - return true; -} - -bool -is_regular_file(const char *path) { - struct stat path_stat; - int r = stat(path, &path_stat); - if (r) { - perror("stat"); - return false; - } - return S_ISREG(path_stat.st_mode); -} diff --git a/app/src/adb.h b/app/src/adb.h new file mode 100644 index 00000000..e27f34fa --- /dev/null +++ b/app/src/adb.h @@ -0,0 +1,34 @@ +#ifndef SC_ADB_H +#define SC_ADB_H + +#include "common.h" + +#include +#include + +#include "util/process.h" + +process_t +adb_execute(const char *serial, const char *const adb_cmd[], size_t len); + +process_t +adb_forward(const char *serial, uint16_t local_port, + const char *device_socket_name); + +process_t +adb_forward_remove(const char *serial, uint16_t local_port); + +process_t +adb_reverse(const char *serial, const char *device_socket_name, + uint16_t local_port); + +process_t +adb_reverse_remove(const char *serial, const char *device_socket_name); + +process_t +adb_push(const char *serial, const char *local, const char *remote); + +process_t +adb_install(const char *serial, const char *local); + +#endif diff --git a/app/src/android/input.h b/app/src/android/input.h index b51731b4..30c4bcb9 100644 --- a/app/src/android/input.h +++ b/app/src/android/input.h @@ -21,7 +21,7 @@ #define _ANDROID_INPUT_H /** - * Meta key / modifer state. + * Meta key / modifier state. */ enum android_metastate { /** No meta keys are pressed. */ diff --git a/app/src/cli.c b/app/src/cli.c index d9e1013a..3eab8d27 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1,21 +1,20 @@ #include "cli.h" +#include #include #include +#include #include -#include "config.h" -#include "recorder.h" +#include "scrcpy.h" #include "util/log.h" #include "util/str_util.h" +#define STR_IMPL_(x) #x +#define STR(x) STR_IMPL_(x) + void scrcpy_print_usage(const char *arg0) { -#ifdef __APPLE__ -# define CTRL_OR_CMD "Cmd" -#else -# define CTRL_OR_CMD "Ctrl" -#endif fprintf(stderr, "Usage: %s [options]\n" "\n" @@ -27,7 +26,16 @@ scrcpy_print_usage(const char *arg0) { " -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 %d.\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" @@ -35,21 +43,61 @@ scrcpy_print_usage(const char *arg0) { " (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 (only supported on\n" - " devices with Android >= 10).\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 %d%s.\n" + " Default is 0 (unlimited).\n" "\n" " -n, --no-control\n" " Disable device control (mirror the device in read-only).\n" @@ -58,9 +106,18 @@ scrcpy_print_usage(const char *arg0) { " Do not display device (only when screen recording is\n" " enabled).\n" "\n" - " -p, --port port\n" - " Set the TCP port the client listens on.\n" - " Default is %d.\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" @@ -72,7 +129,7 @@ scrcpy_print_usage(const char *arg0) { " --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/\".\n" + " Default is \"/sdcard/Download/\".\n" "\n" " -r, --record file.mp4\n" " Record screen to file.\n" @@ -82,26 +139,65 @@ scrcpy_print_usage(const char *arg0) { " --record-format format\n" " Force recording format (either mp4 or mkv).\n" "\n" - " --render-expired-frames\n" - " By default, to minimize latency, scrcpy always renders the\n" - " last available decoded frame, and drops any previous ones.\n" - " This flag forces to render all frames, at a cost of a\n" - " possible increased latency.\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, disable on quit.\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" @@ -110,90 +206,106 @@ scrcpy_print_usage(const char *arg0) { "\n" " --window-x value\n" " Set the initial window horizontal position.\n" - " Default is -1 (automatic).\n" + " Default is \"auto\".\n" "\n" " --window-y value\n" " Set the initial window vertical position.\n" - " Default is -1 (automatic).\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 width.\n" + " Set the initial window height.\n" " Default is 0 (automatic).\n" "\n" "Shortcuts:\n" "\n" - " " CTRL_OR_CMD "+f\n" - " switch fullscreen mode\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" - " " CTRL_OR_CMD "+g\n" - " resize window to 1:1 (pixel-perfect)\n" + " MOD+f\n" + " Switch fullscreen mode\n" "\n" - " " CTRL_OR_CMD "+x\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" + " Resize window to remove black borders\n" "\n" - " Ctrl+h\n" + " MOD+h\n" " Middle-click\n" - " click on HOME\n" + " Click on HOME\n" "\n" - " " CTRL_OR_CMD "+b\n" - " " CTRL_OR_CMD "+Backspace\n" + " MOD+b\n" + " MOD+Backspace\n" " Right-click (when screen is on)\n" - " click on BACK\n" + " Click on BACK\n" "\n" - " " CTRL_OR_CMD "+s\n" - " click on APP_SWITCH\n" + " MOD+s\n" + " Click on APP_SWITCH\n" "\n" - " Ctrl+m\n" - " click on MENU\n" + " MOD+m\n" + " Click on MENU\n" "\n" - " " CTRL_OR_CMD "+Up\n" - " click on VOLUME_UP\n" + " MOD+Up\n" + " Click on VOLUME_UP\n" "\n" - " " CTRL_OR_CMD "+Down\n" - " click on VOLUME_DOWN\n" + " MOD+Down\n" + " Click on VOLUME_DOWN\n" "\n" - " " CTRL_OR_CMD "+p\n" - " click on POWER (turn screen on/off)\n" + " MOD+p\n" + " Click on POWER (turn screen on/off)\n" "\n" " Right-click (when screen is off)\n" - " power on\n" + " Power on\n" "\n" - " " CTRL_OR_CMD "+o\n" - " turn device screen off (keep mirroring)\n" + " MOD+o\n" + " Turn device screen off (keep mirroring)\n" "\n" - " " CTRL_OR_CMD "+r\n" - " rotate device screen\n" + " MOD+Shift+o\n" + " Turn device screen on\n" "\n" - " " CTRL_OR_CMD "+n\n" - " expand notification panel\n" + " MOD+r\n" + " Rotate device screen\n" "\n" - " " CTRL_OR_CMD "+Shift+n\n" - " collapse notification panel\n" + " MOD+n\n" + " Expand notification panel\n" "\n" - " " CTRL_OR_CMD "+c\n" - " copy device clipboard to computer\n" + " MOD+Shift+n\n" + " Collapse notification panel\n" "\n" - " " CTRL_OR_CMD "+v\n" - " paste computer clipboard to device\n" + " MOD+c\n" + " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n" "\n" - " " CTRL_OR_CMD "+Shift+v\n" - " copy computer clipboard to device\n" + " MOD+x\n" + " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n" "\n" - " " CTRL_OR_CMD "+i\n" - " enable/disable FPS counter (print frames/second in logs)\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, - DEFAULT_BIT_RATE, - DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)", - DEFAULT_LOCAL_PORT); + " Install APK from computer\n" + "\n", arg0); } static bool @@ -221,6 +333,27 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, return true; } +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); + if (!count) { + LOGE("Could not parse %s: %s", name, s); + return 0; + } + + for (size_t i = 0; i < count; ++i) { + long value = out[i]; + if (value < min || value > max) { + LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", + name, value, min, max); + return 0; + } + } + + return count; +} + static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; @@ -260,9 +393,54 @@ parse_max_fps(const char *s, uint16_t *max_fps) { } static bool -parse_window_position(const char *s, int16_t *position) { +parse_lock_video_orientation(const char *s, + enum sc_lock_video_orientation *lock_mode) { + if (!s || !strcmp(s, "initial")) { + // Without argument, lock the initial orientation + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + return true; + } + + if (!strcmp(s, "unlocked")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED; + return true; + } + long value; - bool ok = parse_integer_arg(s, &value, false, -1, 0x7FFF, + bool ok = parse_integer_arg(s, &value, false, 0, 3, + "lock video orientation"); + if (!ok) { + return false; + } + + *lock_mode = (enum sc_lock_video_orientation) value; + return true; +} + +static bool +parse_rotation(const char *s, uint8_t *rotation) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); + if (!ok) { + return false; + } + + *rotation = (uint8_t) value; + return true; +} + +static bool +parse_window_position(const char *s, int16_t *position) { + // special value for "auto" + static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); + + if (!strcmp(s, "auto")) { + *position = SC_WINDOW_POSITION_UNDEFINED; + return true; + } + + long value; + bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF, "window position"); if (!ok) { return false; @@ -286,32 +464,188 @@ parse_window_dimension(const char *s, uint16_t *dimension) { } static bool -parse_port(const char *s, uint16_t *port) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "port"); - if (!ok) { +parse_port_range(const char *s, struct sc_port_range *port_range) { + long values[2]; + size_t count = parse_integers_arg(s, 2, values, 0, 0xFFFF, "port"); + if (!count) { return false; } - *port = (uint16_t) value; + uint16_t v0 = (uint16_t) values[0]; + if (count == 1) { + port_range->first = v0; + port_range->last = v0; + return true; + } + + assert(count == 2); + uint16_t v1 = (uint16_t) values[1]; + if (v0 < v1) { + port_range->first = v0; + port_range->last = v1; + } else { + port_range->first = v1; + port_range->last = v0; + } + return true; } static bool -parse_record_format(const char *optarg, enum recorder_format *format) { +parse_display_id(const char *s, uint32_t *display_id) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "display id"); + if (!ok) { + return false; + } + + *display_id = (uint32_t) value; + return true; +} + +static bool +parse_log_level(const char *s, enum sc_log_level *log_level) { + if (!strcmp(s, "verbose")) { + *log_level = SC_LOG_LEVEL_VERBOSE; + return true; + } + + if (!strcmp(s, "debug")) { + *log_level = SC_LOG_LEVEL_DEBUG; + return true; + } + + if (!strcmp(s, "info")) { + *log_level = SC_LOG_LEVEL_INFO; + return true; + } + + if (!strcmp(s, "warn")) { + *log_level = SC_LOG_LEVEL_WARN; + return true; + } + + if (!strcmp(s, "error")) { + *log_level = SC_LOG_LEVEL_ERROR; + return true; + } + + LOGE("Could not parse log level: %s", s); + return false; +} + +// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt") +// returns a bitwise-or of SC_MOD_* constants (or 0 on error) +static unsigned +parse_shortcut_mods_item(const char *item, size_t len) { + unsigned mod = 0; + + for (;;) { + char *plus = strchr(item, '+'); + // strchr() does not consider the "len" parameter, to it could find an + // occurrence too far in the string (there is no strnchr()) + bool has_plus = plus && plus < item + len; + + assert(!has_plus || plus > item); + size_t key_len = has_plus ? (size_t) (plus - item) : len; + +#define STREQ(literal, s, len) \ + ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) + + if (STREQ("lctrl", item, key_len)) { + mod |= SC_MOD_LCTRL; + } else if (STREQ("rctrl", item, key_len)) { + mod |= SC_MOD_RCTRL; + } else if (STREQ("lalt", item, key_len)) { + mod |= SC_MOD_LALT; + } else if (STREQ("ralt", item, key_len)) { + mod |= SC_MOD_RALT; + } else if (STREQ("lsuper", item, key_len)) { + mod |= SC_MOD_LSUPER; + } else if (STREQ("rsuper", item, key_len)) { + mod |= SC_MOD_RSUPER; + } else { + LOGE("Unknown modifier key: %.*s " + "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", + (int) key_len, item); + return 0; + } +#undef STREQ + + if (!has_plus) { + break; + } + + item = plus + 1; + assert(len >= key_len + 1); + len -= key_len + 1; + } + + return mod; +} + +static bool +parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + unsigned count = 0; + unsigned current = 0; + + // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper" + + for (;;) { + char *comma = strchr(s, ','); + if (comma && count == SC_MAX_SHORTCUT_MODS - 1) { + assert(count < SC_MAX_SHORTCUT_MODS); + LOGW("Too many shortcut modifiers alternatives"); + return false; + } + + assert(!comma || comma > s); + size_t limit = comma ? (size_t) (comma - s) : strlen(s); + + unsigned mod = parse_shortcut_mods_item(s, limit); + if (!mod) { + LOGE("Invalid modifier keys: %.*s", (int) limit, s); + return false; + } + + mods->data[current++] = mod; + ++count; + + if (!comma) { + break; + } + + s = comma + 1; + } + + mods->count = count; + + return true; +} + +#ifdef SC_TEST +// expose the function to unit-tests +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { + return parse_shortcut_mods(s, mods); +} +#endif + +static bool +parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { - *format = RECORDER_FORMAT_MP4; + *format = SC_RECORD_FORMAT_MP4; return true; } if (!strcmp(optarg, "mkv")) { - *format = RECORDER_FORMAT_MKV; + *format = SC_RECORD_FORMAT_MKV; return true; } LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); return false; } -static enum recorder_format +static enum sc_record_format guess_record_format(const char *filename) { size_t len = strlen(filename); if (len < 4) { @@ -319,59 +653,98 @@ guess_record_format(const char *filename) { } const char *ext = &filename[len - 4]; if (!strcmp(ext, ".mp4")) { - return RECORDER_FORMAT_MP4; + return SC_RECORD_FORMAT_MP4; } if (!strcmp(ext, ".mkv")) { - return RECORDER_FORMAT_MKV; + return SC_RECORD_FORMAT_MKV; } 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_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'}, - {"crop", required_argument, NULL, OPT_CROP}, - {"fullscreen", no_argument, NULL, 'f'}, - {"help", no_argument, NULL, 'h'}, - {"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'}, - {"port", required_argument, NULL, 'p'}, - {"push-target", required_argument, NULL, OPT_PUSH_TARGET}, - {"record", required_argument, NULL, 'r'}, - {"record-format", required_argument, NULL, OPT_RECORD_FORMAT}, - {"render-expired-frames", no_argument, NULL, - OPT_RENDER_EXPIRED_FRAMES}, - {"serial", required_argument, NULL, 's'}, - {"show-touches", no_argument, NULL, 't'}, - {"turn-screen-off", no_argument, NULL, 'S'}, - {"prefer-text", no_argument, NULL, OPT_PREFER_TEXT}, - {"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}, - {NULL, 0, NULL, 0 }, + {"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 }, }; struct scrcpy_options *opts = &args->opts; @@ -379,8 +752,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { 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:StTv", long_options, - NULL)) != -1) { + while ((c = getopt_long(argc, argv, "b:c:fF:hm:nNp:r:s:StTvV:w", + long_options, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &opts->bit_rate)) { @@ -393,6 +766,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_CROP: opts->crop = optarg; break; + case OPT_DISPLAY_ID: + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; case 'f': opts->fullscreen = true; break; @@ -417,6 +795,12 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case OPT_LOCK_VIDEO_ORIENTATION: + if (!parse_lock_video_orientation(optarg, + &opts->lock_video_orientation)) { + return false; + } + break; case 'n': opts->control = false; break; @@ -424,7 +808,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->display = false; break; case 'p': - if (!parse_port(optarg, &opts->port)) { + if (!parse_port_range(optarg, &opts->port_range)) { return false; } break; @@ -449,8 +833,17 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'v': args->version = true; break; + case 'V': + if (!parse_log_level(optarg, &opts->log_level)) { + return false; + } + break; + case 'w': + opts->stay_awake = true; + break; case OPT_RENDER_EXPIRED_FRAMES: - opts->render_expired_frames = true; + LOGW("Option --render-expired-frames has been removed. This " + "flag has been ignored."); break; case OPT_WINDOW_TITLE: opts->window_title = optarg; @@ -484,21 +877,76 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_PREFER_TEXT: opts->prefer_text = true; break; + case OPT_ROTATION: + if (!parse_rotation(optarg, &opts->rotation)) { + return false; + } + break; + case OPT_RENDER_DRIVER: + opts->render_driver = optarg; + break; + case OPT_NO_MIPMAPS: + opts->mipmaps = false; + break; + case OPT_NO_KEY_REPEAT: + opts->forward_key_repeat = false; + break; + case OPT_CODEC_OPTIONS: + opts->codec_options = optarg; + break; + case OPT_ENCODER_NAME: + opts->encoder_name = optarg; + break; + case OPT_FORCE_ADB_FORWARD: + opts->force_adb_forward = true; + break; + case OPT_DISABLE_SCREENSAVER: + opts->disable_screensaver = true; + break; + case OPT_SHORTCUT_MOD: + if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) { + return false; + } + break; + case OPT_FORWARD_ALL_CLICKS: + opts->forward_all_clicks = true; + break; + case OPT_LEGACY_PASTE: + opts->legacy_paste = true; + break; + case OPT_POWER_OFF_ON_CLOSE: + opts->power_off_on_close = true; + break; +#ifdef HAVE_V4L2 + case OPT_V4L2_SINK: + opts->v4l2_device = optarg; + break; +#endif default: // getopt prints the error message on stderr return false; } } +#ifdef HAVE_V4L2 + if (!opts->display && !opts->record_filename && !opts->v4l2_device) { + LOGE("-N/--no-display requires either screen recording (-r/--record)" + " or sink to v4l2loopback device (--v4l2-sink)"); + return false; + } + + if (opts->v4l2_device && opts->lock_video_orientation + == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + LOGI("Video orientation is locked for v4l2 sink. " + "See --lock-video-orientation."); + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + } +#else if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } - - if (!opts->display && opts->fullscreen) { - LOGE("-f/--fullscreen-window is incompatible with -N/--no-display"); - return false; - } +#endif int index = optind; if (index < argc) { @@ -514,7 +962,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { if (opts->record_filename && !opts->record_format) { opts->record_format = guess_record_format(opts->record_filename); if (!opts->record_format) { - LOGE("No format specified for \"%s\" (try with -F mkv)", + LOGE("No format specified for \"%s\" " + "(try with --record-format=mkv)", opts->record_filename); return false; } @@ -525,5 +974,10 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } + if (!opts->control && opts->stay_awake) { + LOGE("Could not request to stay awake if control is disabled"); + return false; + } + return true; } diff --git a/app/src/cli.h b/app/src/cli.h index 2e2bfe93..419f156f 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -1,9 +1,10 @@ #ifndef SCRCPY_CLI_H #define SCRCPY_CLI_H +#include "common.h" + #include -#include "config.h" #include "scrcpy.h" struct scrcpy_cli_args { @@ -18,4 +19,9 @@ scrcpy_print_usage(const char *arg0); bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); +#ifdef SC_TEST +bool +sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); +#endif + #endif diff --git a/app/src/common.h b/app/src/common.h index e5cbe953..accbc615 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -1,30 +1,14 @@ #ifndef COMMON_H #define COMMON_H -#include - #include "config.h" +#include "compat.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) -struct size { - uint16_t width; - uint16_t height; -}; - -struct point { - int32_t x; - int32_t y; -}; - -struct 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; -}; +#define container_of(ptr, type, member) \ + ((type *) (((char *) (ptr)) - offsetof(type, member))) #endif diff --git a/app/src/compat.c b/app/src/compat.c new file mode 100644 index 00000000..b3b98bf1 --- /dev/null +++ b/app/src/compat.c @@ -0,0 +1,14 @@ +#include "compat.h" + +#include "config.h" + +#ifndef HAVE_STRDUP +char *strdup(const char *s) { + size_t size = strlen(s) + 1; + char *dup = malloc(size); + if (dup) { + memcpy(dup, s, size); + } + return dup; +} +#endif diff --git a/app/src/compat.h b/app/src/compat.h index de667bbf..8e2d18f4 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,19 +1,16 @@ #ifndef COMPAT_H #define COMPAT_H +#define _POSIX_C_SOURCE 200809L +#define _XOPEN_SOURCE 700 +#define _GNU_SOURCE +#ifdef __APPLE__ +# define _DARWIN_C_SOURCE +#endif + #include #include -// In ffmpeg/doc/APIchanges: -// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h -// Add AVStream.codecpar, deprecate AVStream.codec. -#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \ - LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \ - || (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \ - LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0)) -# define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API -#endif - // In ffmpeg/doc/APIchanges: // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h // Deprecate use of av_register_input_format(), av_register_output_format(), @@ -25,13 +22,16 @@ # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif + // In ffmpeg/doc/APIchanges: -// 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h -// Add a new audio/video encoding and decoding API with decoupled input -// and output -- avcodec_send_packet(), avcodec_receive_frame(), -// avcodec_send_frame() and avcodec_receive_packet(). -#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100) -# define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API +// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h +// Deprecate AVFormatContext filename field which had limited length, use the +// new dynamically allocated url field instead. +// +// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h +// Add url field to AVFormatContext and add ff_format_set_url helper function. +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100) +# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL #endif #if SDL_VERSION_ATLEAST(2, 0, 5) @@ -48,4 +48,8 @@ # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR #endif +#ifndef HAVE_STRDUP +char *strdup(const char *s); +#endif + #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 45113139..1257010e 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -1,13 +1,60 @@ #include "control_msg.h" #include +#include +#include #include -#include "config.h" #include "util/buffer_util.h" #include "util/log.h" #include "util/str_util.h" +/** + * Map an enum value to a string based on an array, without crashing on an + * out-of-bounds index. + */ +#define ENUM_TO_LABEL(labels, value) \ + ((size_t) (value) < ARRAY_LEN(labels) ? labels[value] : "???") + +#define KEYEVENT_ACTION_LABEL(value) \ + ENUM_TO_LABEL(android_keyevent_action_labels, value) + +#define MOTIONEVENT_ACTION_LABEL(value) \ + ENUM_TO_LABEL(android_motionevent_action_labels, value) + +#define SCREEN_POWER_MODE_LABEL(value) \ + ENUM_TO_LABEL(screen_power_mode_labels, value) + +static const char *const android_keyevent_action_labels[] = { + "down", + "up", + "multi", +}; + +static const char *const android_motionevent_action_labels[] = { + "down", + "up", + "move", + "cancel", + "outside", + "ponter-down", + "pointer-up", + "hover-move", + "scroll", + "hover-enter" + "hover-exit", + "btn-press", + "btn-release", +}; + +static const char *const screen_power_mode_labels[] = { + "off", + "doze", + "normal", + "doze-suspend", + "suspend", +}; + static void write_position(uint8_t *buf, const struct position *position) { buffer_write32be(&buf[0], position->point.x); @@ -20,9 +67,9 @@ write_position(uint8_t *buf, const struct position *position) { static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { size_t len = utf8_truncation_index(utf8, max_len); - buffer_write16be(buf, (uint16_t) len); - memcpy(&buf[2], utf8, len); - return 2 + len; + buffer_write32be(buf, len); + memcpy(&buf[4], utf8, len); + return 4 + len; } static uint16_t @@ -42,11 +89,13 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: buf[1] = msg->inject_keycode.action; buffer_write32be(&buf[2], msg->inject_keycode.keycode); - buffer_write32be(&buf[6], msg->inject_keycode.metastate); - return 10; + buffer_write32be(&buf[6], msg->inject_keycode.repeat); + buffer_write32be(&buf[10], msg->inject_keycode.metastate); + return 14; case CONTROL_MSG_TYPE_INJECT_TEXT: { - size_t len = write_string(msg->inject_text.text, - CONTROL_MSG_TEXT_MAX_LENGTH, &buf[1]); + size_t len = + write_string(msg->inject_text.text, + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: @@ -65,18 +114,22 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { buffer_write32be(&buf[17], (uint32_t) msg->inject_scroll_event.vscroll); return 21; + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + buf[1] = msg->inject_keycode.action; + return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { - size_t len = write_string(msg->inject_text.text, + buf[1] = !!msg->set_clipboard.paste; + size_t len = write_string(msg->set_clipboard.text, CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[1]); - return 1 + len; + &buf[2]); + return 2 + len; } case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; - case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: - case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: + case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data @@ -87,14 +140,102 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { } } +void +control_msg_log(const struct control_msg *msg) { +#define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) + switch (msg->type) { + case CONTROL_MSG_TYPE_INJECT_KEYCODE: + LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx", + KEYEVENT_ACTION_LABEL(msg->inject_keycode.action), + (int) msg->inject_keycode.keycode, + msg->inject_keycode.repeat, + (long) msg->inject_keycode.metastate); + break; + case CONTROL_MSG_TYPE_INJECT_TEXT: + LOG_CMSG("text \"%s\"", msg->inject_text.text); + break; + case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { + int action = msg->inject_touch_event.action + & AMOTION_EVENT_ACTION_MASK; + uint64_t id = msg->inject_touch_event.pointer_id; + if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) { + // string pointer id + LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 + " pressure=%g buttons=%06lx", + id == POINTER_ID_MOUSE ? "mouse" : "vfinger", + MOTIONEVENT_ACTION_LABEL(action), + msg->inject_touch_event.position.point.x, + msg->inject_touch_event.position.point.y, + msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.buttons); + } else { + // numeric pointer id +#ifndef __WIN32 +# define PRIu64_ PRIu64 +#else +# define PRIu64_ "I64u" // Windows... +#endif + LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" + PRIi32 " pressure=%g buttons=%06lx", + id, + MOTIONEVENT_ACTION_LABEL(action), + msg->inject_touch_event.position.point.x, + msg->inject_touch_event.position.point.y, + msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.buttons); + } + break; + } + case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 + " vscroll=%" PRIi32, + msg->inject_scroll_event.position.point.x, + msg->inject_scroll_event.position.point.y, + msg->inject_scroll_event.hscroll, + msg->inject_scroll_event.vscroll); + break; + case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + LOG_CMSG("back-or-screen-on %s", + KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); + break; + case CONTROL_MSG_TYPE_SET_CLIPBOARD: + LOG_CMSG("clipboard %s \"%s\"", + msg->set_clipboard.paste ? "paste" : "copy", + msg->set_clipboard.text); + break; + case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + LOG_CMSG("power mode %s", + SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); + break; + case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + LOG_CMSG("expand notification panel"); + break; + case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + LOG_CMSG("expand settings panel"); + break; + case CONTROL_MSG_TYPE_COLLAPSE_PANELS: + LOG_CMSG("collapse panels"); + break; + case CONTROL_MSG_TYPE_GET_CLIPBOARD: + LOG_CMSG("get clipboard"); + break; + case CONTROL_MSG_TYPE_ROTATE_DEVICE: + LOG_CMSG("rotate device"); + break; + default: + LOG_CMSG("unknown type: %u", (unsigned) msg->type); + break; + } +} + void control_msg_destroy(struct control_msg *msg) { switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_TEXT: - SDL_free(msg->inject_text.text); + free(msg->inject_text.text); break; case CONTROL_MSG_TYPE_SET_CLIPBOARD: - SDL_free(msg->set_clipboard.text); + free(msg->set_clipboard.text); break; default: // do nothing diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 49a159a6..920a493a 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -1,21 +1,24 @@ #ifndef CONTROLMSG_H #define CONTROLMSG_H +#include "common.h" + #include #include #include -#include "config.h" #include "android/input.h" #include "android/keycodes.h" -#include "common.h" +#include "coords.h" -#define CONTROL_MSG_TEXT_MAX_LENGTH 300 -#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH 4093 -#define CONTROL_MSG_SERIALIZED_MAX_SIZE \ - (3 + CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH) +#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k -#define POINTER_ID_MOUSE UINT64_C(-1); +#define CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 +// type: 1 byte; paste flag: 1 byte; length: 4 bytes +#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) + +#define POINTER_ID_MOUSE UINT64_C(-1) +#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) enum control_msg_type { CONTROL_MSG_TYPE_INJECT_KEYCODE, @@ -24,7 +27,8 @@ enum control_msg_type { CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, - CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + CONTROL_MSG_TYPE_COLLAPSE_PANELS, CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, @@ -43,10 +47,11 @@ struct control_msg { struct { enum android_keyevent_action action; enum android_keycode keycode; + uint32_t repeat; enum android_metastate metastate; } inject_keycode; struct { - char *text; // owned, to be freed by SDL_free() + char *text; // owned, to be freed by free() } inject_text; struct { enum android_motionevent_action action; @@ -61,7 +66,12 @@ struct control_msg { int32_t vscroll; } inject_scroll_event; struct { - char *text; // owned, to be freed by SDL_free() + enum android_keyevent_action action; // action for the BACK key + // screen may only be turned on on ACTION_DOWN + } back_or_screen_on; + struct { + char *text; // owned, to be freed by free() + bool paste; } set_clipboard; struct { enum screen_power_mode mode; @@ -69,11 +79,14 @@ struct control_msg { }; }; -// buf size must be at least CONTROL_MSG_SERIALIZED_MAX_SIZE +// buf size must be at least CONTROL_MSG_MAX_SIZE // return the number of bytes written size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf); +void +control_msg_log(const struct control_msg *msg); + void control_msg_destroy(struct control_msg *msg); diff --git a/app/src/controller.c b/app/src/controller.c index d59a7411..3a428aa8 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -2,26 +2,27 @@ #include -#include "config.h" -#include "util/lock.h" #include "util/log.h" bool controller_init(struct controller *controller, socket_t control_socket) { cbuf_init(&controller->queue); - if (!receiver_init(&controller->receiver, control_socket)) { + bool ok = receiver_init(&controller->receiver, control_socket); + if (!ok) { return false; } - if (!(controller->mutex = SDL_CreateMutex())) { + ok = sc_mutex_init(&controller->mutex); + if (!ok) { receiver_destroy(&controller->receiver); return false; } - if (!(controller->msg_cond = SDL_CreateCond())) { + ok = sc_cond_init(&controller->msg_cond); + if (!ok) { receiver_destroy(&controller->receiver); - SDL_DestroyMutex(controller->mutex); + sc_mutex_destroy(&controller->mutex); return false; } @@ -33,8 +34,8 @@ controller_init(struct controller *controller, socket_t control_socket) { void controller_destroy(struct controller *controller) { - SDL_DestroyCond(controller->msg_cond); - SDL_DestroyMutex(controller->mutex); + sc_cond_destroy(&controller->msg_cond); + sc_mutex_destroy(&controller->mutex); struct control_msg msg; while (cbuf_take(&controller->queue, &msg)) { @@ -47,26 +48,30 @@ controller_destroy(struct controller *controller) { bool controller_push_msg(struct controller *controller, const struct control_msg *msg) { - mutex_lock(controller->mutex); + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + control_msg_log(msg); + } + + sc_mutex_lock(&controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); bool res = cbuf_push(&controller->queue, *msg); if (was_empty) { - cond_signal(controller->msg_cond); + sc_cond_signal(&controller->msg_cond); } - mutex_unlock(controller->mutex); + sc_mutex_unlock(&controller->mutex); return res; } static bool process_msg(struct controller *controller, const struct control_msg *msg) { - unsigned char serialized_msg[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int length = control_msg_serialize(msg, serialized_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); - return w == length; + return (size_t) w == length; } static int @@ -74,20 +79,20 @@ run_controller(void *data) { struct controller *controller = data; for (;;) { - mutex_lock(controller->mutex); + sc_mutex_lock(&controller->mutex); while (!controller->stopped && cbuf_is_empty(&controller->queue)) { - cond_wait(controller->msg_cond, controller->mutex); + sc_cond_wait(&controller->msg_cond, &controller->mutex); } if (controller->stopped) { // stop immediately, do not process further msgs - mutex_unlock(controller->mutex); + sc_mutex_unlock(&controller->mutex); break; } struct control_msg msg; bool non_empty = cbuf_take(&controller->queue, &msg); assert(non_empty); (void) non_empty; - mutex_unlock(controller->mutex); + sc_mutex_unlock(&controller->mutex); bool ok = process_msg(controller, &msg); control_msg_destroy(&msg); @@ -103,16 +108,16 @@ bool controller_start(struct controller *controller) { LOGD("Starting controller thread"); - controller->thread = SDL_CreateThread(run_controller, "controller", - controller); - if (!controller->thread) { + bool ok = sc_thread_create(&controller->thread, run_controller, + "controller", controller); + if (!ok) { LOGC("Could not start controller thread"); return false; } if (!receiver_start(&controller->receiver)) { controller_stop(controller); - SDL_WaitThread(controller->thread, NULL); + sc_thread_join(&controller->thread, NULL); return false; } @@ -121,14 +126,14 @@ controller_start(struct controller *controller) { void controller_stop(struct controller *controller) { - mutex_lock(controller->mutex); + sc_mutex_lock(&controller->mutex); controller->stopped = true; - cond_signal(controller->msg_cond); - mutex_unlock(controller->mutex); + sc_cond_signal(&controller->msg_cond); + sc_mutex_unlock(&controller->mutex); } void controller_join(struct controller *controller) { - SDL_WaitThread(controller->thread, NULL); + sc_thread_join(&controller->thread, NULL); receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index 8011ef6a..c53d0a61 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,23 +1,23 @@ #ifndef CONTROLLER_H #define CONTROLLER_H -#include -#include -#include +#include "common.h" + +#include -#include "config.h" #include "control_msg.h" #include "receiver.h" #include "util/cbuf.h" #include "util/net.h" +#include "util/thread.h" struct control_msg_queue CBUF(struct control_msg, 64); struct controller { socket_t control_socket; - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *msg_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond msg_cond; bool stopped; struct control_msg_queue queue; struct receiver receiver; diff --git a/app/src/coords.h b/app/src/coords.h new file mode 100644 index 00000000..7be6836d --- /dev/null +++ b/app/src/coords.h @@ -0,0 +1,24 @@ +#ifndef SC_COORDS +#define SC_COORDS + +#include + +struct size { + uint16_t width; + uint16_t height; +}; + +struct point { + int32_t x; + int32_t y; +}; + +struct 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; +}; + +#endif diff --git a/app/src/decoder.c b/app/src/decoder.c index 49d4ce86..aa5018b3 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,42 +1,43 @@ #include "decoder.h" #include -#include -#include -#include -#include -#include -#include "config.h" -#include "compat.h" #include "events.h" -#include "recorder.h" #include "video_buffer.h" -#include "util/buffer_util.h" +#include "trait/frame_sink.h" #include "util/log.h" -// set the decoded frame as ready for rendering, and notify +/** Downcast packet_sink to decoder */ +#define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink) + static void -push_frame(struct decoder *decoder) { - bool previous_frame_skipped; - video_buffer_offer_decoded_frame(decoder->video_buffer, - &previous_frame_skipped); - if (previous_frame_skipped) { - // the previous EVENT_NEW_FRAME will consume this frame - return; +decoder_close_first_sinks(struct decoder *decoder, unsigned count) { + while (count) { + struct sc_frame_sink *sink = decoder->sinks[--count]; + sink->ops->close(sink); } - static SDL_Event new_frame_event = { - .type = EVENT_NEW_FRAME, - }; - SDL_PushEvent(&new_frame_event); } -void -decoder_init(struct decoder *decoder, struct video_buffer *vb) { - decoder->video_buffer = vb; +static inline void +decoder_close_sinks(struct decoder *decoder) { + decoder_close_first_sinks(decoder, decoder->sink_count); } -bool +static bool +decoder_open_sinks(struct decoder *decoder) { + for (unsigned i = 0; i < decoder->sink_count; ++i) { + struct sc_frame_sink *sink = decoder->sinks[i]; + if (!sink->ops->open(sink)) { + LOGE("Could not open frame sink %d", i); + decoder_close_first_sinks(decoder, i); + return false; + } + } + + return true; +} + +static bool decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); if (!decoder->codec_ctx) { @@ -50,52 +51,110 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) { return false; } + decoder->frame = av_frame_alloc(); + if (!decoder->frame) { + LOGE("Could not create decoder frame"); + avcodec_close(decoder->codec_ctx); + avcodec_free_context(&decoder->codec_ctx); + return false; + } + + if (!decoder_open_sinks(decoder)) { + LOGE("Could not open decoder sinks"); + av_frame_free(&decoder->frame); + avcodec_close(decoder->codec_ctx); + avcodec_free_context(&decoder->codec_ctx); + return false; + } + return true; } -void +static void decoder_close(struct decoder *decoder) { + decoder_close_sinks(decoder); + av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); } -bool +static bool +push_frame_to_sinks(struct decoder *decoder, const AVFrame *frame) { + for (unsigned i = 0; i < decoder->sink_count; ++i) { + struct sc_frame_sink *sink = decoder->sinks[i]; + if (!sink->ops->push(sink, frame)) { + LOGE("Could not send frame to sink %d", i); + return false; + } + } + + return true; +} + +static bool decoder_push(struct decoder *decoder, const AVPacket *packet) { -// the new decoding/encoding API has been introduced by: -// -#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API - int ret; - if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + if (is_config) { + // nothing to do + return true; + } + + int ret = avcodec_send_packet(decoder->codec_ctx, packet); + if (ret < 0 && ret != AVERROR(EAGAIN)) { LOGE("Could not send video packet: %d", ret); return false; } - ret = avcodec_receive_frame(decoder->codec_ctx, - decoder->video_buffer->decoding_frame); + ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); if (!ret) { // a frame was received - push_frame(decoder); + bool ok = push_frame_to_sinks(decoder, decoder->frame); + // A frame lost should not make the whole pipeline fail. The error, if + // any, is already logged. + (void) ok; + + av_frame_unref(decoder->frame); } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive video frame: %d", ret); return false; } -#else - int got_picture; - int len = avcodec_decode_video2(decoder->codec_ctx, - decoder->video_buffer->decoding_frame, - &got_picture, - packet); - if (len < 0) { - LOGE("Could not decode video packet: %d", len); - return false; - } - if (got_picture) { - push_frame(decoder); - } -#endif return true; } -void -decoder_interrupt(struct decoder *decoder) { - video_buffer_interrupt(decoder->video_buffer); +static bool +decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { + struct decoder *decoder = DOWNCAST(sink); + return decoder_open(decoder, codec); +} + +static void +decoder_packet_sink_close(struct sc_packet_sink *sink) { + struct decoder *decoder = DOWNCAST(sink); + decoder_close(decoder); +} + +static bool +decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { + struct decoder *decoder = DOWNCAST(sink); + return decoder_push(decoder, packet); +} + +void +decoder_init(struct decoder *decoder) { + decoder->sink_count = 0; + + static const struct sc_packet_sink_ops ops = { + .open = decoder_packet_sink_open, + .close = decoder_packet_sink_close, + .push = decoder_packet_sink_push, + }; + + decoder->packet_sink.ops = &ops; +} + +void +decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink) { + assert(decoder->sink_count < DECODER_MAX_SINKS); + assert(sink); + assert(sink->ops); + decoder->sinks[decoder->sink_count++] = sink; } diff --git a/app/src/decoder.h b/app/src/decoder.h index f243812c..257f751a 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -1,31 +1,29 @@ #ifndef DECODER_H #define DECODER_H +#include "common.h" + +#include "trait/packet_sink.h" + #include #include -#include "config.h" - -struct video_buffer; +#define DECODER_MAX_SINKS 2 struct decoder { - struct video_buffer *video_buffer; + struct sc_packet_sink packet_sink; // packet sink trait + + struct sc_frame_sink *sinks[DECODER_MAX_SINKS]; + unsigned sink_count; + AVCodecContext *codec_ctx; + AVFrame *frame; }; void -decoder_init(struct decoder *decoder, struct video_buffer *vb); - -bool -decoder_open(struct decoder *decoder, const AVCodec *codec); +decoder_init(struct decoder *decoder); void -decoder_close(struct decoder *decoder); - -bool -decoder_push(struct decoder *decoder, const AVPacket *packet); - -void -decoder_interrupt(struct decoder *decoder); +decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink); #endif diff --git a/app/src/device.c b/app/src/device.c deleted file mode 100644 index f4c2628b..00000000 --- a/app/src/device.c +++ /dev/null @@ -1,24 +0,0 @@ -#include "device.h" - -#include "config.h" -#include "util/log.h" - -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) { - 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]; - return true; -} diff --git a/app/src/device.h b/app/src/device.h deleted file mode 100644 index 8a94cd86..00000000 --- a/app/src/device.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef DEVICE_H -#define DEVICE_H - -#include - -#include "config.h" -#include "common.h" -#include "util/net.h" - -#define DEVICE_NAME_FIELD_LENGTH 64 - -// name must be at least DEVICE_NAME_FIELD_LENGTH bytes -bool -device_read_info(socket_t device_socket, char *device_name, struct size *size); - -#endif diff --git a/app/src/device_msg.c b/app/src/device_msg.c index db176129..827f4213 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,15 +1,15 @@ #include "device_msg.h" +#include #include -#include "config.h" #include "util/buffer_util.h" #include "util/log.h" ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, struct device_msg *msg) { - if (len < 3) { + if (len < 5) { // at least type + empty string length return 0; // not available } @@ -17,22 +17,22 @@ device_msg_deserialize(const unsigned char *buf, size_t len, msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - uint16_t clipboard_len = buffer_read16be(&buf[1]); - if (clipboard_len > len - 3) { + size_t clipboard_len = buffer_read32be(&buf[1]); + if (clipboard_len > len - 5) { return 0; // not available } - char *text = SDL_malloc(clipboard_len + 1); + char *text = malloc(clipboard_len + 1); if (!text) { LOGW("Could not allocate text for clipboard"); return -1; } if (clipboard_len) { - memcpy(text, &buf[3], clipboard_len); + memcpy(text, &buf[5], clipboard_len); } text[clipboard_len] = '\0'; msg->clipboard.text = text; - return 3 + clipboard_len; + return 5 + clipboard_len; } default: LOGW("Unknown device message type: %d", (int) msg->type); @@ -43,6 +43,6 @@ device_msg_deserialize(const unsigned char *buf, size_t len, void device_msg_destroy(struct device_msg *msg) { if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { - SDL_free(msg->clipboard.text); + free(msg->clipboard.text); } } diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 04723597..888d9216 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -1,14 +1,15 @@ #ifndef DEVICEMSG_H #define DEVICEMSG_H +#include "common.h" + #include #include #include -#include "config.h" - -#define DEVICE_MSG_TEXT_MAX_LENGTH 4093 -#define DEVICE_MSG_SERIALIZED_MAX_SIZE (3 + DEVICE_MSG_TEXT_MAX_LENGTH) +#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k +// type: 1 byte; length: 4 bytes +#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, @@ -18,7 +19,7 @@ struct device_msg { enum device_msg_type type; union { struct { - char *text; // owned, to be freed by SDL_free() + char *text; // owned, to be freed by free() } clipboard; }; }; diff --git a/app/src/event_converter.c b/app/src/event_converter.c index 80ead615..a3c2da89 100644 --- a/app/src/event_converter.c +++ b/app/src/event_converter.c @@ -1,7 +1,5 @@ #include "event_converter.h" -#include "config.h" - #define MAP(FROM, TO) case FROM: *to = TO; return true #define FAIL default: return false @@ -16,7 +14,7 @@ convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { static enum android_metastate autocomplete_metastate(enum android_metastate metastate) { - // fill dependant flags + // fill dependent flags if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { metastate |= AMETA_SHIFT_ON; } @@ -92,10 +90,31 @@ convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT); MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN); MAP(SDLK_UP, AKEYCODE_DPAD_UP); + MAP(SDLK_LCTRL, AKEYCODE_CTRL_LEFT); + MAP(SDLK_RCTRL, AKEYCODE_CTRL_RIGHT); + MAP(SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT); + MAP(SDLK_RSHIFT, AKEYCODE_SHIFT_RIGHT); } - if (prefer_text) { - // do not forward alpha and space key events + if (!(mod & (KMOD_NUM | KMOD_SHIFT))) { + // Handle Numpad events when Num Lock is disabled + // If SHIFT is pressed, a text event will be sent instead + switch(from) { + MAP(SDLK_KP_0, AKEYCODE_INSERT); + MAP(SDLK_KP_1, AKEYCODE_MOVE_END); + MAP(SDLK_KP_2, AKEYCODE_DPAD_DOWN); + MAP(SDLK_KP_3, AKEYCODE_PAGE_DOWN); + MAP(SDLK_KP_4, AKEYCODE_DPAD_LEFT); + MAP(SDLK_KP_6, AKEYCODE_DPAD_RIGHT); + MAP(SDLK_KP_7, AKEYCODE_MOVE_HOME); + MAP(SDLK_KP_8, AKEYCODE_DPAD_UP); + MAP(SDLK_KP_9, AKEYCODE_PAGE_UP); + MAP(SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL); + } + } + + if (prefer_text && !(mod & KMOD_CTRL)) { + // do not forward alpha and space key events (unless Ctrl is pressed) return false; } diff --git a/app/src/event_converter.h b/app/src/event_converter.h index c41887e1..d28e9fdc 100644 --- a/app/src/event_converter.h +++ b/app/src/event_converter.h @@ -1,10 +1,11 @@ #ifndef CONVERT_H #define CONVERT_H +#include "common.h" + #include #include -#include "config.h" #include "control_msg.h" bool diff --git a/app/src/events.h b/app/src/events.h index e9512048..a4d6f3df 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,3 +1,2 @@ -#define EVENT_NEW_SESSION SDL_USEREVENT -#define EVENT_NEW_FRAME (SDL_USEREVENT + 1) -#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2) +#define EVENT_NEW_FRAME SDL_USEREVENT +#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) diff --git a/app/src/file_handler.c b/app/src/file_handler.c index ba689404..27fe6fa3 100644 --- a/app/src/file_handler.c +++ b/app/src/file_handler.c @@ -3,16 +3,14 @@ #include #include -#include "config.h" -#include "command.h" -#include "util/lock.h" +#include "adb.h" #include "util/log.h" -#define DEFAULT_PUSH_TARGET "/sdcard/" +#define DEFAULT_PUSH_TARGET "/sdcard/Download/" static void file_handler_request_destroy(struct file_handler_request *req) { - SDL_free(req->file); + free(req->file); } bool @@ -21,21 +19,23 @@ file_handler_init(struct file_handler *file_handler, const char *serial, cbuf_init(&file_handler->queue); - if (!(file_handler->mutex = SDL_CreateMutex())) { + bool ok = sc_mutex_init(&file_handler->mutex); + if (!ok) { return false; } - if (!(file_handler->event_cond = SDL_CreateCond())) { - SDL_DestroyMutex(file_handler->mutex); + ok = sc_cond_init(&file_handler->event_cond); + if (!ok) { + sc_mutex_destroy(&file_handler->mutex); return false; } if (serial) { - file_handler->serial = SDL_strdup(serial); + file_handler->serial = strdup(serial); if (!file_handler->serial) { LOGW("Could not strdup serial"); - SDL_DestroyCond(file_handler->event_cond); - SDL_DestroyMutex(file_handler->mutex); + sc_cond_destroy(&file_handler->event_cond); + sc_mutex_destroy(&file_handler->mutex); return false; } } else { @@ -55,9 +55,9 @@ file_handler_init(struct file_handler *file_handler, const char *serial, void file_handler_destroy(struct file_handler *file_handler) { - SDL_DestroyCond(file_handler->event_cond); - SDL_DestroyMutex(file_handler->mutex); - SDL_free(file_handler->serial); + sc_cond_destroy(&file_handler->event_cond); + sc_mutex_destroy(&file_handler->mutex); + free(file_handler->serial); struct file_handler_request req; while (cbuf_take(&file_handler->queue, &req)) { @@ -93,13 +93,13 @@ file_handler_request(struct file_handler *file_handler, .file = file, }; - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); bool was_empty = cbuf_is_empty(&file_handler->queue); bool res = cbuf_push(&file_handler->queue, req); if (was_empty) { - cond_signal(file_handler->event_cond); + sc_cond_signal(&file_handler->event_cond); } - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); return res; } @@ -108,14 +108,14 @@ run_file_handler(void *data) { struct file_handler *file_handler = data; for (;;) { - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); file_handler->current_process = PROCESS_NONE; while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { - cond_wait(file_handler->event_cond, file_handler->mutex); + sc_cond_wait(&file_handler->event_cond, &file_handler->mutex); } if (file_handler->stopped) { // stop immediately, do not process further events - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); break; } struct file_handler_request req; @@ -133,16 +133,16 @@ run_file_handler(void *data) { file_handler->push_target); } file_handler->current_process = process; - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); if (req.action == ACTION_INSTALL_APK) { - if (process_check_success(process, "adb install")) { + if (process_check_success(process, "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")) { + if (process_check_success(process, "adb push", false)) { LOGI("%s successfully pushed to %s", req.file, file_handler->push_target); } else { @@ -151,6 +151,14 @@ run_file_handler(void *data) { } } + sc_mutex_lock(&file_handler->mutex); + // Close the process (it is necessary 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_mutex_unlock(&file_handler->mutex); + file_handler_request_destroy(&req); } return 0; @@ -160,9 +168,9 @@ bool file_handler_start(struct file_handler *file_handler) { LOGD("Starting file_handler thread"); - file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler", - file_handler); - if (!file_handler->thread) { + bool ok = sc_thread_create(&file_handler->thread, run_file_handler, + "file_handler", file_handler); + if (!ok) { LOGC("Could not start file_handler thread"); return false; } @@ -172,20 +180,18 @@ file_handler_start(struct file_handler *file_handler) { void file_handler_stop(struct file_handler *file_handler) { - mutex_lock(file_handler->mutex); + sc_mutex_lock(&file_handler->mutex); file_handler->stopped = true; - cond_signal(file_handler->event_cond); + sc_cond_signal(&file_handler->event_cond); if (file_handler->current_process != PROCESS_NONE) { - if (!cmd_terminate(file_handler->current_process)) { - LOGW("Could not terminate install process"); + if (!process_terminate(file_handler->current_process)) { + LOGW("Could not terminate push/install process"); } - cmd_simple_wait(file_handler->current_process, NULL); - file_handler->current_process = PROCESS_NONE; } - mutex_unlock(file_handler->mutex); + sc_mutex_unlock(&file_handler->mutex); } void file_handler_join(struct file_handler *file_handler) { - SDL_WaitThread(file_handler->thread, NULL); + sc_thread_join(&file_handler->thread, NULL); } diff --git a/app/src/file_handler.h b/app/src/file_handler.h index 078d0ca5..fe1d1804 100644 --- a/app/src/file_handler.h +++ b/app/src/file_handler.h @@ -1,13 +1,13 @@ #ifndef FILE_HANDLER_H #define FILE_HANDLER_H -#include -#include -#include +#include "common.h" -#include "config.h" -#include "command.h" +#include + +#include "adb.h" #include "util/cbuf.h" +#include "util/thread.h" typedef enum { ACTION_INSTALL_APK, @@ -24,9 +24,9 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16); struct file_handler { char *serial; const char *push_target; - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *event_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond event_cond; bool stopped; bool initialized; process_t current_process; @@ -49,7 +49,7 @@ file_handler_stop(struct file_handler *file_handler); void file_handler_join(struct file_handler *file_handler); -// take ownership of file, and will SDL_free() it +// take ownership of file, and will free() it bool file_handler_request(struct file_handler *file_handler, file_handler_action_t action, diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index 58c62d55..bbf71887 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -3,27 +3,25 @@ #include #include -#include "config.h" -#include "util/lock.h" #include "util/log.h" #define FPS_COUNTER_INTERVAL_MS 1000 bool fps_counter_init(struct fps_counter *counter) { - counter->mutex = SDL_CreateMutex(); - if (!counter->mutex) { + bool ok = sc_mutex_init(&counter->mutex); + if (!ok) { return false; } - counter->state_cond = SDL_CreateCond(); - if (!counter->state_cond) { - SDL_DestroyMutex(counter->mutex); + ok = sc_cond_init(&counter->state_cond); + if (!ok) { + sc_mutex_destroy(&counter->mutex); return false; } - counter->thread = NULL; - SDL_AtomicSet(&counter->started, 0); + counter->thread_started = false; + atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started return true; @@ -31,8 +29,18 @@ fps_counter_init(struct fps_counter *counter) { void fps_counter_destroy(struct fps_counter *counter) { - SDL_DestroyCond(counter->state_cond); - SDL_DestroyMutex(counter->mutex); + sc_cond_destroy(&counter->state_cond); + sc_mutex_destroy(&counter->mutex); +} + +static inline bool +is_started(struct fps_counter *counter) { + return atomic_load_explicit(&counter->started, memory_order_acquire); +} + +static inline void +set_started(struct fps_counter *counter, bool started) { + atomic_store_explicit(&counter->started, started, memory_order_release); } // must be called with mutex locked @@ -68,12 +76,12 @@ static int run_fps_counter(void *data) { struct fps_counter *counter = data; - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); while (!counter->interrupted) { - while (!counter->interrupted && !SDL_AtomicGet(&counter->started)) { - cond_wait(counter->state_cond, counter->mutex); + while (!counter->interrupted && !is_started(counter)) { + sc_cond_wait(&counter->state_cond, &counter->mutex); } - while (!counter->interrupted && SDL_AtomicGet(&counter->started)) { + while (!counter->interrupted && is_started(counter)) { uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); @@ -81,32 +89,35 @@ run_fps_counter(void *data) { uint32_t remaining = counter->next_timestamp - now; // ignore the reason (timeout or signaled), we just loop anyway - cond_wait_timeout(counter->state_cond, counter->mutex, remaining); + sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining); } } - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); return 0; } bool fps_counter_start(struct fps_counter *counter) { - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; counter->nr_rendered = 0; counter->nr_skipped = 0; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); - SDL_AtomicSet(&counter->started, 1); - cond_signal(counter->state_cond); + set_started(counter, true); + sc_cond_signal(&counter->state_cond); - // counter->thread is always accessed from the same thread, no need to lock - if (!counter->thread) { - counter->thread = - SDL_CreateThread(run_fps_counter, "fps counter", counter); - if (!counter->thread) { + // counter->thread_started and counter->thread are always accessed from the + // same thread, no need to lock + if (!counter->thread_started) { + bool ok = sc_thread_create(&counter->thread, run_fps_counter, + "fps counter", counter); + if (!ok) { LOGE("Could not start FPS counter thread"); return false; } + + counter->thread_started = true; } return true; @@ -114,57 +125,61 @@ fps_counter_start(struct fps_counter *counter) { void fps_counter_stop(struct fps_counter *counter) { - SDL_AtomicSet(&counter->started, 0); - cond_signal(counter->state_cond); + set_started(counter, false); + sc_cond_signal(&counter->state_cond); } bool fps_counter_is_started(struct fps_counter *counter) { - return SDL_AtomicGet(&counter->started); + return is_started(counter); } void fps_counter_interrupt(struct fps_counter *counter) { - if (!counter->thread) { + if (!counter->thread_started) { return; } - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); counter->interrupted = true; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); // wake up blocking wait - cond_signal(counter->state_cond); + sc_cond_signal(&counter->state_cond); } void fps_counter_join(struct fps_counter *counter) { - if (counter->thread) { - SDL_WaitThread(counter->thread, NULL); + if (counter->thread_started) { + // interrupted must be set by the thread calling join(), so no need to + // lock for the assertion + assert(counter->interrupted); + + sc_thread_join(&counter->thread, NULL); } } void fps_counter_add_rendered_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); ++counter->nr_rendered; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); } void fps_counter_add_skipped_frame(struct fps_counter *counter) { - if (!SDL_AtomicGet(&counter->started)) { + if (!is_started(counter)) { return; } - mutex_lock(counter->mutex); + sc_mutex_lock(&counter->mutex); uint32_t now = SDL_GetTicks(); check_interval_expired(counter, now); ++counter->nr_skipped; - mutex_unlock(counter->mutex); + sc_mutex_unlock(&counter->mutex); } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 1c56bb01..de252586 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,22 +1,24 @@ #ifndef FPSCOUNTER_H #define FPSCOUNTER_H +#include "common.h" + +#include #include #include -#include -#include -#include -#include "config.h" +#include "util/thread.h" struct fps_counter { - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *state_cond; + sc_thread thread; + sc_mutex mutex; + sc_cond state_cond; + + bool thread_started; // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily - SDL_atomic_t started; + atomic_bool started; // the following fields are protected by the mutex bool interrupted; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 8c4c230a..a5d0ad07 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,42 +1,87 @@ #include "input_manager.h" #include +#include -#include "config.h" #include "event_converter.h" -#include "util/lock.h" #include "util/log.h" -// Convert window coordinates (as provided by SDL_GetMouseState() to renderer -// coordinates (as provided in SDL mouse events) -// -// See my question: -// -static void -convert_to_renderer_coordinates(SDL_Renderer *renderer, int *x, int *y) { - SDL_Rect viewport; - float scale_x, scale_y; - SDL_RenderGetViewport(renderer, &viewport); - SDL_RenderGetScale(renderer, &scale_x, &scale_y); - *x = (int) (*x / scale_x) - viewport.x; - *y = (int) (*y / scale_y) - viewport.y; -} - -static struct point -get_mouse_point(struct screen *screen) { - int x; - int y; - SDL_GetMouseState(&x, &y); - convert_to_renderer_coordinates(screen->renderer, &x, &y); - return (struct point) { - .x = x, - .y = y, - }; -} - static const int ACTION_DOWN = 1; static const int ACTION_UP = 1 << 1; +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +static inline uint16_t +to_sdl_mod(unsigned mod) { + uint16_t sdl_mod = 0; + if (mod & SC_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (mod & SC_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (mod & SC_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (mod & SC_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (mod & SC_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (mod & SC_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static bool +is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { + // keep only the relevant modifier keys + sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; + + assert(im->sdl_shortcut_mods.count); + assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS); + for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) { + if (im->sdl_shortcut_mods.data[i] == sdl_mod) { + return true; + } + } + + return false; +} + +void +input_manager_init(struct input_manager *im, struct controller *controller, + struct screen *screen, + const struct scrcpy_options *options) { + im->controller = controller; + im->screen = screen; + im->repeat = 0; + + 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; + + const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; + assert(shortcut_mods->count); + assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); + for (unsigned i = 0; i < shortcut_mods->count; ++i) { + uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]); + assert(sdl_mod); + im->sdl_shortcut_mods.data[i] = sdl_mod; + } + im->sdl_shortcut_mods.count = shortcut_mods->count; + + im->vfinger_down = false; + + im->last_keycode = SDLK_UNKNOWN; + im->last_mod = 0; + im->key_repeat = 0; +} + static void send_keycode(struct controller *controller, enum android_keycode keycode, int actions, const char *name) { @@ -45,6 +90,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode, msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; + msg.inject_keycode.repeat = 0; if (actions & ACTION_DOWN) { msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; @@ -97,14 +143,36 @@ action_menu(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); } +static inline void +action_copy(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_COPY, actions, "COPY"); +} + +static inline void +action_cut(struct controller *controller, int actions) { + send_keycode(controller, AKEYCODE_CUT, actions, "CUT"); +} + // turn the screen on if it was off, press BACK otherwise +// If the screen is off, it is turned on only on ACTION_DOWN static void -press_back_or_turn_screen_on(struct controller *controller) { +press_back_or_turn_screen_on(struct controller *controller, int actions) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'press back or turn screen on'"); + if (actions & ACTION_DOWN) { + msg.back_or_screen_on.action = AKEY_EVENT_ACTION_DOWN; + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'press back or turn screen on'"); + return; + } + } + + if (actions & ACTION_UP) { + msg.back_or_screen_on.action = AKEY_EVENT_ACTION_UP; + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'press back or turn screen on'"); + } } } @@ -119,9 +187,19 @@ expand_notification_panel(struct controller *controller) { } static void -collapse_notification_panel(struct controller *controller) { +expand_settings_panel(struct controller *controller) { struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; + msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; + + if (!controller_push_msg(controller, &msg)) { + LOGW("Could not request 'expand settings panel'"); + } +} + +static void +collapse_panels(struct controller *controller) { + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); @@ -129,17 +207,7 @@ collapse_notification_panel(struct controller *controller) { } static void -request_device_clipboard(struct controller *controller) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; - - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request device clipboard"); - } -} - -static void -set_device_clipboard(struct controller *controller) { +set_device_clipboard(struct controller *controller, bool paste) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -151,12 +219,20 @@ set_device_clipboard(struct controller *controller) { return; } + char *text_dup = strdup(text); + SDL_free(text); + if (!text_dup) { + LOGW("Could not strdup input text"); + return; + } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; - msg.set_clipboard.text = text; + msg.set_clipboard.text = text_dup; + msg.set_clipboard.paste = paste; if (!controller_push_msg(controller, &msg)) { - SDL_free(text); + free(text_dup); LOGW("Could not request 'set device clipboard'"); } } @@ -202,11 +278,18 @@ clipboard_paste(struct controller *controller) { return; } + char *text_dup = strdup(text); + SDL_free(text); + if (!text_dup) { + LOGW("Could not strdup input text"); + return; + } + struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = text; + msg.inject_text.text = text_dup; if (!controller_push_msg(controller, &msg)) { - SDL_free(text); + free(text_dup); LOGW("Could not request 'paste clipboard'"); } } @@ -221,9 +304,25 @@ rotate_device(struct controller *controller) { } } -void +static void +rotate_client_left(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 1) % 4; + screen_set_rotation(screen, new_rotation); +} + +static void +rotate_client_right(struct screen *screen) { + unsigned new_rotation = (screen->rotation + 3) % 4; + screen_set_rotation(screen, new_rotation); +} + +static void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { + if (is_shortcut_mod(im, SDL_GetModState())) { + // A shortcut must never generate text events + return; + } if (!im->prefer_text) { char c = event->text[0]; if (isalpha(c) || c == ' ') { @@ -235,20 +334,50 @@ input_manager_process_text_input(struct input_manager *im, struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = SDL_strdup(event->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)) { - SDL_free(msg.inject_text.text); + free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } +static bool +simulate_virtual_finger(struct input_manager *im, + enum android_motionevent_action action, + struct point point) { + bool up = action == AMOTION_EVENT_ACTION_UP; + + struct control_msg msg; + msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + msg.inject_touch_event.action = action; + msg.inject_touch_event.position.screen_size = im->screen->frame_size; + msg.inject_touch_event.position.point = point; + msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER; + msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; + msg.inject_touch_event.buttons = 0; + + if (!controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject virtual finger event'"); + return false; + } + + return true; +} + +static struct point +inverse_point(struct point point, struct 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) { + bool prefer_text, uint32_t repeat) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { @@ -261,145 +390,153 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, return false; } + to->inject_keycode.repeat = repeat; to->inject_keycode.metastate = convert_meta_state(mod); return true; } -void +static void input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event, - bool control) { + const SDL_KeyboardEvent *event) { // control: indicates the state of the command-line option --no-control - // ctrl: the Ctrl key - - bool ctrl = event->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL); - bool alt = event->keysym.mod & (KMOD_LALT | KMOD_RALT); - bool meta = event->keysym.mod & (KMOD_LGUI | KMOD_RGUI); - - // use Cmd on macOS, Ctrl on other platforms -#ifdef __APPLE__ - bool cmd = !ctrl && meta; -#else - if (meta) { - // no shortcuts involve Meta on platforms other than macOS, and it must - // not be forwarded to the device - return; - } - bool cmd = ctrl; // && !meta, already guaranteed -#endif - - if (alt) { - // no shortcuts involve Alt, and it must not be forwarded to the device - return; - } + bool control = im->control; struct controller *controller = im->controller; - // capture all Ctrl events - if (ctrl || cmd) { - SDL_Keycode keycode = event->keysym.sym; - bool down = event->type == SDL_KEYDOWN; + SDL_Keycode keycode = event->keysym.sym; + uint16_t mod = event->keysym.mod; + bool down = event->type == SDL_KEYDOWN; + bool ctrl = event->keysym.mod & KMOD_CTRL; + bool shift = event->keysym.mod & KMOD_SHIFT; + bool repeat = event->repeat; + + bool smod = is_shortcut_mod(im, mod); + + if (down && !repeat) { + if (keycode == im->last_keycode && mod == im->last_mod) { + ++im->key_repeat; + } else { + im->key_repeat = 0; + im->last_keycode = keycode; + im->last_mod = mod; + } + } + + // The shortcut modifier is pressed + if (smod) { int action = down ? ACTION_DOWN : ACTION_UP; - bool repeat = event->repeat; - bool shift = event->keysym.mod & (KMOD_LSHIFT | KMOD_RSHIFT); switch (keycode) { case SDLK_h: - // Ctrl+h on all platform, since Cmd+h is already captured by - // the system on macOS to hide the window - if (control && ctrl && !meta && !shift && !repeat) { + if (control && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && cmd && !shift && !repeat) { + if (control && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: - if (control && cmd && !shift && !repeat) { + if (control && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: - // Ctrl+m on all platform, since Cmd+m is already captured by - // the system on macOS to minimize the window - if (control && ctrl && !meta && !shift && !repeat) { + if (control && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: - if (control && cmd && !shift && !repeat) { + if (control && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: - if (control && cmd && !shift && down) { - set_screen_power_mode(controller, SCREEN_POWER_MODE_OFF); + if (control && !repeat && down) { + enum screen_power_mode mode = shift + ? SCREEN_POWER_MODE_NORMAL + : SCREEN_POWER_MODE_OFF; + set_screen_power_mode(controller, mode); } return; case SDLK_DOWN: - if (control && cmd && !shift) { + if (control && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (control && cmd && !shift) { + if (control && !shift) { // forward repeated events action_volume_up(controller, action); } return; + case SDLK_LEFT: + if (!shift && !repeat && down) { + rotate_client_left(im->screen); + } + return; + case SDLK_RIGHT: + if (!shift && !repeat && down) { + rotate_client_right(im->screen); + } + return; case SDLK_c: - if (control && cmd && !shift && !repeat && down) { - request_device_clipboard(controller); + if (control && !shift && !repeat) { + action_copy(controller, action); + } + return; + case SDLK_x: + if (control && !shift && !repeat) { + action_cut(controller, action); } return; case SDLK_v: - if (control && cmd && !repeat && down) { - if (shift) { - // store the text in the device clipboard - set_device_clipboard(controller); - } else { + if (control && !repeat && down) { + if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(controller); + } else { + // store the text in the device clipboard and paste + set_device_clipboard(controller, true); } } return; case SDLK_f: - if (!shift && cmd && !repeat && down) { + if (!shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; - case SDLK_x: - if (!shift && cmd && !repeat && down) { + case SDLK_w: + if (!shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: - if (!shift && cmd && !repeat && down) { + if (!shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: - if (!shift && cmd && !repeat && down) { - struct fps_counter *fps_counter = - im->video_buffer->fps_counter; - switch_fps_counter_state(fps_counter); + if (!shift && !repeat && down) { + switch_fps_counter_state(&im->screen->fps_counter); } return; case SDLK_n: - if (control && cmd && !repeat && down) { + if (control && !repeat && down) { if (shift) { - collapse_notification_panel(controller); - } else { + collapse_panels(controller); + } else if (im->key_repeat == 0) { expand_notification_panel(controller); + } else { + expand_settings_panel(controller); } } return; case SDLK_r: - if (control && cmd && !shift && !repeat && down) { + if (control && !shift && !repeat && down) { rotate_device(controller); } return; @@ -412,8 +549,28 @@ 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 + clipboard_paste(controller); + return; + } + // Synchronize the computer clipboard to the device clipboard before + // sending Ctrl+v, to allow seamless copy-paste. + set_device_clipboard(controller, false); + } + struct control_msg msg; - if (convert_input_key(event, &msg, im->prefer_text)) { + if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) { if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject keycode'"); } @@ -427,19 +584,23 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, 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.x = from->x; - to->inject_touch_event.position.point.y = from->y; + 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; } -void +static void input_manager_process_mouse_motion(struct input_manager *im, const SDL_MouseMotionEvent *event) { - if (!event->state) { - // do not send motion events when no button is pressed + uint32_t mask = SDL_BUTTON_LMASK; + if (im->forward_all_clicks) { + mask |= SDL_BUTTON_MMASK | SDL_BUTTON_RMASK; + } + if (!(event->state & mask)) { + // do not send motion events when no click is pressed return; } if (event->which == SDL_TOUCH_MOUSEID) { @@ -447,10 +608,18 @@ input_manager_process_mouse_motion(struct input_manager *im, return; } struct control_msg msg; - if (convert_mouse_motion(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse motion event'"); - } + if (!convert_mouse_motion(event, im->screen, &msg)) { + return; + } + + if (!controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject mouse motion event'"); + } + + if (im->vfinger_down) { + struct point mouse = msg.inject_touch_event.position.point; + struct point vfinger = inverse_point(mouse, im->screen->frame_size); + simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } @@ -463,19 +632,25 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, return false; } - struct size frame_size = screen->frame_size; - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = frame_size; + 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] - to->inject_touch_event.position.point.x = from->x * frame_size.width; - to->inject_touch_event.position.point.y = from->y * frame_size.height; + 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; } -void +static void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { struct control_msg msg; @@ -486,13 +661,6 @@ input_manager_process_touch(struct input_manager *im, } } -static bool -is_outside_device_screen(struct input_manager *im, int x, int y) -{ - return x < 0 || x >= im->screen->frame_size.width || - y < 0 || y >= im->screen->frame_size.height; -} - static bool convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, struct control_msg *to) { @@ -504,38 +672,63 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, 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.x = from->x; - to->inject_touch_event.position.point.y = from->y; - to->inject_touch_event.pressure = 1.f; + 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; } -void +static void input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event, - bool control) { + const SDL_MouseButtonEvent *event) { + bool control = im->control; + if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } - if (event->type == SDL_MOUSEBUTTONDOWN) { + + bool down = event->type == SDL_MOUSEBUTTONDOWN; + if (!im->forward_all_clicks) { + int action = down ? ACTION_DOWN : ACTION_UP; + + if (control && event->button == SDL_BUTTON_X1) { + action_app_switch(im->controller, action); + return; + } + if (control && event->button == SDL_BUTTON_X2 && down) { + if (event->clicks < 2) { + expand_notification_panel(im->controller); + } else { + expand_settings_panel(im->controller); + } + return; + } if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im->controller); + press_back_or_turn_screen_on(im->controller, action); return; } if (control && event->button == SDL_BUTTON_MIDDLE) { - action_home(im->controller, ACTION_DOWN | ACTION_UP); + action_home(im->controller, action); return; } + // double-click on black borders resize to fit the device screen if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) { - bool outside = - is_outside_device_screen(im, event->x, event->y); + int32_t x = event->x; + int32_t y = event->y; + screen_hidpi_scale_coords(im->screen, &x, &y); + SDL_Rect *r = &im->screen->rect; + bool outside = x < r->x || x >= r->x + r->w + || y < r->y || y >= r->y + r->h; if (outside) { - screen_resize_to_fit(im->screen); + if (down) { + screen_resize_to_fit(im->screen); + } return; } } @@ -547,19 +740,52 @@ input_manager_process_mouse_button(struct input_manager *im, } struct control_msg msg; - if (convert_mouse_button(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse button event'"); + 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; + } + + // Pinch-to-zoom simulation. + // + // If Ctrl is hold when the left-click button is pressed, then + // pinch-to-zoom mode is enabled: on every mouse event until the left-click + // button is released, an additional "virtual finger" event is generated, + // having a position inverted through the center of the screen. + // + // In other words, the center of the rotation/scaling is the center of the + // screen. +#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); + enum android_motionevent_action action = down + ? AMOTION_EVENT_ACTION_DOWN + : AMOTION_EVENT_ACTION_UP; + if (!simulate_virtual_finger(im, action, vfinger)) { + return; } + im->vfinger_down = down; } } 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 = get_mouse_point(screen), + .point = screen_convert_window_to_frame_coords(screen, + mouse_x, mouse_y), }; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; @@ -571,7 +797,7 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, return true; } -void +static void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { struct control_msg msg; @@ -581,3 +807,46 @@ input_manager_process_mouse_wheel(struct input_manager *im, } } } + +bool +input_manager_handle_event(struct input_manager *im, SDL_Event *event) { + switch (event->type) { + case SDL_TEXTINPUT: + if (!im->control) { + return true; + } + input_manager_process_text_input(im, &event->text); + return true; + case SDL_KEYDOWN: + case SDL_KEYUP: + // some key events do not interact with the device, so process the + // event even if control is disabled + input_manager_process_key(im, &event->key); + return true; + case SDL_MOUSEMOTION: + if (!im->control) { + break; + } + input_manager_process_mouse_motion(im, &event->motion); + return true; + case SDL_MOUSEWHEEL: + if (!im->control) { + break; + } + input_manager_process_mouse_wheel(im, &event->wheel); + return true; + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + // some mouse events do not interact with the device, so process + // the event even if control is disabled + input_manager_process_mouse_button(im, &event->button); + return true; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + input_manager_process_touch(im, &event->tfinger); + return true; + } + + return false; +} diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 43fc0eeb..1dd7825f 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -1,46 +1,51 @@ #ifndef INPUTMANAGER_H #define INPUTMANAGER_H +#include "common.h" + #include -#include "config.h" -#include "common.h" +#include + #include "controller.h" #include "fps_counter.h" -#include "video_buffer.h" +#include "scrcpy.h" #include "screen.h" struct input_manager { struct controller *controller; - struct video_buffer *video_buffer; struct screen *screen; + + // SDL reports repeated events as a boolean, but Android expects the actual + // number of repetitions. This variable keeps track of the count. + unsigned repeat; + + bool control; + bool forward_key_repeat; bool prefer_text; + bool forward_all_clicks; + bool legacy_paste; + + struct { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; + } sdl_shortcut_mods; + + bool vfinger_down; + + // Tracks the number of identical consecutive shortcut key down events. + // Not to be confused with event->repeat, which counts the number of + // system-generated repeated key presses. + unsigned key_repeat; + SDL_Keycode last_keycode; + uint16_t last_mod; }; void -input_manager_process_text_input(struct input_manager *im, - const SDL_TextInputEvent *event); +input_manager_init(struct input_manager *im, struct controller *controller, + struct screen *screen, const struct scrcpy_options *options); -void -input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event, - bool control); - -void -input_manager_process_mouse_motion(struct input_manager *im, - const SDL_MouseMotionEvent *event); - -void -input_manager_process_touch(struct input_manager *im, - const SDL_TouchFingerEvent *event); - -void -input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event, - bool control); - -void -input_manager_process_mouse_wheel(struct input_manager *im, - const SDL_MouseWheelEvent *event); +bool +input_manager_handle_event(struct input_manager *im, SDL_Event *event); #endif diff --git a/app/src/main.c b/app/src/main.c index d683c508..2afa3c4e 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,14 +1,18 @@ #include "scrcpy.h" +#include "common.h" + +#include #include #include #include +#ifdef HAVE_V4L2 +# include +#endif #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include -#include "config.h" #include "cli.h" -#include "compat.h" #include "util/log.h" static void @@ -27,6 +31,11 @@ print_version(void) { fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO); +#ifdef HAVE_V4L2 + fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR, + LIBAVDEVICE_VERSION_MINOR, + LIBAVDEVICE_VERSION_MICRO); +#endif } int @@ -38,20 +47,22 @@ main(int argc, char *argv[]) { setbuf(stderr, NULL); #endif -#ifndef NDEBUG - SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); -#endif - struct scrcpy_cli_args args = { .opts = SCRCPY_OPTIONS_DEFAULT, .help = false, .version = false, }; +#ifndef NDEBUG + args.opts.log_level = SC_LOG_LEVEL_DEBUG; +#endif + if (!scrcpy_parse_args(&args, argc, argv)) { return 1; } + sc_set_log_level(args.opts.log_level); + if (args.help) { scrcpy_print_usage(argv[0]); return 0; @@ -68,6 +79,12 @@ main(int argc, char *argv[]) { av_register_all(); #endif +#ifdef HAVE_V4L2 + if (args.opts.v4l2_device) { + avdevice_register_all(); + } +#endif + if (avformat_network_init()) { return 1; } @@ -76,11 +93,5 @@ main(int argc, char *argv[]) { avformat_network_deinit(); // ignore failure -#if defined (__WINDOWS__) && ! defined (WINDOWS_NOCONSOLE) - if (res != 0) { - fprintf(stderr, "Press any key to continue...\n"); - getchar(); - } -#endif return res; } diff --git a/app/src/opengl.c b/app/src/opengl.c new file mode 100644 index 00000000..da05c082 --- /dev/null +++ b/app/src/opengl.c @@ -0,0 +1,56 @@ +#include "opengl.h" + +#include +#include +#include "SDL2/SDL.h" + +void +sc_opengl_init(struct sc_opengl *gl) { + gl->GetString = SDL_GL_GetProcAddress("glGetString"); + assert(gl->GetString); + + gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf"); + assert(gl->TexParameterf); + + gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri"); + assert(gl->TexParameteri); + + // optional + gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap"); + + const char *version = (const char *) gl->GetString(GL_VERSION); + assert(version); + gl->version = version; + +#define OPENGL_ES_PREFIX "OpenGL ES " + /* starts with "OpenGL ES " */ + gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX, + sizeof(OPENGL_ES_PREFIX) - 1); + if (gl->is_opengles) { + /* skip the prefix */ + version += sizeof(PREFIX) - 1; + } + + int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); + if (r != 2) { + // failed to parse the version + gl->version_major = 0; + gl->version_minor = 0; + } +} + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor) +{ + if (gl->is_opengles) { + return gl->version_major > minver_es_major + || (gl->version_major == minver_es_major + && gl->version_minor >= minver_es_minor); + } + + return gl->version_major > minver_major + || (gl->version_major == minver_major + && gl->version_minor >= minver_minor); +} diff --git a/app/src/opengl.h b/app/src/opengl.h new file mode 100644 index 00000000..81163704 --- /dev/null +++ b/app/src/opengl.h @@ -0,0 +1,36 @@ +#ifndef SC_OPENGL_H +#define SC_OPENGL_H + +#include "common.h" + +#include +#include + +struct sc_opengl { + const char *version; + bool is_opengles; + int version_major; + int version_minor; + + const GLubyte * + (*GetString)(GLenum name); + + void + (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); + + void + (*TexParameteri)(GLenum target, GLenum pname, GLint param); + + void + (*GenerateMipmap)(GLenum target); +}; + +void +sc_opengl_init(struct sc_opengl *gl); + +bool +sc_opengl_version_at_least(struct sc_opengl *gl, + int minver_major, int minver_minor, + int minver_es_major, int minver_es_minor); + +#endif diff --git a/app/src/receiver.c b/app/src/receiver.c index 0474ff55..337d2a17 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -3,14 +3,13 @@ #include #include -#include "config.h" #include "device_msg.h" -#include "util/lock.h" #include "util/log.h" bool receiver_init(struct receiver *receiver, socket_t control_socket) { - if (!(receiver->mutex = SDL_CreateMutex())) { + bool ok = sc_mutex_init(&receiver->mutex); + if (!ok) { return false; } receiver->control_socket = control_socket; @@ -19,16 +18,25 @@ receiver_init(struct receiver *receiver, socket_t control_socket) { void receiver_destroy(struct receiver *receiver) { - SDL_DestroyMutex(receiver->mutex); + sc_mutex_destroy(&receiver->mutex); } static void process_msg(struct device_msg *msg) { switch (msg->type) { - case DEVICE_MSG_TYPE_CLIPBOARD: + case DEVICE_MSG_TYPE_CLIPBOARD: { + char *current = SDL_GetClipboardText(); + bool same = current && !strcmp(current, msg->clipboard.text); + SDL_free(current); + if (same) { + LOGD("Computer clipboard unchanged"); + return; + } + LOGI("Device clipboard copied"); SDL_SetClipboardText(msg->clipboard.text); break; + } } } @@ -60,28 +68,29 @@ static int run_receiver(void *data) { struct receiver *receiver = data; - unsigned char buf[DEVICE_MSG_SERIALIZED_MAX_SIZE]; + static unsigned char buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { - assert(head < DEVICE_MSG_SERIALIZED_MAX_SIZE); - ssize_t r = net_recv(receiver->control_socket, buf, - DEVICE_MSG_SERIALIZED_MAX_SIZE - head); + assert(head < DEVICE_MSG_MAX_SIZE); + ssize_t r = net_recv(receiver->control_socket, buf + head, + DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); break; } - ssize_t consumed = process_msgs(buf, r); + head += r; + ssize_t consumed = process_msgs(buf, head); if (consumed == -1) { // an error occurred break; } if (consumed) { + head -= consumed; // shift the remaining data in the buffer - memmove(buf, &buf[consumed], r - consumed); - head = r - consumed; + memmove(buf, &buf[consumed], head); } } @@ -92,8 +101,9 @@ bool receiver_start(struct receiver *receiver) { LOGD("Starting receiver thread"); - receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); - if (!receiver->thread) { + bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver", + receiver); + if (!ok) { LOGC("Could not start receiver thread"); return false; } @@ -103,5 +113,5 @@ receiver_start(struct receiver *receiver) { void receiver_join(struct receiver *receiver) { - SDL_WaitThread(receiver->thread, NULL); + sc_thread_join(&receiver->thread, NULL); } diff --git a/app/src/receiver.h b/app/src/receiver.h index 8387903b..36523b62 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -1,19 +1,19 @@ #ifndef RECEIVER_H #define RECEIVER_H -#include -#include -#include +#include "common.h" + +#include -#include "config.h" #include "util/net.h" +#include "util/thread.h" // receive events from the device // managed by the controller struct receiver { socket_t control_socket; - SDL_Thread *thread; - SDL_mutex *mutex; + sc_thread thread; + sc_mutex mutex; }; bool diff --git a/app/src/recorder.c b/app/src/recorder.c index 465b24e8..85570324 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -3,10 +3,11 @@ #include #include -#include "config.h" -#include "compat.h" -#include "util/lock.h" #include "util/log.h" +#include "util/str_util.h" + +/** Downcast packet_sink to recorder */ +#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -22,24 +23,27 @@ find_muxer(const char *name) { #else oformat = av_oformat_next(oformat); #endif - // until null or with name "mp4" - } while (oformat && strcmp(oformat->name, name)); + // until null or containing the requested name + } while (oformat && !strlist_contains(oformat->name, ',', name)); return oformat; } static struct record_packet * record_packet_new(const AVPacket *packet) { - struct record_packet *rec = SDL_malloc(sizeof(*rec)); + struct record_packet *rec = malloc(sizeof(*rec)); if (!rec) { return NULL; } - // av_packet_ref() does not initialize all fields in old FFmpeg versions - // See - av_init_packet(&rec->packet); + rec->packet = av_packet_alloc(); + if (!rec->packet) { + free(rec); + return NULL; + } - if (av_packet_ref(&rec->packet, packet)) { - SDL_free(rec); + if (av_packet_ref(rec->packet, packet)) { + av_packet_free(&rec->packet); + free(rec); return NULL; } return rec; @@ -47,8 +51,9 @@ record_packet_new(const AVPacket *packet) { static void record_packet_delete(struct record_packet *rec) { - av_packet_unref(&rec->packet); - SDL_free(rec); + av_packet_unref(rec->packet); + av_packet_free(&rec->packet); + free(rec); } static void @@ -60,141 +65,15 @@ recorder_queue_clear(struct recorder_queue *queue) { } } -bool -recorder_init(struct recorder *recorder, - const char *filename, - enum recorder_format format, - struct size declared_frame_size) { - recorder->filename = SDL_strdup(filename); - if (!recorder->filename) { - LOGE("Could not strdup filename"); - return false; - } - - recorder->mutex = SDL_CreateMutex(); - if (!recorder->mutex) { - LOGC("Could not create mutex"); - SDL_free(recorder->filename); - return false; - } - - recorder->queue_cond = SDL_CreateCond(); - if (!recorder->queue_cond) { - LOGC("Could not create cond"); - SDL_DestroyMutex(recorder->mutex); - SDL_free(recorder->filename); - return false; - } - - queue_init(&recorder->queue); - recorder->stopped = false; - recorder->failed = false; - recorder->format = format; - recorder->declared_frame_size = declared_frame_size; - recorder->header_written = false; - recorder->previous = NULL; - - return true; -} - -void -recorder_destroy(struct recorder *recorder) { - SDL_DestroyCond(recorder->queue_cond); - SDL_DestroyMutex(recorder->mutex); - SDL_free(recorder->filename); -} - static const char * -recorder_get_format_name(enum recorder_format format) { +recorder_get_format_name(enum sc_record_format format) { switch (format) { - case RECORDER_FORMAT_MP4: return "mp4"; - case RECORDER_FORMAT_MKV: return "matroska"; + case SC_RECORD_FORMAT_MP4: return "mp4"; + case SC_RECORD_FORMAT_MKV: return "matroska"; default: return NULL; } } -bool -recorder_open(struct recorder *recorder, const AVCodec *input_codec) { - const char *format_name = recorder_get_format_name(recorder->format); - assert(format_name); - const AVOutputFormat *format = find_muxer(format_name); - if (!format) { - LOGE("Could not find muxer"); - return false; - } - - recorder->ctx = avformat_alloc_context(); - if (!recorder->ctx) { - LOGE("Could not allocate output context"); - return false; - } - - // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() - // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat - // still expects a pointer-to-non-const (it has not be updated accordingly) - // - recorder->ctx->oformat = (AVOutputFormat *) format; - - av_dict_set(&recorder->ctx->metadata, "comment", - "Recorded by scrcpy " SCRCPY_VERSION, 0); - - AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); - if (!ostream) { - avformat_free_context(recorder->ctx); - return false; - } - -#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API - ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codecpar->codec_id = input_codec->id; - ostream->codecpar->format = AV_PIX_FMT_YUV420P; - ostream->codecpar->width = recorder->declared_frame_size.width; - ostream->codecpar->height = recorder->declared_frame_size.height; -#else - ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO; - ostream->codec->codec_id = input_codec->id; - ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P; - ostream->codec->width = recorder->declared_frame_size.width; - ostream->codec->height = recorder->declared_frame_size.height; -#endif - - int ret = avio_open(&recorder->ctx->pb, recorder->filename, - AVIO_FLAG_WRITE); - if (ret < 0) { - LOGE("Failed to open output file: %s", recorder->filename); - // ostream will be cleaned up during context cleaning - avformat_free_context(recorder->ctx); - return false; - } - - LOGI("Recording started to %s file: %s", format_name, recorder->filename); - - return true; -} - -void -recorder_close(struct recorder *recorder) { - if (recorder->header_written) { - int ret = av_write_trailer(recorder->ctx); - if (ret < 0) { - LOGE("Failed to write trailer to %s", recorder->filename); - recorder->failed = true; - } - } else { - // the recorded file is empty - recorder->failed = true; - } - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); - - if (recorder->failed) { - 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); - } -} - static bool recorder_write_header(struct recorder *recorder, const AVPacket *packet) { AVStream *ostream = recorder->ctx->streams[0]; @@ -208,13 +87,8 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { // copy the first packet to the extra data memcpy(extradata, packet->data, packet->size); -#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; -#else - ostream->codec->extradata = extradata; - ostream->codec->extradata_size = packet->size; -#endif int ret = avformat_write_header(recorder->ctx, NULL); if (ret < 0) { @@ -231,7 +105,7 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); } -bool +static bool recorder_write(struct recorder *recorder, AVPacket *packet) { if (!recorder->header_written) { if (packet->pts != AV_NOPTS_VALUE) { @@ -260,22 +134,22 @@ run_recorder(void *data) { struct recorder *recorder = data; for (;;) { - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); while (!recorder->stopped && queue_is_empty(&recorder->queue)) { - cond_wait(recorder->queue_cond, recorder->mutex); + 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)) { - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); struct record_packet *last = recorder->previous; if (last) { // assign an arbitrary duration to the last packet - last->packet.duration = 100000; - bool ok = recorder_write(recorder, &last->packet); + last->packet->duration = 100000; + bool ok = recorder_write(recorder, last->packet); if (!ok) { // failing to write the last frame is not very serious, no // future frame may depend on it, so the resulting file @@ -290,7 +164,7 @@ run_recorder(void *data) { struct record_packet *rec; queue_take(&recorder->queue, next, &rec); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); // recorder->previous is only written from this thread, no need to lock struct record_packet *previous = recorder->previous; @@ -302,25 +176,45 @@ run_recorder(void *data) { } // config packets have no PTS, we must ignore them - if (rec->packet.pts != AV_NOPTS_VALUE - && previous->packet.pts != AV_NOPTS_VALUE) { + if (rec->packet->pts != AV_NOPTS_VALUE + && previous->packet->pts != AV_NOPTS_VALUE) { // we now know the duration of the previous packet - previous->packet.duration = rec->packet.pts - previous->packet.pts; + previous->packet->duration = + rec->packet->pts - previous->packet->pts; } - bool ok = recorder_write(recorder, &previous->packet); + bool ok = recorder_write(recorder, previous->packet); record_packet_delete(previous); if (!ok) { LOGE("Could not record packet"); - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); recorder->failed = true; // discard pending packets recorder_queue_clear(&recorder->queue); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); break; } + } + if (!recorder->failed) { + if (recorder->header_written) { + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + recorder->failed = true; + } + } else { + // the recorded file is empty + recorder->failed = true; + } + } + + if (recorder->failed) { + LOGE("Recording failed to %s", recorder->filename); + } else { + const char *format_name = recorder_get_format_name(recorder->format); + LOGI("Recording complete to %s file: %s", format_name, recorder->filename); } LOGD("Recorder thread ended"); @@ -328,51 +222,176 @@ run_recorder(void *data) { return 0; } -bool -recorder_start(struct recorder *recorder) { - LOGD("Starting recorder thread"); - - recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder); - if (!recorder->thread) { - LOGC("Could not start recorder thread"); +static bool +recorder_open(struct recorder *recorder, const AVCodec *input_codec) { + bool ok = sc_mutex_init(&recorder->mutex); + if (!ok) { + LOGC("Could not create mutex"); return false; } + ok = sc_cond_init(&recorder->queue_cond); + if (!ok) { + LOGC("Could not create cond"); + goto error_mutex_destroy; + } + + queue_init(&recorder->queue); + recorder->stopped = false; + recorder->failed = false; + recorder->header_written = false; + recorder->previous = NULL; + + const char *format_name = recorder_get_format_name(recorder->format); + assert(format_name); + const AVOutputFormat *format = find_muxer(format_name); + if (!format) { + LOGE("Could not find muxer"); + goto error_cond_destroy; + } + + recorder->ctx = avformat_alloc_context(); + if (!recorder->ctx) { + LOGE("Could not allocate output context"); + goto error_cond_destroy; + } + + // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() + // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat + // still expects a pointer-to-non-const (it has not be updated accordingly) + // + recorder->ctx->oformat = (AVOutputFormat *) format; + + av_dict_set(&recorder->ctx->metadata, "comment", + "Recorded by scrcpy " SCRCPY_VERSION, 0); + + AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); + if (!ostream) { + goto error_avformat_free_context; + } + + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = input_codec->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = recorder->declared_frame_size.width; + ostream->codecpar->height = recorder->declared_frame_size.height; + + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output file: %s", recorder->filename); + // ostream will be cleaned up during context cleaning + goto error_avformat_free_context; + } + + LOGD("Starting recorder thread"); + ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", + recorder); + if (!ok) { + LOGC("Could not start recorder thread"); + goto error_avio_close; + } + + LOGI("Recording started to %s file: %s", format_name, recorder->filename); + return true; + +error_avio_close: + avio_close(recorder->ctx->pb); +error_avformat_free_context: + avformat_free_context(recorder->ctx); +error_cond_destroy: + sc_cond_destroy(&recorder->queue_cond); +error_mutex_destroy: + sc_mutex_destroy(&recorder->mutex); + + return false; } -void -recorder_stop(struct recorder *recorder) { - mutex_lock(recorder->mutex); +static void +recorder_close(struct recorder *recorder) { + sc_mutex_lock(&recorder->mutex); recorder->stopped = true; - cond_signal(recorder->queue_cond); - mutex_unlock(recorder->mutex); + sc_cond_signal(&recorder->queue_cond); + sc_mutex_unlock(&recorder->mutex); + + sc_thread_join(&recorder->thread, NULL); + + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); + sc_cond_destroy(&recorder->queue_cond); + sc_mutex_destroy(&recorder->mutex); } -void -recorder_join(struct recorder *recorder) { - SDL_WaitThread(recorder->thread, NULL); -} - -bool +static bool recorder_push(struct recorder *recorder, const AVPacket *packet) { - mutex_lock(recorder->mutex); + sc_mutex_lock(&recorder->mutex); assert(!recorder->stopped); if (recorder->failed) { // reject any new packet (this will stop the stream) + sc_mutex_unlock(&recorder->mutex); return false; } struct record_packet *rec = record_packet_new(packet); if (!rec) { LOGC("Could not allocate record packet"); + sc_mutex_unlock(&recorder->mutex); return false; } queue_push(&recorder->queue, next, rec); - cond_signal(recorder->queue_cond); + sc_cond_signal(&recorder->queue_cond); - mutex_unlock(recorder->mutex); + sc_mutex_unlock(&recorder->mutex); return true; } + +static bool +recorder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { + struct recorder *recorder = DOWNCAST(sink); + return recorder_open(recorder, codec); +} + +static void +recorder_packet_sink_close(struct sc_packet_sink *sink) { + struct recorder *recorder = DOWNCAST(sink); + recorder_close(recorder); +} + +static bool +recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { + struct recorder *recorder = DOWNCAST(sink); + return recorder_push(recorder, packet); +} + +bool +recorder_init(struct recorder *recorder, + const char *filename, + enum sc_record_format format, + struct size declared_frame_size) { + recorder->filename = strdup(filename); + if (!recorder->filename) { + LOGE("Could not strdup filename"); + return false; + } + + recorder->format = format; + recorder->declared_frame_size = declared_frame_size; + + static const struct sc_packet_sink_ops ops = { + .open = recorder_packet_sink_open, + .close = recorder_packet_sink_close, + .push = recorder_packet_sink_push, + }; + + recorder->packet_sink.ops = &ops; + + return true; +} + +void +recorder_destroy(struct recorder *recorder) { + free(recorder->filename); +} diff --git a/app/src/recorder.h b/app/src/recorder.h index 4f5d526c..0c376cd1 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -1,39 +1,37 @@ #ifndef RECORDER_H #define RECORDER_H +#include "common.h" + #include #include -#include -#include -#include "config.h" -#include "common.h" +#include "coords.h" +#include "scrcpy.h" +#include "trait/packet_sink.h" #include "util/queue.h" - -enum recorder_format { - RECORDER_FORMAT_AUTO, - RECORDER_FORMAT_MP4, - RECORDER_FORMAT_MKV, -}; +#include "util/thread.h" struct record_packet { - AVPacket packet; + AVPacket *packet; struct record_packet *next; }; struct recorder_queue QUEUE(struct record_packet); struct recorder { + struct sc_packet_sink packet_sink; // packet sink trait + char *filename; - enum recorder_format format; + enum sc_record_format format; AVFormatContext *ctx; struct size declared_frame_size; bool header_written; - SDL_Thread *thread; - SDL_mutex *mutex; - SDL_cond *queue_cond; - bool stopped; // set on recorder_stop() by the stream reader + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; + bool stopped; // set on recorder_close() bool failed; // set on packet write failure struct recorder_queue queue; @@ -46,27 +44,9 @@ struct recorder { bool recorder_init(struct recorder *recorder, const char *filename, - enum recorder_format format, struct size declared_frame_size); + enum sc_record_format format, struct size declared_frame_size); void recorder_destroy(struct recorder *recorder); -bool -recorder_open(struct recorder *recorder, const AVCodec *input_codec); - -void -recorder_close(struct recorder *recorder); - -bool -recorder_start(struct recorder *recorder); - -void -recorder_stop(struct recorder *recorder); - -void -recorder_join(struct recorder *recorder); - -bool -recorder_push(struct recorder *recorder, const AVPacket *packet); - #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 17be1ed4..4dcb412f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -7,47 +7,58 @@ #include #include -#include "config.h" -#include "command.h" -#include "common.h" -#include "compat.h" +#ifdef _WIN32 +// not needed here, but winsock2.h must never be included AFTER windows.h +# include +# include +#endif + #include "controller.h" #include "decoder.h" -#include "device.h" #include "events.h" #include "file_handler.h" -#include "fps_counter.h" #include "input_manager.h" #include "recorder.h" #include "screen.h" #include "server.h" #include "stream.h" #include "tiny_xpm.h" -#include "video_buffer.h" -#include "util/lock.h" #include "util/log.h" #include "util/net.h" +#ifdef HAVE_V4L2 +# include "v4l2_sink.h" +#endif -static struct server server = SERVER_INITIALIZER; -static struct screen screen = SCREEN_INITIALIZER; -static struct fps_counter fps_counter; -static struct video_buffer video_buffer; -static struct stream stream; -static struct decoder decoder; -static struct recorder recorder; -static struct controller controller; -static struct file_handler file_handler; - -static struct input_manager input_manager = { - .controller = &controller, - .video_buffer = &video_buffer, - .screen = &screen, - .prefer_text = false, // initialized later +struct scrcpy { + struct server server; + struct screen screen; + struct stream stream; + struct decoder decoder; + struct recorder recorder; +#ifdef HAVE_V4L2 + struct sc_v4l2_sink v4l2_sink; +#endif + struct controller controller; + struct file_handler file_handler; + struct input_manager input_manager; }; +#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); + return TRUE; + } + return FALSE; +} +#endif // _WIN32 + // init SDL and set appropriate hints static bool -sdl_init_and_configure(bool display) { +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()); @@ -56,13 +67,25 @@ sdl_init_and_configure(bool display) { 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; } - // Use the best available scale quality - if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) { - LOGW("Could not enable bilinear filtering"); + if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { + LOGW("Could not set render driver"); + } + + // Linear filtering + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + LOGW("Could not enable linear filtering"); } #ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH @@ -84,35 +107,17 @@ sdl_init_and_configure(bool display) { LOGW("Could not disable minimize on focus loss"); } - // Do not disable the screensaver when scrcpy is running - SDL_EnableScreenSaver(); + if (disable_screensaver) { + LOGD("Screensaver disabled"); + SDL_DisableScreenSaver(); + } else { + LOGD("Screensaver enabled"); + SDL_EnableScreenSaver(); + } return true; } - -#if defined(__APPLE__) || defined(__WINDOWS__) -# define CONTINUOUS_RESIZING_WORKAROUND -#endif - -#ifdef CONTINUOUS_RESIZING_WORKAROUND -// On Windows and MacOS, resizing blocks the event loop, so resizing events are -// not triggered. As a workaround, handle them in an event handler. -// -// -// -static int -event_watcher(void *data, SDL_Event *event) { - (void) data; - if (event->type == SDL_WINDOWEVENT - && event->window.event == SDL_WINDOWEVENT_RESIZED) { - // called from another thread, not very safe, but it's a workaround! - screen_render(&screen); - } - return 0; -} -#endif - static bool is_apk(const char *file) { const char *ext = strrchr(file, '.'); @@ -126,7 +131,8 @@ enum event_result { }; static enum event_result -handle_event(SDL_Event *event, bool control) { +handle_event(struct scrcpy *s, const struct scrcpy_options *options, + SDL_Event *event) { switch (event->type) { case EVENT_STREAM_STOPPED: LOGD("Video stream stopped"); @@ -134,83 +140,45 @@ handle_event(SDL_Event *event, bool control) { case SDL_QUIT: LOGD("User requested to quit"); return EVENT_RESULT_STOPPED_BY_USER; - case EVENT_NEW_FRAME: - if (!screen.has_frame) { - screen.has_frame = true; - // this is the very first frame, show the window - screen_show_window(&screen); - } - if (!screen_update_frame(&screen, &video_buffer)) { - return EVENT_RESULT_CONTINUE; - } - break; - case SDL_WINDOWEVENT: - screen_handle_window_event(&screen, &event->window); - break; - case SDL_TEXTINPUT: - if (!control) { - break; - } - input_manager_process_text_input(&input_manager, &event->text); - break; - case SDL_KEYDOWN: - case SDL_KEYUP: - // some key events do not interact with the device, so process the - // event even if control is disabled - input_manager_process_key(&input_manager, &event->key, control); - break; - case SDL_MOUSEMOTION: - if (!control) { - break; - } - input_manager_process_mouse_motion(&input_manager, &event->motion); - break; - case SDL_MOUSEWHEEL: - if (!control) { - break; - } - input_manager_process_mouse_wheel(&input_manager, &event->wheel); - break; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - // some mouse events do not interact with the device, so process - // the event even if control is disabled - input_manager_process_mouse_button(&input_manager, &event->button, - control); - break; - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: - input_manager_process_touch(&input_manager, &event->tfinger); - break; case SDL_DROPFILE: { - if (!control) { + if (!options->control) { break; } + char *file = strdup(event->drop.file); + SDL_free(event->drop.file); + if (!file) { + LOGW("Could not strdup drop filename\n"); + break; + } + file_handler_action_t action; - if (is_apk(event->drop.file)) { + if (is_apk(file)) { action = ACTION_INSTALL_APK; } else { action = ACTION_PUSH_FILE; } - file_handler_request(&file_handler, action, event->drop.file); - break; + file_handler_request(&s->file_handler, action, file); + goto end; } } + + bool consumed = screen_handle_event(&s->screen, event); + if (consumed) { + goto end; + } + + consumed = input_manager_handle_event(&s->input_manager, event); + (void) consumed; + +end: return EVENT_RESULT_CONTINUE; } static bool -event_loop(bool display, bool control) { - (void) display; -#ifdef CONTINUOUS_RESIZING_WORKAROUND - if (display) { - SDL_AddEventWatch(event_watcher, NULL); - } -#endif +event_loop(struct scrcpy *s, const struct scrcpy_options *options) { SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(&event, control); + enum event_result result = handle_event(s, options, &event); switch (result) { case EVENT_RESULT_STOPPED_BY_USER: return true; @@ -224,21 +192,6 @@ event_loop(bool display, bool control) { return false; } -static process_t -set_show_touches_enabled(const char *serial, bool enabled) { - const char *value = enabled ? "1" : "0"; - const char *const adb_cmd[] = { - "shell", "settings", "put", "system", "show_touches", value - }; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); -} - -static void -wait_show_touches(process_t process) { - // reap the process, ignore the result - process_check_success(process, "show_touches"); -} - static SDL_LogPriority sdl_priority_from_av_level(int level) { switch (level) { @@ -263,125 +216,141 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { if (priority == 0) { return; } - char *local_fmt = SDL_malloc(strlen(fmt) + 10); + + size_t fmt_len = strlen(fmt); + char *local_fmt = malloc(fmt_len + 10); if (!local_fmt) { LOGC("Could not allocate string"); return; } - // strcpy is safe here, the destination is large enough - strcpy(local_fmt, "[FFmpeg] "); - strcpy(local_fmt + 9, fmt); + memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0' + memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0' SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); - SDL_free(local_fmt); + free(local_fmt); +} + +static void +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); } bool scrcpy(const struct scrcpy_options *options) { - bool record = !!options->record_filename; - struct server_params params = { - .crop = options->crop, - .local_port = options->port, - .max_size = options->max_size, - .bit_rate = options->bit_rate, - .max_fps = options->max_fps, - .control = options->control, - }; - if (!server_start(&server, options->serial, ¶ms)) { - return false; - } + static struct scrcpy scrcpy; + struct scrcpy *s = &scrcpy; - process_t proc_show_touches = PROCESS_NONE; - bool show_touches_waited; - if (options->show_touches) { - LOGI("Enable show_touches"); - proc_show_touches = set_show_touches_enabled(options->serial, true); - show_touches_waited = false; + if (!server_init(&s->server)) { + return false; } bool ret = false; - bool fps_counter_initialized = false; - bool video_buffer_initialized = false; + bool server_started = false; bool file_handler_initialized = false; bool recorder_initialized = false; +#ifdef HAVE_V4L2 + bool v4l2_sink_initialized = false; +#endif bool stream_started = false; bool controller_initialized = false; bool controller_started = false; + bool screen_initialized = false; - if (!sdl_init_and_configure(options->display)) { + bool record = !!options->record_filename; + struct server_params params = { + .serial = options->serial, + .log_level = options->log_level, + .crop = options->crop, + .port_range = options->port_range, + .max_size = options->max_size, + .bit_rate = options->bit_rate, + .max_fps = options->max_fps, + .lock_video_orientation = options->lock_video_orientation, + .control = options->control, + .display_id = options->display_id, + .show_touches = options->show_touches, + .stay_awake = options->stay_awake, + .codec_options = options->codec_options, + .encoder_name = options->encoder_name, + .force_adb_forward = options->force_adb_forward, + .power_off_on_close = options->power_off_on_close, + }; + if (!server_start(&s->server, ¶ms)) { goto end; } - if (!server_connect_to(&server)) { + server_started = true; + + if (!sdl_init_and_configure(options->display, options->render_driver, + options->disable_screensaver)) { goto end; } char device_name[DEVICE_NAME_FIELD_LENGTH]; struct size frame_size; - // screenrecord does not send frames when the screen content does not - // change therefore, we transmit the screen size before the video stream, - // to be able to init the window immediately - if (!device_read_info(server.video_socket, device_name, &frame_size)) { + if (!server_connect_to(&s->server, device_name, &frame_size)) { goto end; } + if (options->display && options->control) { + if (!file_handler_init(&s->file_handler, s->server.serial, + options->push_target)) { + goto end; + } + file_handler_initialized = true; + } + struct decoder *dec = NULL; - if (options->display) { - if (!fps_counter_init(&fps_counter)) { - goto end; - } - fps_counter_initialized = true; - - if (!video_buffer_init(&video_buffer, &fps_counter, - options->render_expired_frames)) { - goto end; - } - video_buffer_initialized = true; - - if (options->control) { - if (!file_handler_init(&file_handler, server.serial, - options->push_target)) { - goto end; - } - file_handler_initialized = true; - } - - decoder_init(&decoder, &video_buffer); - dec = &decoder; + bool needs_decoder = options->display; +#ifdef HAVE_V4L2 + needs_decoder |= !!options->v4l2_device; +#endif + if (needs_decoder) { + decoder_init(&s->decoder); + dec = &s->decoder; } struct recorder *rec = NULL; if (record) { - if (!recorder_init(&recorder, + if (!recorder_init(&s->recorder, options->record_filename, options->record_format, frame_size)) { goto end; } - rec = &recorder; + rec = &s->recorder; recorder_initialized = true; } av_log_set_callback(av_log_callback); - stream_init(&stream, server.video_socket, dec, rec); + const struct stream_callbacks stream_cbs = { + .on_eos = stream_on_eos, + }; + stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL); - // now we consumed the header values, the socket receives the video stream - // start the stream - if (!stream_start(&stream)) { - goto end; + if (dec) { + stream_add_sink(&s->stream, &dec->packet_sink); + } + + if (rec) { + stream_add_sink(&s->stream, &rec->packet_sink); } - stream_started = true; if (options->display) { if (options->control) { - if (!controller_init(&controller, server.control_socket)) { + if (!controller_init(&s->controller, s->server.control_socket)) { goto end; } controller_initialized = true; - if (!controller_start(&controller)) { + if (!controller_start(&s->controller)) { goto end; } controller_started = true; @@ -390,101 +359,120 @@ scrcpy(const struct scrcpy_options *options) { const char *window_title = options->window_title ? options->window_title : device_name; - if (!screen_init_rendering(&screen, window_title, frame_size, - options->always_on_top, options->window_x, - options->window_y, options->window_width, - options->window_height, - options->window_borderless)) { + 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)) { goto end; } + screen_initialized = true; + + decoder_add_sink(&s->decoder, &s->screen.frame_sink); if (options->turn_screen_off) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; - if (!controller_push_msg(&controller, &msg)) { + if (!controller_push_msg(&s->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } + } - if (options->fullscreen) { - screen_switch_fullscreen(&screen); +#ifdef HAVE_V4L2 + if (options->v4l2_device) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size)) { + goto end; } + + decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); + + v4l2_sink_initialized = true; } +#endif - if (options->show_touches) { - wait_show_touches(proc_show_touches); - show_touches_waited = true; + // now we consumed the header values, the socket receives the video stream + // start the stream + if (!stream_start(&s->stream)) { + goto end; } + stream_started = true; - input_manager.prefer_text = options->prefer_text; + input_manager_init(&s->input_manager, &s->controller, &s->screen, options); - ret = event_loop(options->display, options->control); + ret = event_loop(s, options); LOGD("quit..."); - screen_destroy(&screen); + // Close the window immediately on closing, because screen_destroy() may + // only be called once the stream thread is joined (it may take time) + screen_hide_window(&s->screen); end: - // stop stream and controller so that they don't continue once their socket - // is shutdown - if (stream_started) { - stream_stop(&stream); - } + // The stream is not stopped explicitly, because it will stop by itself on + // end-of-stream if (controller_started) { - controller_stop(&controller); + controller_stop(&s->controller); } if (file_handler_initialized) { - file_handler_stop(&file_handler); + file_handler_stop(&s->file_handler); } - if (fps_counter_initialized) { - fps_counter_interrupt(&fps_counter); + if (screen_initialized) { + screen_interrupt(&s->screen); } - // shutdown the sockets and kill the server - server_stop(&server); + if (server_started) { + // shutdown the sockets and kill the server + server_stop(&s->server); + } // now that the sockets are shutdown, the stream and controller are // interrupted, we can join them if (stream_started) { - stream_join(&stream); + stream_join(&s->stream); } + +#ifdef HAVE_V4L2 + if (v4l2_sink_initialized) { + sc_v4l2_sink_destroy(&s->v4l2_sink); + } +#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) { + screen_join(&s->screen); + screen_destroy(&s->screen); + } + if (controller_started) { - controller_join(&controller); + controller_join(&s->controller); } if (controller_initialized) { - controller_destroy(&controller); + controller_destroy(&s->controller); } if (recorder_initialized) { - recorder_destroy(&recorder); + recorder_destroy(&s->recorder); } if (file_handler_initialized) { - file_handler_join(&file_handler); - file_handler_destroy(&file_handler); + file_handler_join(&s->file_handler); + file_handler_destroy(&s->file_handler); } - if (video_buffer_initialized) { - video_buffer_destroy(&video_buffer); - } - - if (fps_counter_initialized) { - fps_counter_join(&fps_counter); - fps_counter_destroy(&fps_counter); - } - - if (options->show_touches) { - if (!show_touches_waited) { - // wait the process which enabled "show touches" - wait_show_touches(proc_show_touches); - } - LOGI("Disable show_touches"); - proc_show_touches = set_show_touches_enabled(options->serial, false); - wait_show_touches(proc_show_touches); - } - - server_destroy(&server); + server_destroy(&s->server); return ret; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 75de8717..0a2deb71 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -1,12 +1,58 @@ #ifndef SCRCPY_H #define SCRCPY_H +#include "common.h" + #include +#include #include -#include "config.h" -#include "input_manager.h" -#include "recorder.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, +}; + +#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; @@ -14,24 +60,40 @@ struct scrcpy_options { const char *record_filename; const char *window_title; const char *push_target; - enum recorder_format record_format; - uint16_t port; + 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; - int16_t window_x; - int16_t window_y; + 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 render_expired_frames; 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 { \ @@ -40,24 +102,46 @@ struct scrcpy_options { .record_filename = NULL, \ .window_title = NULL, \ .push_target = NULL, \ - .record_format = RECORDER_FORMAT_AUTO, \ - .port = DEFAULT_LOCAL_PORT, \ - .max_size = DEFAULT_MAX_SIZE, \ + .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, \ - .window_x = -1, \ - .window_y = -1, \ + .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, \ - .render_expired_frames = 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, \ } bool diff --git a/app/src/screen.c b/app/src/screen.c index beb10754..99327b3b 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -4,23 +4,36 @@ #include #include -#include "config.h" -#include "common.h" -#include "compat.h" +#include "events.h" #include "icon.xpm" +#include "scrcpy.h" #include "tiny_xpm.h" #include "video_buffer.h" -#include "util/lock.h" #include "util/log.h" #define DISPLAY_MARGINS 96 +#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; + if (rotation & 1) { + rotated_size.width = size.height; + rotated_size.height = size.width; + } else { + rotated_size.width = size.width; + rotated_size.height = size.height; + } + return rotated_size; +} + // get the window size in a struct size static struct size -get_window_size(SDL_Window *window) { +get_window_size(const struct screen *screen) { int width; int height; - SDL_GetWindowSize(window, &width, &height); + SDL_GetWindowSize(screen->window, &width, &height); struct size size; size.width = width; @@ -28,31 +41,24 @@ get_window_size(SDL_Window *window) { return size; } -// get the windowed window size -static struct size -get_windowed_window_size(const struct screen *screen) { - if (screen->fullscreen || screen->maximized) { - return screen->windowed_window_size; - } - return get_window_size(screen->window); -} +static struct point +get_window_position(const struct screen *screen) { + int x; + int y; + SDL_GetWindowPosition(screen->window, &x, &y); -// apply the windowed window size if fullscreen and maximized are disabled -static void -apply_windowed_size(struct screen *screen) { - if (!screen->fullscreen && !screen->maximized) { - SDL_SetWindowSize(screen->window, screen->windowed_window_size.width, - screen->windowed_window_size.height); - } + struct point point; + point.x = x; + point.y = y; + return point; } // set the window size to be applied when fullscreen is disabled static void set_window_size(struct screen *screen, struct size new_size) { - // setting the window size during fullscreen is implementation defined, - // so apply the resize only after fullscreen is disabled - screen->windowed_window_size = new_size; - apply_windowed_size(screen); + assert(!screen->fullscreen); + assert(!screen->maximized); + SDL_SetWindowSize(screen->window, new_size.width, new_size.height); } // get the preferred display bounds (i.e. the screen bounds with some margins) @@ -74,107 +80,257 @@ get_preferred_display_bounds(struct size *bounds) { return true; } +static bool +is_optimal_size(struct size current_size, struct size content_size) { + // The size is optimal if we can recompute one dimension of the current + // size from the other + return current_size.height == current_size.width * content_size.height + / content_size.width + || current_size.width == current_size.height * content_size.width + / content_size.height; +} + // return the optimal size of the window, with the following constraints: // - it attempts to keep at least one dimension of the current_size (i.e. it // 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 frame_size) { - if (frame_size.width == 0 || frame_size.height == 0) { +get_optimal_size(struct size current_size, struct size content_size) { + if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } - struct size display_size; - // 32 bits because we need to multiply two 16 bits values - uint32_t w; - uint32_t h; + struct size window_size; + struct size display_size; if (!get_preferred_display_bounds(&display_size)) { // could not get display bounds, do not constraint the size - w = current_size.width; - h = current_size.height; + window_size.width = current_size.width; + window_size.height = current_size.height; } else { - w = MIN(current_size.width, display_size.width); - h = MIN(current_size.height, display_size.height); + window_size.width = MIN(current_size.width, display_size.width); + window_size.height = MIN(current_size.height, display_size.height); } - bool keep_width = frame_size.width * h > frame_size.height * w; + if (is_optimal_size(window_size, content_size)) { + return window_size; + } + + bool keep_width = content_size.width * window_size.height + > content_size.height * window_size.width; if (keep_width) { // remove black borders on top and bottom - h = frame_size.height * w / frame_size.width; + window_size.height = content_size.height * window_size.width + / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) - w = frame_size.width * h / frame_size.height; + window_size.width = content_size.width * window_size.height + / content_size.height; } - // w and h must fit into 16 bits - assert(w < 0x10000 && h < 0x10000); - return (struct size) {w, h}; -} - -// same as get_optimal_size(), but read the current size from the window -static inline struct size -get_optimal_window_size(const struct screen *screen, struct size frame_size) { - struct size windowed_size = get_windowed_window_size(screen); - return get_optimal_size(windowed_size, frame_size); + return window_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 frame_size, uint16_t req_width, +get_initial_optimal_size(struct size content_size, uint16_t req_width, uint16_t req_height) { struct size window_size; if (!req_width && !req_height) { - window_size = get_optimal_size(frame_size, frame_size); + window_size = get_optimal_size(content_size, content_size); } else { if (req_width) { window_size.width = req_width; } else { // compute from the requested height - window_size.width = (uint32_t) req_height * frame_size.width - / frame_size.height; + window_size.width = (uint32_t) req_height * content_size.width + / content_size.height; } if (req_height) { window_size.height = req_height; } else { // compute from the requested width - window_size.height = (uint32_t) req_width * frame_size.height - / frame_size.width; + window_size.height = (uint32_t) req_width * content_size.height + / content_size.width; } } return window_size; } -void -screen_init(struct screen *screen) { - *screen = (struct screen) SCREEN_INITIALIZER; +static void +screen_update_content_rect(struct screen *screen) { + int dw; + int dh; + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); + + struct size content_size = screen->content_size; + // The drawable size is the window size * the HiDPI scale + struct size drawable_size = {dw, dh}; + + SDL_Rect *rect = &screen->rect; + + if (is_optimal_size(drawable_size, content_size)) { + rect->x = 0; + rect->y = 0; + rect->w = drawable_size.width; + rect->h = drawable_size.height; + return; + } + + bool keep_width = content_size.width * drawable_size.height + > content_size.height * drawable_size.width; + if (keep_width) { + rect->x = 0; + rect->w = drawable_size.width; + rect->h = drawable_size.width * content_size.height + / content_size.width; + rect->y = (drawable_size.height - rect->h) / 2; + } else { + rect->y = 0; + rect->h = drawable_size.height; + rect->w = drawable_size.height * content_size.width + / content_size.height; + rect->x = (drawable_size.width - rect->w) / 2; + } } static inline SDL_Texture * -create_texture(SDL_Renderer *renderer, struct size frame_size) { - return SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, - SDL_TEXTUREACCESS_STREAMING, - frame_size.width, frame_size.height); +create_texture(struct screen *screen) { + SDL_Renderer *renderer = screen->renderer; + struct size size = screen->frame_size; + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + size.width, size.height); + if (!texture) { + return NULL; + } + + if (screen->mipmaps) { + struct sc_opengl *gl = &screen->gl; + + SDL_GL_BindTexture(texture, NULL, NULL); + + // Enable trilinear filtering for downscaling + gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f); + + SDL_GL_UnbindTexture(texture); + } + + return texture; +} + +#if defined(__APPLE__) || defined(__WINDOWS__) +# define CONTINUOUS_RESIZING_WORKAROUND +#endif + +#ifdef CONTINUOUS_RESIZING_WORKAROUND +// On Windows and MacOS, resizing blocks the event loop, so resizing events are +// not triggered. As a workaround, handle them in an event handler. +// +// +// +static int +event_watcher(void *data, SDL_Event *event) { + struct screen *screen = data; + if (event->type == SDL_WINDOWEVENT + && event->window.event == SDL_WINDOWEVENT_RESIZED) { + // In practice, it seems to always be called from the same thread in + // that specific case. Anyway, it's just a workaround. + screen_render(screen, true); + } + return 0; +} +#endif + +static bool +screen_frame_sink_open(struct sc_frame_sink *sink) { + struct screen *screen = DOWNCAST(sink); + (void) screen; +#ifndef NDEBUG + screen->open = true; +#endif + + // nothing to do, the screen is already open on the main thread + return true; +} + +static void +screen_frame_sink_close(struct sc_frame_sink *sink) { + struct screen *screen = DOWNCAST(sink); + (void) screen; +#ifndef NDEBUG + screen->open = false; +#endif + + // nothing to do, the screen lifecycle is not managed by the frame producer +} + +static bool +screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { + struct screen *screen = DOWNCAST(sink); + + bool previous_frame_skipped; + bool ok = video_buffer_push(&screen->vb, frame, &previous_frame_skipped); + if (!ok) { + return false; + } + + if (previous_frame_skipped) { + fps_counter_add_skipped_frame(&screen->fps_counter); + // The EVENT_NEW_FRAME triggered for the previous frame will consume + // this new frame instead + } else { + static SDL_Event new_frame_event = { + .type = EVENT_NEW_FRAME, + }; + + // Post the event on the UI thread + SDL_PushEvent(&new_frame_event); + } + + return true; } bool -screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top, - int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless) { - screen->frame_size = frame_size; +screen_init(struct screen *screen, const struct screen_params *params) { + screen->resize_pending = false; + screen->has_frame = false; + screen->fullscreen = false; + screen->maximized = false; - struct size window_size = - get_initial_optimal_size(frame_size, window_width, window_height); - uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; -#ifdef HIDPI_SUPPORT - window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; -#endif - if (always_on_top) { + bool ok = video_buffer_init(&screen->vb); + if (!ok) { + LOGE("Could not initialize video buffer"); + return false; + } + + if (!fps_counter_init(&screen->fps_counter)) { + LOGE("Could not initialize FPS counter"); + goto error_destroy_video_buffer; + } + + screen->frame_size = params->frame_size; + screen->rotation = params->rotation; + if (screen->rotation) { + LOGI("Initial display rotation set to %u", screen->rotation); + } + struct 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); + uint32_t window_flags = SDL_WINDOW_HIDDEN + | SDL_WINDOW_RESIZABLE + | SDL_WINDOW_ALLOW_HIGHDPI; + if (params->always_on_top) { #ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; #else @@ -182,33 +338,60 @@ screen_init_rendering(struct screen *screen, const char *window_title, "(compile with SDL >= 2.0.5 to enable it)"); #endif } - if (window_borderless) { + if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } - int x = window_x != -1 ? window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = window_y != -1 ? window_y : (int) SDL_WINDOWPOS_UNDEFINED; - screen->window = SDL_CreateWindow(window_title, x, y, + int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED + ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; + int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED + ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; + screen->window = SDL_CreateWindow(params->window_title, x, y, window_size.width, window_size.height, window_flags); if (!screen->window) { LOGC("Could not create window: %s", SDL_GetError()); - return false; + goto error_destroy_fps_counter; } screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED); if (!screen->renderer) { LOGC("Could not create renderer: %s", SDL_GetError()); - screen_destroy(screen); - return false; + goto error_destroy_window; } - if (SDL_RenderSetLogicalSize(screen->renderer, frame_size.width, - frame_size.height)) { - LOGE("Could not set renderer logical size: %s", SDL_GetError()); - screen_destroy(screen); - return false; + SDL_RendererInfo renderer_info; + int r = SDL_GetRendererInfo(screen->renderer, &renderer_info); + const char *renderer_name = r ? NULL : renderer_info.name; + LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); + + screen->mipmaps = false; + + // starts with "opengl" + bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (use_opengl) { + struct sc_opengl *gl = &screen->gl; + sc_opengl_init(gl); + + LOGI("OpenGL version: %s", gl->version); + + if (params->mipmaps) { + bool supports_mipmaps = + sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ + 2, 0 /* OpenGL ES 2.0+ */); + if (supports_mipmaps) { + LOGI("Trilinear filtering enabled"); + screen->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required)"); + } + } else { + LOGI("Trilinear filtering disabled"); + } + } else if (params->mipmaps) { + LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); } SDL_Surface *icon = read_xpm(icon_xpm); @@ -219,36 +402,151 @@ screen_init_rendering(struct screen *screen, const char *window_title, LOGW("Could not load icon"); } - LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, - frame_size.height); - screen->texture = create_texture(screen->renderer, frame_size); + LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width, + params->frame_size.height); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); - screen_destroy(screen); - return false; + goto error_destroy_renderer; } - screen->windowed_window_size = window_size; + screen->frame = av_frame_alloc(); + if (!screen->frame) { + LOGC("Could not create screen frame"); + goto error_destroy_texture; + } + + // Reset the window size to trigger a SIZE_CHANGED event, to workaround + // HiDPI issues with some SDL renderers when several displays having + // different HiDPI scaling are connected + SDL_SetWindowSize(screen->window, window_size.width, window_size.height); + + screen_update_content_rect(screen); + + if (params->fullscreen) { + screen_switch_fullscreen(screen); + } + +#ifdef CONTINUOUS_RESIZING_WORKAROUND + SDL_AddEventWatch(event_watcher, screen); +#endif + + static const struct sc_frame_sink_ops ops = { + .open = screen_frame_sink_open, + .close = screen_frame_sink_close, + .push = screen_frame_sink_push, + }; + + screen->frame_sink.ops = &ops; + +#ifndef NDEBUG + screen->open = false; +#endif return true; + +error_destroy_texture: + SDL_DestroyTexture(screen->texture); +error_destroy_renderer: + SDL_DestroyRenderer(screen->renderer); +error_destroy_window: + SDL_DestroyWindow(screen->window); +error_destroy_fps_counter: + fps_counter_destroy(&screen->fps_counter); +error_destroy_video_buffer: + video_buffer_destroy(&screen->vb); + + return false; } -void +static void screen_show_window(struct screen *screen) { SDL_ShowWindow(screen->window); } +void +screen_hide_window(struct screen *screen) { + SDL_HideWindow(screen->window); +} + +void +screen_interrupt(struct screen *screen) { + fps_counter_interrupt(&screen->fps_counter); +} + +void +screen_join(struct screen *screen) { + fps_counter_join(&screen->fps_counter); +} + void screen_destroy(struct screen *screen) { - if (screen->texture) { - SDL_DestroyTexture(screen->texture); +#ifndef NDEBUG + assert(!screen->open); +#endif + av_frame_free(&screen->frame); + SDL_DestroyTexture(screen->texture); + SDL_DestroyRenderer(screen->renderer); + SDL_DestroyWindow(screen->window); + fps_counter_destroy(&screen->fps_counter); + 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 = { + .width = (uint32_t) window_size.width * new_content_size.width + / old_content_size.width, + .height = (uint32_t) window_size.height * new_content_size.height + / old_content_size.height, + }; + target_size = get_optimal_size(target_size, new_content_size); + set_window_size(screen, target_size); +} + +static void +set_content_size(struct screen *screen, struct size new_content_size) { + if (!screen->fullscreen && !screen->maximized) { + resize_for_content(screen, screen->content_size, new_content_size); + } else if (!screen->resize_pending) { + // Store the windowed size to be able to compute the optimal size once + // fullscreen and maximized are disabled + screen->windowed_content_size = screen->content_size; + screen->resize_pending = true; } - if (screen->renderer) { - SDL_DestroyRenderer(screen->renderer); + + screen->content_size = new_content_size; +} + +static void +apply_pending_resize(struct screen *screen) { + assert(!screen->fullscreen); + assert(!screen->maximized); + if (screen->resize_pending) { + resize_for_content(screen, screen->windowed_content_size, + screen->content_size); + screen->resize_pending = false; } - if (screen->window) { - SDL_DestroyWindow(screen->window); +} + +void +screen_set_rotation(struct screen *screen, unsigned rotation) { + assert(rotation < 4); + if (rotation == screen->rotation) { + return; } + + struct size new_content_size = + get_rotated_size(screen->frame_size, rotation); + + set_content_size(screen, new_content_size); + + screen->rotation = rotation; + LOGI("Display rotation set to %u", rotation); + + screen_render(screen, true); } // recreate the texture and resize the window if the frame size has changed @@ -256,30 +554,20 @@ static bool prepare_for_frame(struct screen *screen, struct size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { - if (SDL_RenderSetLogicalSize(screen->renderer, new_frame_size.width, - new_frame_size.height)) { - LOGE("Could not set renderer logical size: %s", SDL_GetError()); - return false; - } - // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); - struct size windowed_size = get_windowed_window_size(screen); - struct size target_size = { - (uint32_t) windowed_size.width * new_frame_size.width - / screen->frame_size.width, - (uint32_t) windowed_size.height * new_frame_size.height - / screen->frame_size.height, - }; - target_size = get_optimal_size(target_size, new_frame_size); - set_window_size(screen, target_size); - screen->frame_size = new_frame_size; + struct size new_content_size = + get_rotated_size(new_frame_size, screen->rotation); + set_content_size(screen, new_content_size); + + screen_update_content_rect(screen); + LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); - screen->texture = create_texture(screen->renderer, new_frame_size); + screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); return false; @@ -296,28 +584,63 @@ update_texture(struct screen *screen, const AVFrame *frame) { frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]); + + if (screen->mipmaps) { + SDL_GL_BindTexture(screen->texture, NULL, NULL); + screen->gl.GenerateMipmap(GL_TEXTURE_2D); + SDL_GL_UnbindTexture(screen->texture); + } } -bool -screen_update_frame(struct screen *screen, struct video_buffer *vb) { - mutex_lock(vb->mutex); - const AVFrame *frame = video_buffer_consume_rendered_frame(vb); +static bool +screen_update_frame(struct screen *screen) { + av_frame_unref(screen->frame); + 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}; if (!prepare_for_frame(screen, new_frame_size)) { - mutex_unlock(vb->mutex); return false; } update_texture(screen, frame); - mutex_unlock(vb->mutex); - screen_render(screen); + screen_render(screen, false); return true; } void -screen_render(struct screen *screen) { +screen_render(struct screen *screen, bool update_content_rect) { + if (update_content_rect) { + screen_update_content_rect(screen); + } + SDL_RenderClear(screen->renderer); - SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + 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); } @@ -330,27 +653,36 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - apply_windowed_size(screen); + if (!screen->fullscreen && !screen->maximized) { + apply_pending_resize(screen); + } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); - screen_render(screen); + screen_render(screen, true); } void screen_resize_to_fit(struct screen *screen) { - if (screen->fullscreen) { + if (screen->fullscreen || screen->maximized) { return; } - if (screen->maximized) { - SDL_RestoreWindow(screen->window); - screen->maximized = false; - } + struct point point = get_window_position(screen); + struct size window_size = get_window_size(screen); struct size optimal_size = - get_optimal_window_size(screen, screen->frame_size); + get_optimal_size(window_size, screen->content_size); + + // Center the window related to the device screen + assert(optimal_size.width <= window_size.width); + assert(optimal_size.height <= window_size.height); + uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2; + uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2; + SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); - LOGD("Resized to optimal size"); + SDL_SetWindowPosition(screen->window, new_x, new_y); + LOGD("Resized to optimal size: %ux%u", optimal_size.width, + optimal_size.height); } void @@ -364,49 +696,113 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - SDL_SetWindowSize(screen->window, screen->frame_size.width, - screen->frame_size.height); - LOGD("Resized to pixel-perfect"); + struct 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); +} + +bool +screen_handle_event(struct screen *screen, SDL_Event *event) { + switch (event->type) { + case EVENT_NEW_FRAME: + if (!screen->has_frame) { + screen->has_frame = true; + // this is the very first frame, show the window + screen_show_window(screen); + } + bool ok = screen_update_frame(screen); + if (!ok) { + LOGW("Frame update failed\n"); + } + return true; + case SDL_WINDOWEVENT: + if (!screen->has_frame) { + // Do nothing + return true; + } + switch (event->window.event) { + case SDL_WINDOWEVENT_EXPOSED: + screen_render(screen, true); + break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + screen_render(screen, true); + break; + case SDL_WINDOWEVENT_MAXIMIZED: + screen->maximized = true; + break; + case SDL_WINDOWEVENT_RESTORED: + if (screen->fullscreen) { + // On Windows, in maximized+fullscreen, disabling + // fullscreen mode unexpectedly triggers the "restored" + // then "maximized" events, leaving the window in a + // weird state (maximized according to the events, but + // not maximized visually). + break; + } + screen->maximized = false; + apply_pending_resize(screen); + screen_render(screen, true); + break; + } + return true; + } + + return false; +} + +struct point +screen_convert_drawable_to_frame_coords(struct screen *screen, + int32_t x, int32_t y) { + unsigned rotation = screen->rotation; + assert(rotation < 4); + + int32_t w = screen->content_size.width; + int32_t h = screen->content_size.height; + + + x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; + y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; + + // rotate + struct point result; + switch (rotation) { + case 0: + result.x = x; + result.y = y; + break; + case 1: + result.x = h - y; + result.y = x; + break; + case 2: + result.x = w - x; + result.y = h - y; + break; + default: + assert(rotation == 3); + result.x = y; + result.y = w - x; + break; + } + return result; +} + +struct point +screen_convert_window_to_frame_coords(struct screen *screen, + int32_t x, int32_t y) { + screen_hidpi_scale_coords(screen, &x, &y); + return screen_convert_drawable_to_frame_coords(screen, x, y); } void -screen_handle_window_event(struct screen *screen, - const SDL_WindowEvent *event) { - switch (event->event) { - case SDL_WINDOWEVENT_EXPOSED: - screen_render(screen); - break; - case SDL_WINDOWEVENT_SIZE_CHANGED: - if (!screen->fullscreen && !screen->maximized) { - // Backup the previous size: if we receive the MAXIMIZED event, - // then the new size must be ignored (it's the maximized size). - // We could not rely on the window flags due to race conditions - // (they could be updated asynchronously, at least on X11). - screen->windowed_window_size_backup = - screen->windowed_window_size; +screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { + // take the HiDPI scaling (dw/ww and dh/wh) into account + int ww, wh, dw, dh; + SDL_GetWindowSize(screen->window, &ww, &wh); + SDL_GL_GetDrawableSize(screen->window, &dw, &dh); - // Save the windowed size, so that it is available once the - // window is maximized or fullscreen is enabled. - screen->windowed_window_size = get_window_size(screen->window); - } - screen_render(screen); - break; - case SDL_WINDOWEVENT_MAXIMIZED: - // The backup size must be non-nul. - assert(screen->windowed_window_size_backup.width); - assert(screen->windowed_window_size_backup.height); - // Revert the last size, it was updated while screen was maximized. - screen->windowed_window_size = screen->windowed_window_size_backup; -#ifdef DEBUG - // Reset the backup to invalid values to detect unexpected usage - screen->windowed_window_size_backup.width = 0; - screen->windowed_window_size_backup.height = 0; -#endif - screen->maximized = true; - break; - case SDL_WINDOWEVENT_RESTORED: - screen->maximized = false; - apply_windowed_size(screen); - break; - } + // scale for HiDPI (64 bits for intermediate multiplications) + *x = (int64_t) *x * dw / ww; + *y = (int64_t) *y * dh / wh; } diff --git a/app/src/screen.h b/app/src/screen.h index 2346ff15..e2a43da7 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,79 +1,99 @@ #ifndef SCREEN_H #define SCREEN_H +#include "common.h" + #include #include #include -#include "config.h" -#include "common.h" - -struct video_buffer; +#include "coords.h" +#include "opengl.h" +#include "trait/frame_sink.h" +#include "video_buffer.h" struct screen { + struct sc_frame_sink frame_sink; // frame sink trait + +#ifndef NDEBUG + bool open; // track the open/close state to assert correct behavior +#endif + + struct 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; - // The window size the last time it was not maximized or fullscreen. - struct size windowed_window_size; - // Since we receive the event SIZE_CHANGED before MAXIMIZED, we must be - // able to revert the size to its non-maximized value. - struct size windowed_window_size_backup; + struct 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; + + // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) + unsigned rotation; + // rectangle of the content (excluding black borders) + struct SDL_Rect rect; bool has_frame; bool fullscreen; bool maximized; - bool no_window; + bool mipmaps; + + AVFrame *frame; }; -#define SCREEN_INITIALIZER { \ - .window = NULL, \ - .renderer = NULL, \ - .texture = NULL, \ - .frame_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .windowed_window_size = { \ - .width = 0, \ - .height = 0, \ - }, \ - .windowed_window_size_backup = { \ - .width = 0, \ - .height = 0, \ - }, \ - .has_frame = false, \ - .fullscreen = false, \ - .maximized = false, \ - .no_window = false, \ -} +struct screen_params { + const char *window_title; + struct size frame_size; + bool always_on_top; -// initialize default values -void -screen_init(struct screen *screen); + int16_t window_x; + int16_t window_y; + uint16_t window_width; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_height; // accepts SC_WINDOW_POSITION_UNDEFINED + + bool window_borderless; + + uint8_t rotation; + bool mipmaps; + + bool fullscreen; +}; // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init_rendering(struct screen *screen, const char *window_title, - struct size frame_size, bool always_on_top, - int16_t window_x, int16_t window_y, uint16_t window_width, - uint16_t window_height, bool window_borderless); +screen_init(struct screen *screen, const struct screen_params *params); -// show the window +// request to interrupt any inner thread +// must be called before screen_join() void -screen_show_window(struct screen *screen); +screen_interrupt(struct screen *screen); + +// join any inner thread +void +screen_join(struct screen *screen); // destroy window, renderer and texture (if any) void screen_destroy(struct screen *screen); -// resize if necessary and write the rendered frame into the texture -bool -screen_update_frame(struct screen *screen, struct video_buffer *vb); +// hide the window +// +// It is used to hide the window immediately on closing without waiting for +// screen_destroy() +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); +screen_render(struct screen *screen, bool update_content_rect); // switch the fullscreen mode void @@ -87,8 +107,31 @@ screen_resize_to_fit(struct screen *screen); void screen_resize_to_pixel_perfect(struct screen *screen); -// react to window events +// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) void -screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); +screen_set_rotation(struct screen *screen, unsigned rotation); + +// react to SDL events +bool +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 +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 +screen_convert_drawable_to_frame_coords(struct screen *screen, + int32_t x, int32_t y); + +// Convert coordinates from window to drawable. +// Events are expressed in window coordinates, but content is expressed in +// drawable coordinates. They are the same if HiDPI scaling is 1, but differ +// otherwise. +void +screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y); #endif diff --git a/app/src/server.c b/app/src/server.c index ff167aeb..a4cdb0c9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -6,11 +6,12 @@ #include #include #include +#include -#include "config.h" -#include "command.h" +#include "adb.h" #include "util/log.h" #include "util/net.h" +#include "util/str_util.h" #define SOCKET_NAME "scrcpy" #define SERVER_FILENAME "scrcpy-server" @@ -18,39 +19,58 @@ #define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME #define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" -static const char * +static char * get_server_path(void) { +#ifdef __WINDOWS__ + const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); +#else const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); +#endif if (server_path_env) { - LOGD("Using SCRCPY_SERVER_PATH: %s", server_path_env); // if the envvar is set, use it - return server_path_env; +#ifdef __WINDOWS__ + char *server_path = utf8_from_wide_char(server_path_env); +#else + char *server_path = strdup(server_path_env); +#endif + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } + LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); + return server_path; } #ifndef PORTABLE LOGD("Using server: " DEFAULT_SERVER_PATH); + char *server_path = strdup(DEFAULT_SERVER_PATH); + if (!server_path) { + LOGE("Could not allocate memory"); + return NULL; + } // the absolute path is hardcoded - return DEFAULT_SERVER_PATH; + 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 SERVER_FILENAME; + 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 = SDL_malloc(len); + char *server_path = malloc(len); if (!server_path) { LOGE("Could not alloc server path string, " "using " SERVER_FILENAME " from current directory"); - SDL_free(executable_path); - return SERVER_FILENAME; + free(executable_path); + return strdup(SERVER_FILENAME); } memcpy(server_path, dir, dirlen); @@ -58,7 +78,7 @@ get_server_path(void) { memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); // the final null byte has been copied with SERVER_FILENAME - SDL_free(executable_path); + free(executable_path); LOGD("Using server (portable): %s", server_path); return server_path; @@ -67,48 +87,42 @@ get_server_path(void) { static bool push_server(const char *serial) { - const char *server_path = get_server_path(); + char *server_path = get_server_path(); + if (!server_path) { + return false; + } if (!is_regular_file(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); - return process_check_success(process, "adb push"); + 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"); + 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"); + 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"); + 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"); -} - -static bool -enable_tunnel(struct server *server) { - if (enable_tunnel_reverse(server->serial, server->local_port)) { - return true; - } - - LOGW("'adb reverse' failed, fallback to 'adb forward'"); - server->tunnel_forward = true; - return enable_tunnel_forward(server->serial, server->local_port); + return process_check_success(process, "adb forward --remove", true); } static bool @@ -119,33 +133,169 @@ disable_tunnel(struct server *server) { 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); +} + +static const char * +log_level_to_server_string(enum sc_log_level level) { + switch (level) { + case SC_LOG_LEVEL_VERBOSE: + return "verbose"; + case SC_LOG_LEVEL_DEBUG: + return "debug"; + case SC_LOG_LEVEL_INFO: + return "info"; + case SC_LOG_LEVEL_WARN: + return "warn"; + case SC_LOG_LEVEL_ERROR: + return "error"; + default: + assert(!"unexpected log level"); + return "(unknown)"; + } +} + static process_t execute_server(struct server *server, const struct server_params *params) { char max_size_string[6]; char bit_rate_string[11]; char max_fps_string[6]; + char lock_video_orientation_string[5]; + char display_id_string[11]; 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(display_id_string, "%"PRIu32, params->display_id); const char *const cmd[] = { "shell", "CLASSPATH=" 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=" +# else + /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" +# endif SERVER_DEBUGGER_PORT, #endif "/", // unused "com.genymobile.scrcpy.Server", SCRCPY_VERSION, + log_level_to_server_string(params->log_level), max_size_string, bit_rate_string, max_fps_string, + lock_video_orientation_string, server->tunnel_forward ? "true" : "false", params->crop ? params->crop : "-", "true", // always send frame meta (packet boundaries + timestamp) params->control ? "true" : "false", + display_id_string, + params->show_touches ? "true" : "false", + params->stay_awake ? "true" : "false", + params->codec_options ? params->codec_options : "-", + params->encoder_name ? params->encoder_name : "-", + params->power_off_on_close ? "true" : "false", }; #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " @@ -158,14 +308,7 @@ execute_server(struct server *server, const struct server_params *params) { // Port: 5005 // Then click on "Debug" #endif - return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0])); -} - -#define IPV4_LOCALHOST 0x7F000001 - -static socket_t -listen_on_port(uint16_t port) { - return net_listen(IPV4_LOCALHOST, port, 1); + return adb_execute(server->serial, cmd, ARRAY_LEN(cmd)); } static socket_t @@ -203,81 +346,147 @@ connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { } static void -close_socket(socket_t *socket) { - assert(*socket != INVALID_SOCKET); - net_shutdown(*socket, SHUT_RDWR); - if (!net_close(*socket)) { +close_socket(socket_t socket) { + assert(socket != INVALID_SOCKET); + net_shutdown(socket, SHUT_RDWR); + if (!net_close(socket)) { LOGW("Could not close socket"); - return; } - *socket = INVALID_SOCKET; -} - -void -server_init(struct server *server) { - *server = (struct server) SERVER_INITIALIZER; } bool -server_start(struct server *server, const char *serial, - const struct server_params *params) { - server->local_port = params->local_port; +server_init(struct server *server) { + server->serial = NULL; + server->process = PROCESS_NONE; + atomic_flag_clear_explicit(&server->server_socket_closed, + memory_order_relaxed); - if (serial) { - server->serial = SDL_strdup(serial); + bool ok = sc_mutex_init(&server->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&server->process_terminated_cond); + if (!ok) { + sc_mutex_destroy(&server->mutex); + 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(serial)) { - SDL_free(server->serial); + if (!push_server(params->serial)) { + /* server->serial will be freed on server_destroy() */ return false; } - if (!enable_tunnel(server)) { - SDL_free(server->serial); + if (!enable_tunnel_any_port(server, params->port_range, + params->force_adb_forward)) { return false; } - // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to - // "adb forward", so the app socket is the client - if (!server->tunnel_forward) { - // 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(params->local_port); - if (server->server_socket == INVALID_SOCKET) { - LOGE("Could not listen on port %" PRIu16, params->local_port); - disable_tunnel(server); - SDL_free(server->serial); - return false; - } - } - // server will connect to our server socket server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - if (!server->tunnel_forward) { - close_socket(&server->server_socket); - } - disable_tunnel(server); - SDL_free(server->serial); - return false; + 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); + if (!ok) { + process_terminate(server->process); + process_wait(server->process, true); // ignore exit code + goto error; } server->tunnel_enabled = true; 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) { + 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]; + return true; } bool -server_connect_to(struct server *server) { +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) { @@ -291,7 +500,11 @@ server_connect_to(struct server *server) { } // we don't need the server socket anymore - close_socket(&server->server_socket); + 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() + } } else { uint32_t attempts = 100; uint32_t delay = 100; // ms @@ -313,37 +526,58 @@ server_connect_to(struct server *server) { disable_tunnel(server); // ignore failure server->tunnel_enabled = false; - return true; + // The sockets will be closed on stop if device_read_info() fails + return device_read_info(server->video_socket, device_name, size); } void server_stop(struct server *server) { - if (server->server_socket != INVALID_SOCKET) { - close_socket(&server->server_socket); + 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); + close_socket(server->video_socket); } if (server->control_socket != INVALID_SOCKET) { - close_socket(&server->control_socket); + close_socket(server->control_socket); } assert(server->process != PROCESS_NONE); - if (!cmd_terminate(server->process)) { - LOGW("Could not terminate server"); - } - - cmd_simple_wait(server->process, NULL); // ignore exit code - LOGD("Server terminated"); - if (server->tunnel_enabled) { // ignore failure disable_tunnel(server); } + + // Give some delay for the server to terminate properly + 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); + } + sc_mutex_unlock(&server->mutex); + + // 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. + LOGW("Killing the server..."); + process_terminate(server->process); + } + + sc_thread_join(&server->wait_server_thread, NULL); + process_close(server->process); } void server_destroy(struct server *server) { - SDL_free(server->serial); + free(server->serial); + sc_cond_destroy(&server->process_terminated_cond); + sc_mutex_destroy(&server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index 0cb1ab3a..c249b374 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,56 +1,69 @@ #ifndef SERVER_H #define SERVER_H +#include "common.h" + +#include #include #include -#include "config.h" -#include "command.h" +#include "adb.h" +#include "coords.h" +#include "scrcpy.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; + uint16_t local_port; // selected from port_range bool tunnel_enabled; bool tunnel_forward; // use "adb forward" instead of "adb reverse" }; -#define SERVER_INITIALIZER { \ - .serial = NULL, \ - .process = PROCESS_NONE, \ - .server_socket = INVALID_SOCKET, \ - .video_socket = INVALID_SOCKET, \ - .control_socket = INVALID_SOCKET, \ - .local_port = 0, \ - .tunnel_enabled = false, \ - .tunnel_forward = false, \ -} - struct server_params { + const char *serial; + enum sc_log_level log_level; const char *crop; - uint16_t local_port; + const char *codec_options; + const char *encoder_name; + struct sc_port_range port_range; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; + int8_t lock_video_orientation; bool control; + uint32_t display_id; + bool show_touches; + bool stay_awake; + bool force_adb_forward; + bool power_off_on_close; }; // init default values -void +bool server_init(struct server *server); // push, enable tunnel et start the server bool -server_start(struct server *server, const char *serial, - const struct server_params *params); +server_start(struct server *server, const struct server_params *params); +#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 bool -server_connect_to(struct server *server); +server_connect_to(struct server *server, char *device_name, struct size *size); // disconnect and kill the server process void diff --git a/app/src/stream.c b/app/src/stream.c index dd2dbd76..d1b8b9f3 100644 --- a/app/src/stream.c +++ b/app/src/stream.c @@ -3,13 +3,8 @@ #include #include #include -#include -#include -#include #include -#include "config.h" -#include "compat.h" #include "decoder.h" #include "events.h" #include "recorder.h" @@ -62,33 +57,12 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) { return true; } -static void -notify_stopped(void) { - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); -} - static bool -process_config_packet(struct stream *stream, AVPacket *packet) { - if (stream->recorder && !recorder_push(stream->recorder, packet)) { - LOGE("Could not send config packet to recorder"); - return false; - } - return true; -} - -static bool -process_frame(struct stream *stream, AVPacket *packet) { - if (stream->decoder && !decoder_push(stream->decoder, packet)) { - return false; - } - - if (stream->recorder) { - packet->dts = packet->pts; - - if (!recorder_push(stream->recorder, packet)) { - LOGE("Could not send packet to recorder"); +push_packet_to_sinks(struct stream *stream, const AVPacket *packet) { + for (unsigned i = 0; i < stream->sink_count; ++i) { + struct sc_packet_sink *sink = stream->sinks[i]; + if (!sink->ops->push(sink, packet)) { + LOGE("Could not send config packet to sink %d", i); return false; } } @@ -115,9 +89,11 @@ stream_parse(struct stream *stream, AVPacket *packet) { packet->flags |= AV_PKT_FLAG_KEY; } - bool ok = process_frame(stream, packet); + packet->dts = packet->pts; + + bool ok = push_packet_to_sinks(stream, packet); if (!ok) { - LOGE("Could not process frame"); + LOGE("Could not process packet"); return false; } @@ -128,39 +104,44 @@ static bool stream_push_packet(struct stream *stream, AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; - // A config packet must not be decoded immetiately (it contains no + // A config packet must not be decoded immediately (it contains no // frame); instead, it must be concatenated with the future data packet. - if (stream->has_pending || is_config) { + if (stream->pending || is_config) { size_t offset; - if (stream->has_pending) { - offset = stream->pending.size; - if (av_grow_packet(&stream->pending, packet->size)) { + if (stream->pending) { + offset = stream->pending->size; + if (av_grow_packet(stream->pending, packet->size)) { LOGE("Could not grow packet"); return false; } } else { offset = 0; - if (av_new_packet(&stream->pending, packet->size)) { - LOGE("Could not create packet"); + stream->pending = av_packet_alloc(); + if (!stream->pending) { + LOGE("Could not allocate packet"); + return false; + } + if (av_new_packet(stream->pending, packet->size)) { + LOGE("Could not create packet"); + av_packet_free(&stream->pending); return false; } - stream->has_pending = true; } - memcpy(stream->pending.data + offset, packet->data, packet->size); + memcpy(stream->pending->data + offset, packet->data, packet->size); if (!is_config) { // prepare the concat packet to send to the decoder - stream->pending.pts = packet->pts; - stream->pending.dts = packet->dts; - stream->pending.flags = packet->flags; - packet = &stream->pending; + stream->pending->pts = packet->pts; + stream->pending->dts = packet->dts; + stream->pending->flags = packet->flags; + packet = stream->pending; } } if (is_config) { // config packet - bool ok = process_config_packet(stream, packet); + bool ok = push_packet_to_sinks(stream, packet); if (!ok) { return false; } @@ -168,10 +149,10 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { // data packet bool ok = stream_parse(stream, packet); - if (stream->has_pending) { + if (stream->pending) { // the pending packet must be discarded (consumed or error) - stream->has_pending = false; - av_packet_unref(&stream->pending); + av_packet_unref(stream->pending); + av_packet_free(&stream->pending); } if (!ok) { @@ -181,6 +162,33 @@ stream_push_packet(struct stream *stream, AVPacket *packet) { return true; } +static void +stream_close_first_sinks(struct stream *stream, unsigned count) { + while (count) { + struct sc_packet_sink *sink = stream->sinks[--count]; + sink->ops->close(sink); + } +} + +static inline void +stream_close_sinks(struct stream *stream) { + stream_close_first_sinks(stream, stream->sink_count); +} + +static bool +stream_open_sinks(struct stream *stream, const AVCodec *codec) { + for (unsigned i = 0; i < stream->sink_count; ++i) { + struct sc_packet_sink *sink = stream->sinks[i]; + if (!sink->ops->open(sink, codec)) { + LOGE("Could not open packet sink %d", i); + stream_close_first_sinks(stream, i); + return false; + } + } + + return true; +} + static int run_stream(void *data) { struct stream *stream = data; @@ -197,43 +205,36 @@ run_stream(void *data) { goto end; } - if (stream->decoder && !decoder_open(stream->decoder, codec)) { - LOGE("Could not open decoder"); + if (!stream_open_sinks(stream, codec)) { + LOGE("Could not open stream sinks"); goto finally_free_codec_ctx; } - if (stream->recorder) { - if (!recorder_open(stream->recorder, codec)) { - LOGE("Could not open recorder"); - goto finally_close_decoder; - } - - if (!recorder_start(stream->recorder)) { - LOGE("Could not start recorder"); - goto finally_close_recorder; - } - } - stream->parser = av_parser_init(AV_CODEC_ID_H264); if (!stream->parser) { LOGE("Could not initialize parser"); - goto finally_stop_and_join_recorder; + goto finally_close_sinks; } // We must only pass complete frames to av_parser_parse2()! // It's more complicated, but this allows to reduce the latency by 1 frame! stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; + AVPacket *packet = av_packet_alloc(); + if (!packet) { + LOGE("Could not allocate packet"); + goto finally_close_parser; + } + for (;;) { - AVPacket packet; - bool ok = stream_recv_packet(stream, &packet); + bool ok = stream_recv_packet(stream, packet); if (!ok) { // end of stream break; } - ok = stream_push_packet(stream, &packet); - av_packet_unref(&packet); + ok = stream_push_packet(stream, packet); + av_packet_unref(packet); if (!ok) { // cannot process packet (error already logged) break; @@ -242,61 +243,58 @@ run_stream(void *data) { LOGD("End of frames"); - if (stream->has_pending) { - av_packet_unref(&stream->pending); + if (stream->pending) { + av_packet_unref(stream->pending); + av_packet_free(&stream->pending); } + av_packet_free(&packet); +finally_close_parser: av_parser_close(stream->parser); -finally_stop_and_join_recorder: - if (stream->recorder) { - recorder_stop(stream->recorder); - LOGI("Finishing recording..."); - recorder_join(stream->recorder); - } -finally_close_recorder: - if (stream->recorder) { - recorder_close(stream->recorder); - } -finally_close_decoder: - if (stream->decoder) { - decoder_close(stream->decoder); - } +finally_close_sinks: + stream_close_sinks(stream); finally_free_codec_ctx: avcodec_free_context(&stream->codec_ctx); end: - notify_stopped(); + stream->cbs->on_eos(stream, stream->cbs_userdata); + return 0; } void stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder) { + const struct stream_callbacks *cbs, void *cbs_userdata) { stream->socket = socket; - stream->decoder = decoder, - stream->recorder = recorder; - stream->has_pending = false; + stream->pending = NULL; + stream->sink_count = 0; + + assert(cbs && cbs->on_eos); + + stream->cbs = cbs; + stream->cbs_userdata = cbs_userdata; +} + +void +stream_add_sink(struct stream *stream, struct sc_packet_sink *sink) { + assert(stream->sink_count < STREAM_MAX_SINKS); + assert(sink); + assert(sink->ops); + stream->sinks[stream->sink_count++] = sink; } bool stream_start(struct stream *stream) { LOGD("Starting stream thread"); - stream->thread = SDL_CreateThread(run_stream, "stream", stream); - if (!stream->thread) { + bool ok = sc_thread_create(&stream->thread, run_stream, "stream", stream); + if (!ok) { LOGC("Could not start stream thread"); return false; } return true; } -void -stream_stop(struct stream *stream) { - if (stream->decoder) { - decoder_interrupt(stream->decoder); - } -} - void stream_join(struct stream *stream) { - SDL_WaitThread(stream->thread, NULL); + sc_thread_join(&stream->thread, NULL); } diff --git a/app/src/stream.h b/app/src/stream.h index f7c5e475..d7047c95 100644 --- a/app/src/stream.h +++ b/app/src/stream.h @@ -1,41 +1,49 @@ #ifndef STREAM_H #define STREAM_H +#include "common.h" + #include #include #include -#include -#include -#include "config.h" +#include "trait/packet_sink.h" #include "util/net.h" +#include "util/thread.h" -struct video_buffer; +#define STREAM_MAX_SINKS 2 struct stream { socket_t socket; - struct video_buffer *video_buffer; - SDL_Thread *thread; - struct decoder *decoder; - struct recorder *recorder; + sc_thread thread; + + struct sc_packet_sink *sinks[STREAM_MAX_SINKS]; + unsigned sink_count; + AVCodecContext *codec_ctx; AVCodecParserContext *parser; // successive packets may need to be concatenated, until a non-config // packet is available - bool has_pending; - AVPacket pending; + AVPacket *pending; + + const struct stream_callbacks *cbs; + void *cbs_userdata; +}; + +struct stream_callbacks { + void (*on_eos)(struct stream *stream, void *userdata); }; void stream_init(struct stream *stream, socket_t socket, - struct decoder *decoder, struct recorder *recorder); + const struct stream_callbacks *cbs, void *cbs_userdata); + +void +stream_add_sink(struct stream *stream, struct sc_packet_sink *sink); bool stream_start(struct stream *stream); -void -stream_stop(struct stream *stream); - void stream_join(struct stream *stream); diff --git a/app/src/sys/unix/net.c b/app/src/sys/unix/net.c deleted file mode 100644 index d67a660f..00000000 --- a/app/src/sys/unix/net.c +++ /dev/null @@ -1,21 +0,0 @@ -#include "util/net.h" - -#include - -#include "config.h" - -bool -net_init(void) { - // do nothing - return true; -} - -void -net_cleanup(void) { - // do nothing -} - -bool -net_close(socket_t socket) { - return !close(socket); -} diff --git a/app/src/sys/unix/command.c b/app/src/sys/unix/process.c similarity index 58% rename from app/src/sys/unix/command.c rename to app/src/sys/unix/process.c index fbcf2355..8683a2da 100644 --- a/app/src/sys/unix/command.c +++ b/app/src/sys/unix/process.c @@ -1,27 +1,56 @@ -// for portability -#define _POSIX_SOURCE // for kill() -#define _BSD_SOURCE // for readlink() - -// modern glibc will complain without this -#define _DEFAULT_SOURCE - -#include "command.h" - -#include "config.h" +#include "util/process.h" #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; + + 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; +} + enum process_result -cmd_execute(const char *const argv[], pid_t *pid) { +process_execute(const char *const argv[], pid_t *pid) { int fd[2]; if (pipe(fd) == -1) { @@ -83,29 +112,37 @@ end: } bool -cmd_terminate(pid_t pid) { +process_terminate(pid_t pid) { if (pid <= 0) { LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid); abort(); } - return kill(pid, SIGTERM) != -1; + return kill(pid, SIGKILL) != -1; } -bool -cmd_simple_wait(pid_t pid, int *exit_code) { - int status; +exit_code_t +process_wait(pid_t pid, bool close) { int code; - if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { + int options = WEXITED; + if (!close) { + options |= WNOWAIT; + } + + siginfo_t info; + 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 = -1; + code = NO_EXIT_CODE; } else { - code = WEXITSTATUS(status); + code = info.si_status; } - if (exit_code) { - *exit_code = code; - } - return !code; + return code; +} + +void +process_close(pid_t pid) { + process_wait(pid, true); // ignore exit code } char * @@ -119,7 +156,7 @@ get_executable_path(void) { return NULL; } buf[len] = '\0'; - return SDL_strdup(buf); + 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 @@ -127,3 +164,14 @@ get_executable_path(void) { 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/net.c b/app/src/sys/win/net.c deleted file mode 100644 index aebce7fc..00000000 --- a/app/src/sys/win/net.c +++ /dev/null @@ -1,25 +0,0 @@ -#include "util/net.h" - -#include "config.h" -#include "util/log.h" - -bool -net_init(void) { - WSADATA wsa; - int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; - if (res < 0) { - LOGC("WSAStartup failed with error %d", res); - return false; - } - return true; -} - -void -net_cleanup(void) { - WSACleanup(); -} - -bool -net_close(socket_t socket) { - return !closesocket(socket); -} diff --git a/app/src/sys/win/command.c b/app/src/sys/win/process.c similarity index 58% rename from app/src/sys/win/command.c rename to app/src/sys/win/process.c index 55edaf8f..aafd5d34 100644 --- a/app/src/sys/win/command.c +++ b/app/src/sys/win/process.c @@ -1,10 +1,14 @@ -#include "command.h" +#include "util/process.h" + +#include +#include -#include "config.h" #include "util/log.h" #include "util/str_util.h" -static int +#define CMD_MAX_LEN 8192 + +static bool build_cmd(char *cmd, size_t len, const char *const argv[]) { // Windows command-line parsing is WTF: // @@ -13,38 +17,34 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { size_t ret = xstrjoin(cmd, argv, ' ', len); if (ret >= len) { LOGE("Command too long (%" PRIsizet " chars)", len - 1); - return -1; + return false; } - return 0; + return true; } enum process_result -cmd_execute(const char *const argv[], HANDLE *handle) { +process_execute(const char *const argv[], HANDLE *handle) { STARTUPINFOW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); si.cb = sizeof(si); - char cmd[256]; - if (build_cmd(cmd, sizeof(cmd), argv)) { + char *cmd = malloc(CMD_MAX_LEN); + if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { *handle = NULL; return PROCESS_ERROR_GENERIC; } wchar_t *wide = utf8_to_wide_char(cmd); + free(cmd); if (!wide) { LOGC("Could not allocate wide char string"); return PROCESS_ERROR_GENERIC; } -#ifdef WINDOWS_NOCONSOLE - int flags = CREATE_NO_WINDOW; -#else - int flags = 0; -#endif - if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si, + if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { - SDL_free(wide); + free(wide); *handle = NULL; if (GetLastError() == ERROR_FILE_NOT_FOUND) { return PROCESS_ERROR_MISSING_BINARY; @@ -52,28 +52,35 @@ cmd_execute(const char *const argv[], HANDLE *handle) { return PROCESS_ERROR_GENERIC; } - SDL_free(wide); + free(wide); *handle = pi.hProcess; return PROCESS_SUCCESS; } bool -cmd_terminate(HANDLE handle) { - return TerminateProcess(handle, 1) && CloseHandle(handle); +process_terminate(HANDLE handle) { + return TerminateProcess(handle, 1); } -bool -cmd_simple_wait(HANDLE handle, DWORD *exit_code) { +exit_code_t +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 = -1; // max value, it's unsigned + code = NO_EXIT_CODE; // max value, it's unsigned } - if (exit_code) { - *exit_code = code; + if (close) { + CloseHandle(handle); } - return !code; + return code; +} + +void +process_close(HANDLE handle) { + bool closed = CloseHandle(handle); + assert(closed); + (void) closed; } char * @@ -90,3 +97,22 @@ get_executable_path(void) { buf[len] = '\0'; return utf8_from_wide_char(buf); } + +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; + } + + 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 index feb3d1cb..df1f9e53 100644 --- a/app/src/tiny_xpm.c +++ b/app/src/tiny_xpm.c @@ -6,7 +6,6 @@ #include #include -#include "config.h" #include "util/log.h" struct index { diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h index 6e6f8035..29b42d14 100644 --- a/app/src/tiny_xpm.h +++ b/app/src/tiny_xpm.h @@ -1,9 +1,9 @@ #ifndef TINYXPM_H #define TINYXPM_H -#include +#include "common.h" -#include "config.h" +#include SDL_Surface * read_xpm(char *xpm[]); diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h new file mode 100644 index 00000000..64ab0de9 --- /dev/null +++ b/app/src/trait/frame_sink.h @@ -0,0 +1,26 @@ +#ifndef SC_FRAME_SINK +#define SC_FRAME_SINK + +#include "common.h" + +#include +#include + +typedef struct AVFrame AVFrame; + +/** + * Frame sink trait. + * + * Component able to receive AVFrames should implement this trait. + */ +struct sc_frame_sink { + const struct sc_frame_sink_ops *ops; +}; + +struct sc_frame_sink_ops { + bool (*open)(struct sc_frame_sink *sink); + void (*close)(struct sc_frame_sink *sink); + bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); +}; + +#endif diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h new file mode 100644 index 00000000..fe9c137d --- /dev/null +++ b/app/src/trait/packet_sink.h @@ -0,0 +1,27 @@ +#ifndef SC_PACKET_SINK +#define SC_PACKET_SINK + +#include "common.h" + +#include +#include + +typedef struct AVCodec AVCodec; +typedef struct AVPacket AVPacket; + +/** + * Packet sink trait. + * + * Component able to receive AVPackets should implement this trait. + */ +struct sc_packet_sink { + const struct sc_packet_sink_ops *ops; +}; + +struct sc_packet_sink_ops { + bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); + void (*close)(struct sc_packet_sink *sink); + bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); +}; + +#endif diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h index 17234e42..337bb262 100644 --- a/app/src/util/buffer_util.h +++ b/app/src/util/buffer_util.h @@ -1,11 +1,11 @@ #ifndef BUFFER_UTIL_H #define BUFFER_UTIL_H +#include "common.h" + #include #include -#include "config.h" - static inline void buffer_write16be(uint8_t *buf, uint16_t value) { buf[0] = value >> 8; @@ -33,7 +33,7 @@ buffer_read16be(const uint8_t *buf) { static inline uint32_t buffer_read32be(const uint8_t *buf) { - return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; + return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } static inline uint64_t diff --git a/app/src/util/cbuf.h b/app/src/util/cbuf.h index c18e4680..01e41044 100644 --- a/app/src/util/cbuf.h +++ b/app/src/util/cbuf.h @@ -2,11 +2,11 @@ #ifndef CBUF_H #define CBUF_H +#include "common.h" + #include #include -#include "config.h" - // To define a circular buffer type of 20 ints: // struct cbuf_int CBUF(int, 20); // diff --git a/app/src/util/lock.h b/app/src/util/lock.h deleted file mode 100644 index cb7c318c..00000000 --- a/app/src/util/lock.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef LOCK_H -#define LOCK_H - -#include -#include - -#include "config.h" -#include "log.h" - -static inline void -mutex_lock(SDL_mutex *mutex) { - int r = SDL_LockMutex(mutex); -#ifndef NDEBUG - if (r) { - LOGC("Could not lock mutex: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -static inline void -mutex_unlock(SDL_mutex *mutex) { - int r = SDL_UnlockMutex(mutex); -#ifndef NDEBUG - if (r) { - LOGC("Could not unlock mutex: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -static inline void -cond_wait(SDL_cond *cond, SDL_mutex *mutex) { - int r = SDL_CondWait(cond, mutex); -#ifndef NDEBUG - if (r) { - LOGC("Could not wait on condition: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -static inline int -cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) { - int r = SDL_CondWaitTimeout(cond, mutex, ms); -#ifndef NDEBUG - if (r < 0) { - LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); - abort(); - } -#endif - return r; -} - -static inline void -cond_signal(SDL_cond *cond) { - int r = SDL_CondSignal(cond); -#ifndef NDEBUG - if (r) { - LOGC("Could not signal a condition: %s", SDL_GetError()); - abort(); - } -#else - (void) r; -#endif -} - -#endif diff --git a/app/src/util/log.c b/app/src/util/log.c new file mode 100644 index 00000000..a285fffb --- /dev/null +++ b/app/src/util/log.c @@ -0,0 +1,53 @@ +#include "log.h" + +#include + +static SDL_LogPriority +log_level_sc_to_sdl(enum sc_log_level level) { + switch (level) { + case SC_LOG_LEVEL_VERBOSE: + return SDL_LOG_PRIORITY_VERBOSE; + case SC_LOG_LEVEL_DEBUG: + return SDL_LOG_PRIORITY_DEBUG; + case SC_LOG_LEVEL_INFO: + return SDL_LOG_PRIORITY_INFO; + case SC_LOG_LEVEL_WARN: + return SDL_LOG_PRIORITY_WARN; + case SC_LOG_LEVEL_ERROR: + return SDL_LOG_PRIORITY_ERROR; + default: + assert(!"unexpected log level"); + return SDL_LOG_PRIORITY_INFO; + } +} + +static enum sc_log_level +log_level_sdl_to_sc(SDL_LogPriority priority) { + switch (priority) { + case SDL_LOG_PRIORITY_VERBOSE: + return SC_LOG_LEVEL_VERBOSE; + case SDL_LOG_PRIORITY_DEBUG: + return SC_LOG_LEVEL_DEBUG; + case SDL_LOG_PRIORITY_INFO: + return SC_LOG_LEVEL_INFO; + case SDL_LOG_PRIORITY_WARN: + return SC_LOG_LEVEL_WARN; + case SDL_LOG_PRIORITY_ERROR: + return SC_LOG_LEVEL_ERROR; + default: + assert(!"unexpected log level"); + return SC_LOG_LEVEL_INFO; + } +} + +void +sc_set_log_level(enum sc_log_level level) { + SDL_LogPriority sdl_log = log_level_sc_to_sdl(level); + SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); +} + +enum sc_log_level +sc_get_log_level(void) { + SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION); + return log_level_sdl_to_sc(sdl_log); +} diff --git a/app/src/util/log.h b/app/src/util/log.h index 5955c7fb..30934b5c 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -1,8 +1,12 @@ -#ifndef LOG_H -#define LOG_H +#ifndef SC_LOG_H +#define SC_LOG_H + +#include "common.h" #include +#include "scrcpy.h" + #define LOGV(...) SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGD(...) SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGI(...) SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) @@ -10,4 +14,10 @@ #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) +void +sc_set_log_level(enum sc_log_level level); + +enum sc_log_level +sc_get_log_level(void); + #endif diff --git a/app/src/util/net.c b/app/src/util/net.c index bf4389dd..bbf57bbc 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,8 +1,8 @@ #include "net.h" #include +#include -#include "config.h" #include "log.h" #ifdef __WINDOWS__ @@ -115,3 +115,32 @@ bool net_shutdown(socket_t socket, int how) { return !shutdown(socket, how); } + +bool +net_init(void) { +#ifdef __WINDOWS__ + WSADATA wsa; + int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; + if (res < 0) { + LOGC("WSAStartup failed with error %d", res); + return false; + } +#endif + return true; +} + +void +net_cleanup(void) { +#ifdef __WINDOWS__ + WSACleanup(); +#endif +} + +bool +net_close(socket_t socket) { +#ifdef __WINDOWS__ + return !closesocket(socket); +#else + return !close(socket); +#endif +} diff --git a/app/src/util/net.h b/app/src/util/net.h index ffd5dd89..d3b1f941 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -1,6 +1,8 @@ #ifndef NET_H #define NET_H +#include "common.h" + #include #include #include @@ -17,8 +19,6 @@ typedef int socket_t; #endif -#include "config.h" - bool net_init(void); diff --git a/app/src/util/process.c b/app/src/util/process.c new file mode 100644 index 00000000..5edeeee6 --- /dev/null +++ b/app/src/util/process.c @@ -0,0 +1,21 @@ +#include "process.h" + +#include "log.h" + +bool +process_check_success(process_t proc, const char *name, bool close) { + if (proc == PROCESS_NONE) { + LOGE("Could not execute \"%s\"", name); + return false; + } + exit_code_t exit_code = process_wait(proc, close); + if (exit_code) { + if (exit_code != NO_EXIT_CODE) { + LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code); + } else { + LOGE("\"%s\" exited unexpectedly", name); + } + return false; + } + return true; +} diff --git a/app/src/command.h b/app/src/util/process.h similarity index 52% rename from app/src/command.h rename to app/src/util/process.h index 9fc81c1c..7838a848 100644 --- a/app/src/command.h +++ b/app/src/util/process.h @@ -1,8 +1,9 @@ -#ifndef COMMAND_H -#define COMMAND_H +#ifndef SC_PROCESS_H +#define SC_PROCESS_H + +#include "common.h" #include -#include #ifdef _WIN32 @@ -12,11 +13,7 @@ # define PATH_SEPARATOR '\\' # define PRIexitcode "lu" // -# ifdef _WIN64 -# define PRIsizet PRIu64 -# else -# define PRIsizet PRIu32 -# endif +# define PRIsizet "Iu" # define PROCESS_NONE NULL # define NO_EXIT_CODE -1u // max value as unsigned typedef HANDLE process_t; @@ -35,53 +32,45 @@ #endif -#include "config.h" - enum process_result { PROCESS_SUCCESS, PROCESS_ERROR_GENERIC, PROCESS_ERROR_MISSING_BINARY, }; +// execute the command and write the result to the output parameter "process" enum process_result -cmd_execute(const char *const argv[], process_t *process); +process_execute(const char *const argv[], process_t *process); +// kill the process bool -cmd_terminate(process_t pid); +process_terminate(process_t pid); -bool -cmd_simple_wait(process_t pid, exit_code_t *exit_code); +// 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); -process_t -adb_execute(const char *serial, const char *const adb_cmd[], size_t len); - -process_t -adb_forward(const char *serial, uint16_t local_port, - const char *device_socket_name); - -process_t -adb_forward_remove(const char *serial, uint16_t local_port); - -process_t -adb_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port); - -process_t -adb_reverse_remove(const char *serial, const char *device_socket_name); - -process_t -adb_push(const char *serial, const char *local, const char *remote); - -process_t -adb_install(const char *serial, const char *local); +// close the process +// +// Semantically, process_wait(close) = process_wait(noclose) + process_close +void +process_close(process_t pid); // 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); +process_check_success(process_t proc, const char *name, bool close); + +#ifndef _WIN32 +// only used to find package manager, not implemented for Windows +bool +search_executable(const char *file); +#endif // return the absolute path of the executable (the scrcpy binary) -// may be NULL on error; to be freed by SDL_free +// may be NULL on error; to be freed by free() char * get_executable_path(void); diff --git a/app/src/util/queue.h b/app/src/util/queue.h index 12bc9e89..0681070c 100644 --- a/app/src/util/queue.h +++ b/app/src/util/queue.h @@ -2,12 +2,12 @@ #ifndef QUEUE_H #define QUEUE_H +#include "common.h" + #include #include #include -#include "config.h" - // To define a queue type of "struct foo": // struct queue_foo QUEUE(struct foo); #define QUEUE(TYPE) { \ diff --git a/app/src/util/str_util.c b/app/src/util/str_util.c index 4d175407..287c08de 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str_util.c @@ -10,10 +10,6 @@ # include #endif -#include - -#include "config.h" - size_t xstrncpy(char *dest, const char *src, size_t n) { size_t i; @@ -51,7 +47,7 @@ truncated: char * strquote(const char *src) { size_t len = strlen(src); - char *quoted = SDL_malloc(len + 3); + char *quoted = malloc(len + 3); if (!quoted) { return NULL; } @@ -81,6 +77,35 @@ parse_integer(const char *s, long *out) { return true; } +size_t +parse_integers(const char *s, const char sep, size_t max_items, long *out) { + size_t count = 0; + char *endptr; + do { + errno = 0; + long value = strtol(s, &endptr, 0); + if (errno == ERANGE) { + return 0; + } + + if (endptr == s || (*endptr != sep && *endptr != '\0')) { + return 0; + } + + out[count++] = value; + if (*endptr == sep) { + if (count >= max_items) { + // max items already reached, could not accept a new item + return 0; + } + // parse the next token during the next iteration + s = endptr + 1; + } + } while (*endptr != '\0'); + + return count; +} + bool parse_integer_with_suffix(const char *s, long *out) { char *endptr; @@ -115,6 +140,24 @@ parse_integer_with_suffix(const char *s, long *out) { return true; } +bool +strlist_contains(const char *list, char sep, const char *s) { + char *p; + do { + p = strchr(list, sep); + + size_t token_len = p ? (size_t) (p - list) : strlen(list); + if (!strncmp(list, s, token_len)) { + return true; + } + + if (p) { + list = p + 1; + } + } while (p); + return false; +} + size_t utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); @@ -140,7 +183,7 @@ utf8_to_wide_char(const char *utf8) { return NULL; } - wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); + wchar_t *wide = malloc(len * sizeof(wchar_t)); if (!wide) { return NULL; } @@ -156,7 +199,7 @@ utf8_from_wide_char(const wchar_t *ws) { return NULL; } - char *utf8 = SDL_malloc(len); + char *utf8 = malloc(len); if (!utf8) { return NULL; } diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h index 8d9b990c..361d2bdd 100644 --- a/app/src/util/str_util.h +++ b/app/src/util/str_util.h @@ -1,11 +1,11 @@ #ifndef STRUTIL_H #define STRUTIL_H +#include "common.h" + #include #include -#include "config.h" - // like strncpy, except: // - it copies at most n-1 chars // - the dest string is nul-terminated @@ -16,7 +16,7 @@ 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 trucation +// 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); @@ -31,6 +31,11 @@ strquote(const char *src); 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 @@ -38,6 +43,11 @@ parse_integer(const char *s, long *out); 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); diff --git a/app/src/util/thread.c b/app/src/util/thread.c new file mode 100644 index 00000000..a0a99f20 --- /dev/null +++ b/app/src/util/thread.c @@ -0,0 +1,160 @@ +#include "thread.h" + +#include +#include + +#include "log.h" + +bool +sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, + void *userdata) { + SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); + if (!sdl_thread) { + return false; + } + + thread->thread = sdl_thread; + return true; +} + +void +sc_thread_join(sc_thread *thread, int *status) { + SDL_WaitThread(thread->thread, status); +} + +bool +sc_mutex_init(sc_mutex *mutex) { + SDL_mutex *sdl_mutex = SDL_CreateMutex(); + if (!sdl_mutex) { + return false; + } + + mutex->mutex = sdl_mutex; +#ifndef NDEBUG + mutex->locker = 0; +#endif + return true; +} + +void +sc_mutex_destroy(sc_mutex *mutex) { + SDL_DestroyMutex(mutex->mutex); +} + +void +sc_mutex_lock(sc_mutex *mutex) { + // SDL mutexes are recursive, but we don't want to use recursive mutexes + assert(!sc_mutex_held(mutex)); + int r = SDL_LockMutex(mutex->mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not lock mutex: %s", SDL_GetError()); + abort(); + } + + mutex->locker = sc_thread_get_id(); +#else + (void) r; +#endif +} + +void +sc_mutex_unlock(sc_mutex *mutex) { +#ifndef NDEBUG + assert(sc_mutex_held(mutex)); + mutex->locker = 0; +#endif + int r = SDL_UnlockMutex(mutex->mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not lock mutex: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +sc_thread_id +sc_thread_get_id(void) { + return SDL_ThreadID(); +} + +#ifndef NDEBUG +bool +sc_mutex_held(struct sc_mutex *mutex) { + return mutex->locker == sc_thread_get_id(); +} +#endif + +bool +sc_cond_init(sc_cond *cond) { + SDL_cond *sdl_cond = SDL_CreateCond(); + if (!sdl_cond) { + return false; + } + + cond->cond = sdl_cond; + return true; +} + +void +sc_cond_destroy(sc_cond *cond) { + SDL_DestroyCond(cond->cond); +} + +void +sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { + int r = SDL_CondWait(cond->cond, mutex->mutex); +#ifndef NDEBUG + if (r) { + LOGC("Could not wait on condition: %s", SDL_GetError()); + abort(); + } + + mutex->locker = sc_thread_get_id(); +#else + (void) r; +#endif +} + +bool +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) { + int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); +#ifndef NDEBUG + if (r < 0) { + LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); + abort(); + } + + mutex->locker = sc_thread_get_id(); +#endif + assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); + return r == 0; +} + +void +sc_cond_signal(sc_cond *cond) { + int r = SDL_CondSignal(cond->cond); +#ifndef NDEBUG + if (r) { + LOGC("Could not signal a condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} + +void +sc_cond_broadcast(sc_cond *cond) { + int r = SDL_CondBroadcast(cond->cond); +#ifndef NDEBUG + if (r) { + LOGC("Could not broadcast a condition: %s", SDL_GetError()); + abort(); + } +#else + (void) r; +#endif +} diff --git a/app/src/util/thread.h b/app/src/util/thread.h new file mode 100644 index 00000000..d23e1432 --- /dev/null +++ b/app/src/util/thread.h @@ -0,0 +1,81 @@ +#ifndef SC_THREAD_H +#define SC_THREAD_H + +#include "common.h" + +#include +#include + +/* Forward declarations */ +typedef struct SDL_Thread SDL_Thread; +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 struct sc_thread { + SDL_Thread *thread; +} sc_thread; + +typedef struct sc_mutex { + SDL_mutex *mutex; +#ifndef NDEBUG + sc_thread_id locker; +#endif +} sc_mutex; + +typedef struct sc_cond { + SDL_cond *cond; +} sc_cond; + +bool +sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, + void *userdata); + +void +sc_thread_join(sc_thread *thread, int *status); + +bool +sc_mutex_init(sc_mutex *mutex); + +void +sc_mutex_destroy(sc_mutex *mutex); + +void +sc_mutex_lock(sc_mutex *mutex); + +void +sc_mutex_unlock(sc_mutex *mutex); + +sc_thread_id +sc_thread_get_id(void); + +#ifndef NDEBUG +bool +sc_mutex_held(struct sc_mutex *mutex); +# define sc_mutex_assert(mutex) assert(sc_mutex_held(mutex)) +#else +# define sc_mutex_assert(mutex) +#endif + +bool +sc_cond_init(sc_cond *cond); + +void +sc_cond_destroy(sc_cond *cond); + +void +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); + +void +sc_cond_signal(sc_cond *cond); + +void +sc_cond_broadcast(sc_cond *cond); + +#endif diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c new file mode 100644 index 00000000..bd184b4d --- /dev/null +++ b/app/src/v4l2_sink.c @@ -0,0 +1,356 @@ +#include "v4l2_sink.h" + +#include "util/log.h" +#include "util/str_util.h" + +/** Downcast frame_sink to sc_v4l2_sink */ +#define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink) + +static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us + +static const AVOutputFormat * +find_muxer(const char *name) { +#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API + void *opaque = NULL; +#endif + const AVOutputFormat *oformat = NULL; + do { +#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API + oformat = av_muxer_iterate(&opaque); +#else + oformat = av_oformat_next(oformat); +#endif + // until null or containing the requested name + } while (oformat && !strlist_contains(oformat->name, ',', name)); + return oformat; +} + +static bool +write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) { + AVStream *ostream = vs->format_ctx->streams[0]; + + uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); + if (!extradata) { + LOGC("Could not allocate extradata"); + return false; + } + + // copy the first packet to the extra data + memcpy(extradata, packet->data, packet->size); + + ostream->codecpar->extradata = extradata; + ostream->codecpar->extradata_size = packet->size; + + int ret = avformat_write_header(vs->format_ctx, NULL); + if (ret < 0) { + LOGE("Failed to write header to %s", vs->device_name); + return false; + } + + return true; +} + +static void +rescale_packet(struct sc_v4l2_sink *vs, AVPacket *packet) { + AVStream *ostream = vs->format_ctx->streams[0]; + av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); +} + +static bool +write_packet(struct sc_v4l2_sink *vs, AVPacket *packet) { + if (!vs->header_written) { + bool ok = write_header(vs, packet); + if (!ok) { + return false; + } + vs->header_written = true; + return true; + } + + rescale_packet(vs, packet); + + bool ok = av_write_frame(vs->format_ctx, packet) >= 0; + + // Failing to write the last frame is not very serious, no future frame may + // depend on it, so the resulting file will still be valid + (void) ok; + + return true; +} + +static bool +encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) { + int ret = avcodec_send_frame(vs->encoder_ctx, frame); + if (ret < 0 && ret != AVERROR(EAGAIN)) { + LOGE("Could not send v4l2 video frame: %d", ret); + return false; + } + + AVPacket *packet = vs->packet; + ret = avcodec_receive_packet(vs->encoder_ctx, packet); + if (ret == 0) { + // A packet was received + + bool ok = write_packet(vs, packet); + av_packet_unref(packet); + if (!ok) { + LOGW("Could not send packet to v4l2 sink"); + return false; + } + } else if (ret != AVERROR(EAGAIN)) { + LOGE("Could not receive v4l2 video packet: %d", ret); + return false; + } + + return true; +} + +static int +run_v4l2_sink(void *data) { + struct sc_v4l2_sink *vs = data; + + for (;;) { + sc_mutex_lock(&vs->mutex); + + while (!vs->stopped && vs->vb.pending_frame_consumed) { + sc_cond_wait(&vs->cond, &vs->mutex); + } + + if (vs->stopped) { + sc_mutex_unlock(&vs->mutex); + break; + } + + sc_mutex_unlock(&vs->mutex); + + video_buffer_consume(&vs->vb, vs->frame); + bool ok = encode_and_write_frame(vs, vs->frame); + av_frame_unref(vs->frame); + if (!ok) { + LOGE("Could not send frame to v4l2 sink"); + break; + } + } + + LOGD("V4l2 thread ended"); + + return 0; +} + +static bool +sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { + bool ok = video_buffer_init(&vs->vb); + if (!ok) { + return false; + } + + ok = sc_mutex_init(&vs->mutex); + if (!ok) { + LOGC("Could not create mutex"); + goto error_video_buffer_destroy; + } + + ok = sc_cond_init(&vs->cond); + if (!ok) { + LOGC("Could not create cond"); + goto error_mutex_destroy; + } + + // FIXME + const AVOutputFormat *format = find_muxer("video4linux2,v4l2"); + if (!format) { + LOGE("Could not find v4l2 muxer"); + goto error_cond_destroy; + } + + const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO); + if (!encoder) { + LOGE("Raw video encoder not found"); + return false; + } + + vs->format_ctx = avformat_alloc_context(); + if (!vs->format_ctx) { + LOGE("Could not allocate v4l2 output context"); + return false; + } + + // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() + // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat + // still expects a pointer-to-non-const (it has not be updated accordingly) + // + vs->format_ctx->oformat = (AVOutputFormat *) format; +#ifdef SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL + vs->format_ctx->url = strdup(vs->device_name); + if (!vs->format_ctx->url) { + LOGE("Could not strdup v4l2 device name"); + goto error_avformat_free_context; + return false; + } +#else + strncpy(vs->format_ctx->filename, vs->device_name, + sizeof(vs->format_ctx->filename)); +#endif + + AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder); + if (!ostream) { + LOGE("Could not allocate new v4l2 stream"); + goto error_avformat_free_context; + return false; + } + + ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + ostream->codecpar->codec_id = encoder->id; + ostream->codecpar->format = AV_PIX_FMT_YUV420P; + ostream->codecpar->width = vs->frame_size.width; + ostream->codecpar->height = vs->frame_size.height; + + int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE); + if (ret < 0) { + LOGE("Failed to open output device: %s", vs->device_name); + // ostream will be cleaned up during context cleaning + goto error_avformat_free_context; + } + + vs->encoder_ctx = avcodec_alloc_context3(encoder); + if (!vs->encoder_ctx) { + LOGC("Could not allocate codec context for v4l2"); + goto error_avio_close; + } + + vs->encoder_ctx->width = vs->frame_size.width; + vs->encoder_ctx->height = vs->frame_size.height; + vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + vs->encoder_ctx->time_base.num = 1; + vs->encoder_ctx->time_base.den = 1; + + if (avcodec_open2(vs->encoder_ctx, encoder, NULL) < 0) { + LOGE("Could not open codec for v4l2"); + goto error_avcodec_free_context; + } + + vs->frame = av_frame_alloc(); + if (!vs->frame) { + LOGE("Could not create v4l2 frame"); + goto error_avcodec_close; + } + + vs->packet = av_packet_alloc(); + if (!vs->packet) { + LOGE("Could not allocate packet"); + goto error_av_frame_free; + } + + LOGD("Starting v4l2 thread"); + ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); + if (!ok) { + LOGC("Could not start v4l2 thread"); + goto error_av_packet_free; + } + + vs->header_written = false; + vs->stopped = false; + + LOGI("v4l2 sink started to device: %s", vs->device_name); + + return true; + +error_av_packet_free: + av_packet_free(&vs->packet); +error_av_frame_free: + av_frame_free(&vs->frame); +error_avcodec_close: + avcodec_close(vs->encoder_ctx); +error_avcodec_free_context: + avcodec_free_context(&vs->encoder_ctx); +error_avio_close: + avio_close(vs->format_ctx->pb); +error_avformat_free_context: + avformat_free_context(vs->format_ctx); +error_cond_destroy: + sc_cond_destroy(&vs->cond); +error_mutex_destroy: + sc_mutex_destroy(&vs->mutex); +error_video_buffer_destroy: + video_buffer_destroy(&vs->vb); + + return false; +} + +static void +sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { + sc_mutex_lock(&vs->mutex); + vs->stopped = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + + sc_thread_join(&vs->thread, NULL); + + av_packet_free(&vs->packet); + av_frame_free(&vs->frame); + avcodec_close(vs->encoder_ctx); + avcodec_free_context(&vs->encoder_ctx); + avio_close(vs->format_ctx->pb); + avformat_free_context(vs->format_ctx); + sc_cond_destroy(&vs->cond); + sc_mutex_destroy(&vs->mutex); + 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; +} + +static bool +sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) { + struct sc_v4l2_sink *vs = DOWNCAST(sink); + return sc_v4l2_sink_open(vs); +} + +static void +sc_v4l2_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_v4l2_sink *vs = DOWNCAST(sink); + sc_v4l2_sink_close(vs); +} + +static bool +sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { + struct sc_v4l2_sink *vs = DOWNCAST(sink); + return sc_v4l2_sink_push(vs, frame); +} + +bool +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, + struct size frame_size) { + vs->device_name = strdup(device_name); + if (!vs->device_name) { + LOGE("Could not strdup v4l2 device name"); + return false; + } + + vs->frame_size = frame_size; + + static const struct sc_frame_sink_ops ops = { + .open = sc_v4l2_frame_sink_open, + .close = sc_v4l2_frame_sink_close, + .push = sc_v4l2_frame_sink_push, + }; + + vs->frame_sink.ops = &ops; + + return true; +} + +void +sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) { + free(vs->device_name); +} diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h new file mode 100644 index 00000000..81bcdd1e --- /dev/null +++ b/app/src/v4l2_sink.h @@ -0,0 +1,39 @@ +#ifndef SC_V4L2_SINK_H +#define SC_V4L2_SINK_H + +#include "common.h" + +#include "coords.h" +#include "trait/frame_sink.h" +#include "video_buffer.h" + +#include + +struct sc_v4l2_sink { + struct sc_frame_sink frame_sink; // frame sink trait + + struct video_buffer vb; + AVFormatContext *format_ctx; + AVCodecContext *encoder_ctx; + + char *device_name; + struct size frame_size; + + sc_thread thread; + sc_mutex mutex; + sc_cond cond; + bool stopped; + bool header_written; + + AVFrame *frame; + AVPacket *packet; +}; + +bool +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, + struct size frame_size); + +void +sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); + +#endif diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 629680d9..7adf098b 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -1,113 +1,88 @@ #include "video_buffer.h" #include -#include #include #include -#include "config.h" -#include "util/lock.h" #include "util/log.h" bool -video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, - bool render_expired_frames) { - vb->fps_counter = fps_counter; - - if (!(vb->decoding_frame = av_frame_alloc())) { - goto error_0; +video_buffer_init(struct video_buffer *vb) { + vb->pending_frame = av_frame_alloc(); + if (!vb->pending_frame) { + return false; } - if (!(vb->rendering_frame = av_frame_alloc())) { - goto error_1; + vb->tmp_frame = av_frame_alloc(); + if (!vb->tmp_frame) { + av_frame_free(&vb->pending_frame); + return false; } - if (!(vb->mutex = SDL_CreateMutex())) { - goto error_2; + bool ok = sc_mutex_init(&vb->mutex); + if (!ok) { + av_frame_free(&vb->pending_frame); + av_frame_free(&vb->tmp_frame); + return false; } - vb->render_expired_frames = render_expired_frames; - if (render_expired_frames) { - if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) { - SDL_DestroyMutex(vb->mutex); - goto error_2; - } - // interrupted is not used if expired frames are not rendered - // since offering a frame will never block - vb->interrupted = false; - } - - // there is initially no rendering frame, so consider it has already been - // consumed - vb->rendering_frame_consumed = true; + // there is initially no frame, so consider it has already been consumed + vb->pending_frame_consumed = true; return true; - -error_2: - av_frame_free(&vb->rendering_frame); -error_1: - av_frame_free(&vb->decoding_frame); -error_0: - return false; } void video_buffer_destroy(struct video_buffer *vb) { - if (vb->render_expired_frames) { - SDL_DestroyCond(vb->rendering_frame_consumed_cond); - } - SDL_DestroyMutex(vb->mutex); - av_frame_free(&vb->rendering_frame); - av_frame_free(&vb->decoding_frame); + sc_mutex_destroy(&vb->mutex); + av_frame_free(&vb->pending_frame); + av_frame_free(&vb->tmp_frame); } -static void -video_buffer_swap_frames(struct video_buffer *vb) { - AVFrame *tmp = vb->decoding_frame; - vb->decoding_frame = vb->rendering_frame; - vb->rendering_frame = tmp; +static inline void +swap_frames(AVFrame **lhs, AVFrame **rhs) { + AVFrame *tmp = *lhs; + *lhs = *rhs; + *rhs = tmp; +} + +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); + 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); + + if (previous_frame_skipped) { + *previous_frame_skipped = !vb->pending_frame_consumed; + } + vb->pending_frame_consumed = false; + + sc_mutex_unlock(&vb->mutex); + + return true; } void -video_buffer_offer_decoded_frame(struct video_buffer *vb, - bool *previous_frame_skipped) { - mutex_lock(vb->mutex); - if (vb->render_expired_frames) { - // wait for the current (expired) frame to be consumed - while (!vb->rendering_frame_consumed && !vb->interrupted) { - cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); - } - } else if (!vb->rendering_frame_consumed) { - fps_counter_add_skipped_frame(vb->fps_counter); - } +video_buffer_consume(struct video_buffer *vb, AVFrame *dst) { + sc_mutex_lock(&vb->mutex); + assert(!vb->pending_frame_consumed); + vb->pending_frame_consumed = true; - video_buffer_swap_frames(vb); + av_frame_move_ref(dst, vb->pending_frame); + // av_frame_move_ref() resets its source frame, so no need to call + // av_frame_unref() - *previous_frame_skipped = !vb->rendering_frame_consumed; - vb->rendering_frame_consumed = false; - - mutex_unlock(vb->mutex); -} - -const AVFrame * -video_buffer_consume_rendered_frame(struct video_buffer *vb) { - assert(!vb->rendering_frame_consumed); - vb->rendering_frame_consumed = true; - fps_counter_add_rendered_frame(vb->fps_counter); - if (vb->render_expired_frames) { - // unblock video_buffer_offer_decoded_frame() - cond_signal(vb->rendering_frame_consumed_cond); - } - return vb->rendering_frame; -} - -void -video_buffer_interrupt(struct video_buffer *vb) { - if (vb->render_expired_frames) { - mutex_lock(vb->mutex); - vb->interrupted = true; - mutex_unlock(vb->mutex); - // wake up blocking wait - cond_signal(vb->rendering_frame_consumed_cond); - } + sc_mutex_unlock(&vb->mutex); } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index 303b3fc2..b9478f4c 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -1,49 +1,50 @@ #ifndef VIDEO_BUFFER_H #define VIDEO_BUFFER_H -#include -#include +#include "common.h" + +#include -#include "config.h" #include "fps_counter.h" +#include "util/thread.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 video_buffer { - AVFrame *decoding_frame; - AVFrame *rendering_frame; - SDL_mutex *mutex; - bool render_expired_frames; - bool interrupted; - SDL_cond *rendering_frame_consumed_cond; - bool rendering_frame_consumed; - struct fps_counter *fps_counter; + AVFrame *pending_frame; + AVFrame *tmp_frame; // To preserve the pending frame on error + + sc_mutex mutex; + + bool pending_frame_consumed; }; bool -video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, - bool render_expired_frames); +video_buffer_init(struct video_buffer *vb); void video_buffer_destroy(struct video_buffer *vb); -// set the decoded frame as ready for rendering -// this function locks frames->mutex during its execution -// the output flag is set to report whether the previous frame has been skipped -void -video_buffer_offer_decoded_frame(struct video_buffer *vb, - bool *previous_frame_skipped); +bool +video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped); -// mark the rendering frame as consumed and return it -// MUST be called with frames->mutex locked!!! -// the caller is expected to render the returned frame to some texture before -// unlocking frames->mutex -const AVFrame * -video_buffer_consume_rendered_frame(struct video_buffer *vb); - -// wake up and avoid any blocking call void -video_buffer_interrupt(struct video_buffer *vb); +video_buffer_consume(struct video_buffer *vb, AVFrame *dst); #endif diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c index ba3f9f06..c7c13bdd 100644 --- a/app/tests/test_buffer_util.c +++ b/app/tests/test_buffer_util.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include "util/buffer_util.h" @@ -65,7 +67,10 @@ static void test_buffer_read64be(void) { assert(val == 0xABCD1234567890EF); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_buffer_write16be(); test_buffer_write32be(); test_buffer_write64be(); diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c index dbe50aab..16674e92 100644 --- a/app/tests/test_cbuf.c +++ b/app/tests/test_cbuf.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include @@ -65,7 +67,10 @@ static void test_cbuf_push_take(void) { assert(item == 35); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_cbuf_empty(); test_cbuf_full(); test_cbuf_push_take(); diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 539c3c94..94740a9a 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -1,7 +1,10 @@ +#include "common.h" + #include +#include #include "cli.h" -#include "common.h" +#include "scrcpy.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { @@ -48,13 +51,13 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", + "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" // "--no-display" is not compatible with "--fulscreen" - "--port", "1234", + "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", "--record-format", "mkv", - "--render-expired-frames", "--serial", "0123456789abcdef", "--show-touches", "--turn-screen-off", @@ -72,17 +75,17 @@ static void test_options(void) { const struct scrcpy_options *opts = &args.opts; assert(opts->always_on_top); - fprintf(stderr, "%d\n", (int) opts->bit_rate); assert(opts->bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); - assert(opts->port == 1234); + assert(opts->lock_video_orientation == 2); + assert(opts->port_range.first == 1234); + assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); - assert(opts->record_format == RECORDER_FORMAT_MKV); - assert(opts->render_expired_frames); + assert(opts->record_format == SC_RECORD_FORMAT_MKV); assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); assert(opts->turn_screen_off); @@ -116,13 +119,54 @@ static void test_options2(void) { assert(!opts->control); assert(!opts->display); assert(!strcmp(opts->record_filename, "file.mp4")); - assert(opts->record_format == RECORDER_FORMAT_MP4); + assert(opts->record_format == SC_RECORD_FORMAT_MP4); } -int main(void) { +static void test_parse_shortcut_mods(void) { + struct sc_shortcut_mods mods; + bool ok; + + ok = sc_parse_shortcut_mods("lctrl", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == SC_MOD_LCTRL); + + ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); + assert(ok); + assert(mods.count == 1); + assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT)); + + ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); + assert(ok); + assert(mods.count == 2); + assert(mods.data[0] == SC_MOD_RCTRL); + assert(mods.data[1] == SC_MOD_LALT); + + ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); + assert(ok); + assert(mods.count == 3); + assert(mods.data[0] == SC_MOD_LSUPER); + assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT)); + assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT)); + + ok = sc_parse_shortcut_mods("", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl+", &mods); + assert(!ok); + + ok = sc_parse_shortcut_mods("lctrl,", &mods); + assert(!ok); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_flag_version(); test_flag_help(); test_options(); test_options2(); + test_parse_shortcut_mods(); return 0; }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index d6f556f3..ef9247ca 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include @@ -9,18 +11,20 @@ static void test_serialize_inject_keycode(void) { .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, + .repeat = 5, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 10); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 14); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER + 0x00, 0x00, 0x00, 0X05, // repeat 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -34,13 +38,13 @@ static void test_serialize_inject_text(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 16); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 18); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_TEXT, - 0x00, 0x0d, // text length + 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -49,20 +53,22 @@ static void test_serialize_inject_text(void) { static void test_serialize_inject_text_long(void) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - char text[CONTROL_MSG_TEXT_MAX_LENGTH + 1]; + char text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', sizeof(text)); - text[CONTROL_MSG_TEXT_MAX_LENGTH] = '\0'; + text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 3 + CONTROL_MSG_TEXT_MAX_LENGTH); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[3 + CONTROL_MSG_TEXT_MAX_LENGTH]; + unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; - expected[1] = 0x01; - expected[2] = 0x2c; // text length (16 bits) - memset(&expected[3], 'a', CONTROL_MSG_TEXT_MAX_LENGTH); + expected[1] = 0x00; + expected[2] = 0x00; + expected[3] = 0x01; + expected[4] = 0x2c; // text length (32 bits) + memset(&expected[5], 'a', CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } @@ -88,8 +94,8 @@ static void test_serialize_inject_touch_event(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 28); const unsigned char expected[] = { @@ -123,8 +129,8 @@ static void test_serialize_inject_scroll_event(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 21); const unsigned char expected[] = { @@ -140,14 +146,18 @@ static void test_serialize_inject_scroll_event(void) { static void test_serialize_back_or_screen_on(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + .back_or_screen_on = { + .action = AKEY_EVENT_ACTION_UP, + }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 1); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + 0x01, // AKEY_EVENT_ACTION_UP }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -157,8 +167,8 @@ static void test_serialize_expand_notification_panel(void) { .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -167,17 +177,32 @@ static void test_serialize_expand_notification_panel(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_collapse_notification_panel(void) { +static void test_serialize_expand_settings_panel(void) { struct control_msg msg = { - .type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { - CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, + CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_collapse_panels(void) { + struct control_msg msg = { + .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, + }; + + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 1); + + const unsigned char expected[] = { + CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -187,8 +212,8 @@ static void test_serialize_get_clipboard(void) { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -200,18 +225,20 @@ static void test_serialize_get_clipboard(void) { static void test_serialize_set_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, - .inject_text = { + .set_clipboard = { + .paste = true, .text = "hello, world!", }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); - assert(size == 16); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); + assert(size == 19); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_CLIPBOARD, - 0x00, 0x0d, // text length + 1, // paste + 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); @@ -225,8 +252,8 @@ static void test_serialize_set_screen_power_mode(void) { }, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { @@ -241,8 +268,8 @@ static void test_serialize_rotate_device(void) { .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, }; - unsigned char buf[CONTROL_MSG_SERIALIZED_MAX_SIZE]; - int size = control_msg_serialize(&msg, buf); + unsigned char buf[CONTROL_MSG_MAX_SIZE]; + size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { @@ -251,7 +278,10 @@ static void test_serialize_rotate_device(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); @@ -259,7 +289,8 @@ int main(void) { test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); - test_serialize_collapse_notification_panel(); + test_serialize_expand_settings_panel(); + test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index e163ad72..3427d640 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -1,19 +1,22 @@ +#include "common.h" + #include #include #include "device_msg.h" #include + static void test_deserialize_clipboard(void) { const unsigned char input[] = { DEVICE_MSG_TYPE_CLIPBOARD, - 0x00, 0x03, // text length + 0x00, 0x00, 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" }; struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); - assert(r == 6); + assert(r == 8); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); @@ -22,7 +25,33 @@ static void test_deserialize_clipboard(void) { device_msg_destroy(&msg); } -int main(void) { +static void test_deserialize_clipboard_big(void) { + unsigned char input[DEVICE_MSG_MAX_SIZE]; + input[0] = DEVICE_MSG_TYPE_CLIPBOARD; + input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; + input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; + input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8; + input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu; + + memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); + + struct device_msg msg; + ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + assert(r == DEVICE_MSG_MAX_SIZE); + + assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); + assert(msg.clipboard.text); + assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); + assert(msg.clipboard.text[0] == 'a'); + + device_msg_destroy(&msg); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_deserialize_clipboard(); + test_deserialize_clipboard_big(); return 0; } diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c index b0950bb0..fcbafc62 100644 --- a/app/tests/test_queue.c +++ b/app/tests/test_queue.c @@ -1,3 +1,5 @@ +#include "common.h" + #include #include "util/queue.h" @@ -32,7 +34,10 @@ static void test_queue(void) { assert(queue_is_empty(&queue)); } -int main(void) { +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + test_queue(); return 0; } diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c index 200e0f63..dfd99658 100644 --- a/app/tests/test_strutil.c +++ b/app/tests/test_strutil.c @@ -1,8 +1,9 @@ +#include "common.h" + #include #include #include #include -#include #include "util/str_util.h" @@ -136,7 +137,7 @@ static void test_strquote(void) { // add '"' at the beginning and the end assert(!strcmp("\"abcde\"", out)); - SDL_free(out); + free(out); } static void test_utf8_truncate(void) { @@ -187,6 +188,55 @@ static void test_parse_integer(void) { 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); @@ -237,7 +287,22 @@ static void test_parse_integer_with_suffix(void) { assert(!ok); } -int main(void) { +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(); @@ -249,6 +314,8 @@ int main(void) { 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 b6ec625d..c977c398 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:4.0.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -19,6 +19,9 @@ allprojects { google() jcenter() } + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" + } } task clean(type: Delete) { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 798814d9..812d060b 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -129,11 +129,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - diff --git a/cross_win32.txt b/cross_win32.txt index d13af0e2..0d8a843a 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -2,11 +2,11 @@ [binaries] name = 'mingw' -c = '/usr/bin/i686-w64-mingw32-gcc' -cpp = '/usr/bin/i686-w64-mingw32-g++' -ar = '/usr/bin/i686-w64-mingw32-ar' -strip = '/usr/bin/i686-w64-mingw32-strip' -pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' +c = 'i686-w64-mingw32-gcc' +cpp = 'i686-w64-mingw32-g++' +ar = 'i686-w64-mingw32-ar' +strip = 'i686-w64-mingw32-strip' +pkgconfig = 'i686-w64-mingw32-pkg-config' [host_machine] system = 'windows' @@ -15,6 +15,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/i686-w64-mingw32' +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' diff --git a/cross_win64.txt b/cross_win64.txt index 09f387e1..6a39c391 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -2,11 +2,11 @@ [binaries] name = 'mingw' -c = '/usr/bin/x86_64-w64-mingw32-gcc' -cpp = '/usr/bin/x86_64-w64-mingw32-g++' -ar = '/usr/bin/x86_64-w64-mingw32-ar' -strip = '/usr/bin/x86_64-w64-mingw32-strip' -pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' +c = 'x86_64-w64-mingw32-gcc' +cpp = 'x86_64-w64-mingw32-g++' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' +pkgconfig = 'x86_64-w64-mingw32-pkg-config' [host_machine] system = 'windows' @@ -15,6 +15,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.2.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.2.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.10/x86_64-w64-mingw32' +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' diff --git a/data/scrcpy-console.bat b/data/scrcpy-console.bat new file mode 100644 index 00000000..b90be29a --- /dev/null +++ b/data/scrcpy-console.bat @@ -0,0 +1,4 @@ +@echo off +scrcpy.exe %* +:: if the exit code is >= 1, then pause +if errorlevel 1 pause diff --git a/data/scrcpy-noconsole.vbs b/data/scrcpy-noconsole.vbs new file mode 100644 index 00000000..d509ad7f --- /dev/null +++ b/data/scrcpy-noconsole.vbs @@ -0,0 +1,7 @@ +strCommand = "cmd /c scrcpy.exe" + +For Each Arg In WScript.Arguments + strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" +Next + +CreateObject("Wscript.Shell").Run strCommand, 0, false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf0..490fda85 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 430dfabc..a4b44297 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-5.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 8e25e6c1..2fe81a7d 100755 --- a/gradlew +++ b/gradlew @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a14..9109989e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" diff --git a/install_release.sh b/install_release.sh new file mode 100755 index 00000000..9158bdd4 --- /dev/null +++ b/install_release.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +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 + +echo "[scrcpy] Downloading prebuilt server..." +wget "$PREBUILT_SERVER_URL" -O scrcpy-server +echo "[scrcpy] Verifying prebuilt server..." +echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check + +echo "[scrcpy] Building client..." +rm -rf "$BUILDDIR" +meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \ + -Dprebuilt_server=scrcpy-server +cd "$BUILDDIR" +ninja + +echo "[scrcpy] Installing (sudo)..." +sudo ninja install diff --git a/meson.build b/meson.build index 412c9c51..2d76f1e9 100644 --- a/meson.build +++ b/meson.build @@ -1,9 +1,10 @@ project('scrcpy', 'c', - version: '1.12.1', - meson_version: '>= 0.37', + version: '1.18', + meson_version: '>= 0.48', default_options: [ 'c_std=c11', 'warning_level=2', + 'b_ndebug=if-release', ]) if get_option('compile_app') diff --git a/meson_options.txt b/meson_options.txt index 4cf4a8bf..66ad5b25 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,8 +1,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') -option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') -option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') +option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile index 892af6c7..d75d0a5c 100644 --- a/prebuilt-deps/Makefile +++ b/prebuilt-deps/Makefile @@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb prepare-ffmpeg-shared-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.2.1-win32-shared.zip \ - 9208255f409410d95147151d7e829b5699bf8d91bfe1e81c3f470f47c2fa66d2 \ - ffmpeg-4.2.1-win32-shared + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-shared.zip \ + 357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 \ + ffmpeg-4.3.1-win32-shared prepare-ffmpeg-dev-win32: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.2.1-win32-dev.zip \ - c3469e6c5f031cbcc8cba88dee92d6548c5c6b6ff14f4097f18f72a92d0d70c4 \ - ffmpeg-4.2.1-win32-dev + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win32-dev.zip \ + 230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b \ + ffmpeg-4.3.1-win32-dev prepare-ffmpeg-shared-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.2.1-win64-shared.zip \ - 55063d3cf750a75485c7bf196031773d81a1b25d0980c7db48ecfc7701a42331 \ - ffmpeg-4.2.1-win64-shared + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-shared.zip \ + dd29b7f92f48dead4dd940492c7509138c0f99db445076d0a597007298a79940 \ + ffmpeg-4.3.1-win64-shared prepare-ffmpeg-dev-win64: - @./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.2.1-win64-dev.zip \ - 5af393be5f25c0a71aa29efce768e477c35347f7f8e0d9696767d5b9d405b74e \ - ffmpeg-4.2.1-win64-dev + @./prepare-dep https://github.com/Genymobile/scrcpy/releases/download/v1.16/ffmpeg-4.3.1-win64-dev.zip \ + 2e8038242cf8e1bd095c2978f196ff0462b122cc6ef7e74626a6af15459d8b81 \ + ffmpeg-4.3.1-win64-dev prepare-sdl2: - @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.10-mingw.tar.gz \ - a90a7cddaec4996f4d7be6d80c57ec69b062e132bffc513965f99217f603274a \ - SDL2-2.0.10 + @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.14-mingw.tar.gz \ + 405eaff3eb18f2e08fe669ef9e63bc9a8710b7d343756f238619761e9b60407d \ + SDL2-2.0.14 prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.5-windows.zip \ - 2df06160056ec9a84c7334af2a1e42740befbb1a2e34370e7af544a2cc78152c \ + @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.2-windows.zip \ + d560cb8ded83ae04763b94632673481f14843a5969256569623cfeac82db4ba5 \ platform-tools diff --git a/prebuilt-deps/prepare-dep b/prebuilt-deps/prepare-dep index 34ddcbf5..f152e6cf 100755 --- a/prebuilt-deps/prepare-dep +++ b/prebuilt-deps/prepare-dep @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e url="$1" sum="$2" diff --git a/Makefile.CrossWindows b/release.mk similarity index 56% rename from Makefile.CrossWindows rename to release.mk index 2b30dcb5..2a026135 100644 --- a/Makefile.CrossWindows +++ b/release.mk @@ -9,21 +9,20 @@ # the server to the device. .PHONY: default clean \ + test \ build-server \ prepare-deps-win32 prepare-deps-win64 \ - build-win32 build-win32-noconsole \ - build-win64 build-win64-noconsole \ + build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ - sums release + release GRADLE ?= ./gradlew +TEST_BUILD_DIR := build-test SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 -WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole WIN64_BUILD_DIR := build-win64 -WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole DIST := dist WIN32_TARGET_DIR := scrcpy-win32 @@ -33,19 +32,35 @@ VERSION := $(shell git describe --tags --always) WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip -release: clean zip-win32 zip-win64 sums - @echo "Windows archives generated in $(DIST)/" +RELEASE_DIR := release-$(VERSION) + +release: clean test build-server zip-win32 zip-win64 + mkdir -p "$(RELEASE_DIR)" + cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ + "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" + cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)" + cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)" + cd "$(RELEASE_DIR)" && \ + sha256sum "scrcpy-server-$(VERSION)" \ + "scrcpy-win32-$(VERSION).zip" \ + "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt + @echo "Release generated in $(RELEASE_DIR)/" clean: $(GRADLE) clean - rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \ - "$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)" + rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ + "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" + +test: + [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ + meson "$(TEST_BUILD_DIR)" -Db_sanitize=address ) + ninja -C "$(TEST_BUILD_DIR)" + $(GRADLE) -p server check build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ - meson "$(SERVER_BUILD_DIR)" \ - --buildtype release -Dcompile_app=false ) - ninja -C "$(SERVER_BUILD_DIR)" + meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) + ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: -$(MAKE) -C prebuilt-deps prepare-win32 @@ -60,17 +75,6 @@ build-win32: prepare-deps-win32 -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" -build-win32-noconsole: prepare-deps-win32 - [ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \ - meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \ - --cross-file cross_win32.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ - -Dcompile_server=false \ - -Dwindows_noconsole=true \ - -Dportable=true ) - ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)" - prepare-deps-win64: -$(MAKE) -C prebuilt-deps prepare-win64 @@ -84,46 +88,37 @@ build-win64: prepare-deps-win64 -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" -build-win64-noconsole: prepare-deps-win64 - [ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \ - meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \ - --cross-file cross_win64.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ - -Dcompile_server=false \ - -Dwindows_noconsole=true \ - -Dportable=true ) - ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)" - -dist-win32: build-server build-win32 build-win32-noconsole +dist-win32: build-server build-win32 mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" + cp data/scrcpy-noconsole.vbs "$(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)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 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.10/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.14/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" -dist-win64: build-server build-win64 build-win64-noconsole +dist-win64: build-server build-win64 mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp prebuilt-deps/ffmpeg-4.2.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" + cp data/scrcpy-noconsole.vbs "$(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)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 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.10/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp prebuilt-deps/SDL2-2.0.14/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ @@ -132,7 +127,3 @@ zip-win32: dist-win32 zip-win64: dist-win64 cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ zip -r "../$(WIN64_TARGET)" . - -sums: - cd "$(DIST)"; \ - sha256sum *.zip > SHA256SUMS.txt diff --git a/release.sh b/release.sh index 4c5afbf1..51ce2e38 100755 --- a/release.sh +++ b/release.sh @@ -1,44 +1,2 @@ #!/bin/bash -set -e - -# test locally -TESTDIR=build_test -rm -rf "$TESTDIR" -# run client tests with ASAN enabled -meson "$TESTDIR" -Db_sanitize=address -ninja -C"$TESTDIR" test - -# test server -GRADLE=${GRADLE:-./gradlew} -$GRADLE -p server check - -BUILDDIR=build_release -rm -rf "$BUILDDIR" -meson "$BUILDDIR" --buildtype release --strip -Db_lto=true -cd "$BUILDDIR" -ninja -cd - - -# build Windows releases -make -f Makefile.CrossWindows - -# the generated server must be the same everywhere -cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server -cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server - -# get version name -TAG=$(git describe --tags --always) - -# create release directory -mkdir -p "release-$TAG" -cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG" -cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/" -cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/" - -# generate checksums -cd "release-$TAG" -sha256sum "scrcpy-server-$TAG" \ - "scrcpy-win32-$TAG.zip" \ - "scrcpy-win64-$TAG.zip" > SHA256SUMS.txt - -echo "Release generated in release-$TAG/" +make -f release.mk diff --git a/run b/run index bfb499ae..628c5c7e 100755 --- a/run +++ b/run @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Run scrcpy generated in the specified BUILDDIR. # # This provides the same feature as "ninja run", except that it is possible to diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh index f3130ee9..e93b639f 100755 --- a/scripts/run-scrcpy.sh +++ b/scripts/run-scrcpy.sh @@ -1,2 +1,2 @@ -#!/bin/bash +#!/usr/bin/env bash SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" diff --git a/server/build.gradle b/server/build.gradle index 539a97b8..f088ba9d 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 29 - versionCode 14 - versionName "1.12.1" + targetSdkVersion 30 + versionCode 11800 + versionName "1.18" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -20,7 +20,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' } apply from: "$project.rootDir/config/android-checkstyle.gradle" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index c117d89c..302d3aaa 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # This script generates the scrcpy binary "manually" (without gradle). # @@ -12,10 +12,10 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.12.1 +SCRCPY_VERSION_NAME=1.18 -PLATFORM=${ANDROID_PLATFORM:-29} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-29.0.2} +PLATFORM=${ANDROID_PLATFORM:-30} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" @@ -42,6 +42,8 @@ echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" "$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ android/view/IRotationWatcher.aidl +"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \ + android/content/IOnPrimaryClipChangedListener.aidl echo "Compiling java sources..." cd ../java @@ -55,6 +57,7 @@ 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 diff --git a/server/meson.build b/server/meson.build index 4ba481d5..984daf3b 100644 --- a/server/meson.build +++ b/server/meson.build @@ -3,7 +3,9 @@ prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', - build_always: true, # gradle is responsible for tracking source changes + # gradle is responsible for tracking source changes + build_by_default: true, + build_always_stale: true, output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, diff --git a/server/scripts/build-wrapper.sh b/server/scripts/build-wrapper.sh index f55e1ea4..7e16dc94 100755 --- a/server/scripts/build-wrapper.sh +++ b/server/scripts/build-wrapper.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Wrapper script to invoke gradle from meson set -e diff --git a/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl new file mode 100644 index 00000000..46d7f7ca --- /dev/null +++ b/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2008, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +/** + * {@hide} + */ +oneway interface IOnPrimaryClipChangedListener { + void dispatchPrimaryClipChanged(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java new file mode 100644 index 00000000..ec61a1c0 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -0,0 +1,191 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Base64; + +import java.io.File; +import java.io.IOException; + +/** + * Handle the cleanup of scrcpy, even if the main process is killed. + *

+ * This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process). + */ +public final class CleanUp { + + public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; + + // A simple struct to be passed from the main process to the cleanup process + public static class Config implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public Config createFromParcel(Parcel in) { + return new Config(in); + } + + @Override + public Config[] newArray(int size) { + return new Config[size]; + } + }; + + private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; + private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; + private static final int FLAG_POWER_OFF_SCREEN = 4; + + private int displayId; + + // Restore the value (between 0 and 7), -1 to not restore + // + private int restoreStayOn = -1; + + private boolean disableShowTouches; + private boolean restoreNormalPowerMode; + private boolean powerOffScreen; + + public Config() { + // Default constructor, the fields are initialized by CleanUp.configure() + } + + protected Config(Parcel in) { + displayId = in.readInt(); + restoreStayOn = in.readInt(); + byte options = in.readByte(); + disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; + restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; + powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(displayId); + dest.writeInt(restoreStayOn); + byte options = 0; + if (disableShowTouches) { + options |= FLAG_DISABLE_SHOW_TOUCHES; + } + if (restoreNormalPowerMode) { + options |= FLAG_RESTORE_NORMAL_POWER_MODE; + } + if (powerOffScreen) { + options |= FLAG_POWER_OFF_SCREEN; + } + dest.writeByte(options); + } + + private boolean hasWork() { + return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; + } + + @Override + public int describeContents() { + return 0; + } + + byte[] serialize() { + Parcel parcel = Parcel.obtain(); + writeToParcel(parcel, 0); + byte[] bytes = parcel.marshall(); + parcel.recycle(); + return bytes; + } + + static Config deserialize(byte[] bytes) { + Parcel parcel = Parcel.obtain(); + parcel.unmarshall(bytes, 0, bytes.length); + parcel.setDataPosition(0); + return CREATOR.createFromParcel(parcel); + } + + static Config fromBase64(String base64) { + byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); + return deserialize(bytes); + } + + String toBase64() { + byte[] bytes = serialize(); + return Base64.encodeToString(bytes, Base64.NO_WRAP); + } + } + + private CleanUp() { + // not instantiable + } + + public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) + throws IOException { + Config config = new Config(); + config.displayId = displayId; + config.disableShowTouches = disableShowTouches; + config.restoreStayOn = restoreStayOn; + config.restoreNormalPowerMode = restoreNormalPowerMode; + config.powerOffScreen = powerOffScreen; + + if (config.hasWork()) { + startProcess(config); + } else { + // There is no additional clean up to do when scrcpy dies + unlinkSelf(); + } + } + + private static void startProcess(Config config) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; + + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("CLASSPATH", SERVER_PATH); + builder.start(); + } + + private static void unlinkSelf() { + try { + new File(SERVER_PATH).delete(); + } catch (Exception e) { + Ln.e("Could not unlink server", e); + } + } + + public static void main(String... args) { + unlinkSelf(); + + try { + // Wait for the server to die + System.in.read(); + } catch (IOException e) { + // Expected when the server is dead + } + + Ln.i("Cleaning up"); + + Config config = Config.fromBase64(args[0]); + + if (config.disableShowTouches || config.restoreStayOn != -1) { + ServiceManager serviceManager = new ServiceManager(); + try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) { + if (config.disableShowTouches) { + Ln.i("Disabling \"show touches\""); + settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0"); + } + if (config.restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); + } + } + } + + if (Device.isScreenOn()) { + if (config.powerOffScreen) { + Ln.i("Power off screen"); + Device.powerOffScreen(config.displayId); + } else if (config.restoreNormalPowerMode) { + Ln.i("Restoring normal power mode"); + Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java new file mode 100644 index 00000000..1897bda3 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -0,0 +1,112 @@ +package com.genymobile.scrcpy; + +import java.util.ArrayList; +import java.util.List; + +public class CodecOption { + private String key; + private Object value; + + public CodecOption(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public static List parse(String codecOptions) { + if ("-".equals(codecOptions)) { + return null; + } + + List result = new ArrayList<>(); + + boolean escape = false; + StringBuilder buf = new StringBuilder(); + + for (char c : codecOptions.toCharArray()) { + switch (c) { + case '\\': + if (escape) { + buf.append('\\'); + escape = false; + } else { + escape = true; + } + break; + case ',': + if (escape) { + buf.append(','); + escape = false; + } else { + // This comma is a separator between codec options + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + // Clear buf + buf.setLength(0); + } + break; + default: + buf.append(c); + break; + } + } + + if (buf.length() > 0) { + String codecOption = buf.toString(); + result.add(parseOption(codecOption)); + } + + return result; + } + + private static CodecOption parseOption(String option) { + int equalSignIndex = option.indexOf('='); + if (equalSignIndex == -1) { + throw new IllegalArgumentException("'=' expected"); + } + String keyAndType = option.substring(0, equalSignIndex); + if (keyAndType.length() == 0) { + throw new IllegalArgumentException("Key may not be null"); + } + + String key; + String type; + + int colonIndex = keyAndType.indexOf(':'); + if (colonIndex != -1) { + key = keyAndType.substring(0, colonIndex); + type = keyAndType.substring(colonIndex + 1); + } else { + key = keyAndType; + type = "int"; // assume int by default + } + + Object value; + String valueString = option.substring(equalSignIndex + 1); + switch (type) { + case "int": + value = Integer.parseInt(valueString); + break; + case "long": + value = Long.parseLong(valueString); + break; + case "float": + value = Float.parseFloat(valueString); + break; + case "string": + value = valueString; + break; + default: + throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type); + } + + return new CodecOption(key, value); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 195b04bf..f8edd53c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -11,11 +11,12 @@ public final class ControlMessage { public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; - public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; - public static final int TYPE_GET_CLIPBOARD = 7; - public static final int TYPE_SET_CLIPBOARD = 8; - public static final int TYPE_SET_SCREEN_POWER_MODE = 9; - public static final int TYPE_ROTATE_DEVICE = 10; + public static final int TYPE_EXPAND_SETTINGS_PANEL = 6; + public static final int TYPE_COLLAPSE_PANELS = 7; + public static final int TYPE_GET_CLIPBOARD = 8; + public static final int TYPE_SET_CLIPBOARD = 9; + public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_ROTATE_DEVICE = 11; private int type; private String text; @@ -28,15 +29,18 @@ public final class ControlMessage { private Position position; private int hScroll; private int vScroll; + private boolean paste; + private int repeat; private ControlMessage() { } - public static ControlMessage createInjectKeycode(int action, int keycode, int metaState) { + public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_KEYCODE; msg.action = action; msg.keycode = keycode; + msg.repeat = repeat; msg.metaState = metaState; return msg; } @@ -68,10 +72,18 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text) { + public static ControlMessage createBackOrScreenOn(int action) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_BACK_OR_SCREEN_ON; + msg.action = action; + return msg; + } + + public static ControlMessage createSetClipboard(String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; msg.text = text; + msg.paste = paste; return msg; } @@ -134,4 +146,12 @@ public final class ControlMessage { public int getVScroll() { return vScroll; } + + public boolean getPaste() { + return paste; + } + + public int getRepeat() { + return repeat; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 726b5659..e4ab8402 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -8,19 +8,20 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { - private static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 9; - private static final int INJECT_MOUSE_EVENT_PAYLOAD_LENGTH = 17; - private static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 21; - private static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; - private static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; + static final int BACK_OR_SCREEN_ON_LENGTH = 1; + static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; - public static final int TEXT_MAX_LENGTH = 300; - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; - private static final int RAW_BUFFER_SIZE = 1024; + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k - private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes + public static final int INJECT_TEXT_MAX_LENGTH = 300; + + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - private final byte[] textBuffer = new byte[CLIPBOARD_TEXT_MAX_LENGTH]; public ControlMessageReader() { // invariant: the buffer is always in "get" mode @@ -66,15 +67,18 @@ public class ControlMessageReader { case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; + case ControlMessage.TYPE_BACK_OR_SCREEN_ON: + msg = parseBackOrScreenOnEvent(); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: msg = parseSetScreenPowerMode(); break; - case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); @@ -98,20 +102,23 @@ public class ControlMessageReader { } int action = toUnsigned(buffer.get()); int keycode = buffer.getInt(); + int repeat = buffer.getInt(); int metaState = buffer.getInt(); - return ControlMessage.createInjectKeycode(action, keycode, metaState); + return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } private String parseString() { - if (buffer.remaining() < 2) { + if (buffer.remaining() < 4) { return null; } - int len = toUnsigned(buffer.getShort()); + int len = buffer.getInt(); if (buffer.remaining() < len) { return null; } - buffer.get(textBuffer, 0, len); - return new String(textBuffer, 0, len, StandardCharsets.UTF_8); + int position = buffer.position(); + // Move the buffer position to consume the text + buffer.position(position + len); + return new String(rawBuffer, position, len, StandardCharsets.UTF_8); } private ControlMessage parseInjectText() { @@ -122,7 +129,6 @@ public class ControlMessageReader { return ControlMessage.createInjectText(text); } - @SuppressWarnings("checkstyle:MagicNumber") private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; @@ -148,12 +154,24 @@ public class ControlMessageReader { return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); } + private ControlMessage parseBackOrScreenOnEvent() { + if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { + return null; + } + int action = toUnsigned(buffer.get()); + return ControlMessage.createBackOrScreenOn(action); + } + private ControlMessage parseSetClipboard() { + if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { + return null; + } + boolean paste = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text); + return ControlMessage.createSetClipboard(text, paste); } private ControlMessage parseSetScreenPowerMode() { @@ -172,12 +190,10 @@ public class ControlMessageReader { return new Position(x, y, screenWidth, screenHeight); } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(short value) { return value & 0xffff; } - @SuppressWarnings("checkstyle:MagicNumber") private static int toUnsigned(byte value) { return value & 0xff; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index dc0fa67b..92986241 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,19 +1,22 @@ package com.genymobile.scrcpy; -import com.genymobile.scrcpy.wrappers.InputManager; - +import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; -import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class Controller { - private static final int DEVICE_ID_VIRTUAL = -1; + private static final int DEFAULT_DEVICE_ID = 0; + + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private final Device device; private final DesktopConnection connection; @@ -26,6 +29,8 @@ public class Controller { private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; + private boolean keepPowerModeOff; + public Controller(Device device, DesktopConnection connection) { this.device = device; this.connection = connection; @@ -40,18 +45,17 @@ public class Controller { MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); coords.orientation = 0; - coords.size = 1; + coords.size = 0; pointerProperties[i] = props; pointerCoords[i] = coords; } } - @SuppressWarnings("checkstyle:MagicNumber") public void control() throws IOException { // on start, power on the device - if (!device.isScreenOn()) { - injectKeycode(KeyEvent.KEYCODE_POWER); + if (!Device.isScreenOn()) { + device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -76,46 +80,71 @@ public class Controller { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - injectKeycode(msg.getAction(), msg.getKeycode(), msg.getMetaState()); + if (device.supportsInputEvents()) { + injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); + } break; case ControlMessage.TYPE_INJECT_TEXT: - injectText(msg.getText()); + if (device.supportsInputEvents()) { + injectText(msg.getText()); + } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + if (device.supportsInputEvents()) { + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + if (device.supportsInputEvents()) { + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - pressBackOrTurnScreenOn(); + if (device.supportsInputEvents()) { + pressBackOrTurnScreenOn(msg.getAction()); + } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: - device.expandNotificationPanel(); + Device.expandNotificationPanel(); break; - case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: - device.collapsePanels(); + case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: + Device.expandSettingsPanel(); + break; + case ControlMessage.TYPE_COLLAPSE_PANELS: + Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: - String clipboardText = device.getClipboardText(); - sender.pushClipboardText(clipboardText); + String clipboardText = Device.getClipboardText(); + if (clipboardText != null) { + sender.pushClipboardText(clipboardText); + } break; case ControlMessage.TYPE_SET_CLIPBOARD: - device.setClipboardText(msg.getText()); + setClipboard(msg.getText(), msg.getPaste()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - device.setScreenPowerMode(msg.getAction()); + if (device.supportsInputEvents()) { + int mode = msg.getAction(); + boolean setPowerModeOk = Device.setScreenPowerMode(mode); + if (setPowerModeOk) { + keepPowerModeOff = mode == Device.POWER_MODE_OFF; + Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + } + } break; case ControlMessage.TYPE_ROTATE_DEVICE: - device.rotateDevice(); + Device.rotateDevice(); break; default: // do nothing } } - private boolean injectKeycode(int action, int keycode, int metaState) { - return injectKeyEvent(action, keycode, 0, metaState); + private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { + if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + schedulePowerModeOff(); + } + return device.injectKeyEvent(action, keycode, repeat, metaState); } private boolean injectChar(char c) { @@ -126,7 +155,7 @@ public class Controller { return false; } for (KeyEvent event : events) { - if (!injectEvent(event)) { + if (!device.injectEvent(event)) { return false; } } @@ -150,7 +179,7 @@ public class Controller { Point point = device.getPhysicalPoint(position); if (point == null) { - // ignore event + Ln.w("Ignore touch event, it was generated for a different device size"); return false; } @@ -179,10 +208,18 @@ public class Controller { } } + // Right-click and middle-click only work if the source is a mouse + boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; + int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; + if (source != InputDevice.SOURCE_MOUSE) { + // Buttons must not be set for touch events + buttons = 0; + } + MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_TOUCHSCREEN, 0); - return injectEvent(event); + .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, + 0); + return device.injectEvent(event); } private boolean injectScroll(Position position, int hScroll, int vScroll) { @@ -203,28 +240,53 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, - InputDevice.SOURCE_MOUSE, 0); - return injectEvent(event); + .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, + InputDevice.SOURCE_TOUCHSCREEN, 0); + return device.injectEvent(event); } - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { - long now = SystemClock.uptimeMillis(); - KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, - InputDevice.SOURCE_KEYBOARD); - return injectEvent(event); + /** + * Schedule a call to set power mode to off after a small delay. + */ + private static void schedulePowerModeOff() { + EXECUTOR.schedule(new Runnable() { + @Override + public void run() { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); + } + }, 200, TimeUnit.MILLISECONDS); } - private boolean injectKeycode(int keyCode) { - return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); + private boolean pressBackOrTurnScreenOn(int action) { + if (Device.isScreenOn()) { + return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0); + } + + // Screen is off + // Only press POWER on ACTION_DOWN + if (action != KeyEvent.ACTION_DOWN) { + // do nothing, + return true; + } + + if (keepPowerModeOff) { + schedulePowerModeOff(); + } + return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); } - private boolean injectEvent(InputEvent event) { - return device.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); - } + private boolean setClipboard(String text, boolean paste) { + boolean ok = device.setClipboardText(text); + if (ok) { + Ln.i("Device clipboard set"); + } - private boolean pressBackOrTurnScreenOn() { - int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; - return injectKeycode(keycode); + // On Android >= 7, also press the PASTE key if requested + if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE); + } + + return ok; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index a725d83d..0ec43040 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -84,7 +84,6 @@ public final class DesktopConnection implements Closeable { controlSocket.close(); } - @SuppressWarnings("checkstyle:MagicNumber") private void send(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9448098a..3e71fe9c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,37 +1,78 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ClipboardManager; +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; +import android.content.IOnPrimaryClipChangedListener; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; -import android.os.RemoteException; +import android.os.SystemClock; import android.view.IRotationWatcher; +import android.view.InputDevice; import android.view.InputEvent; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import java.util.concurrent.atomic.AtomicBoolean; public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; + public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; + public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; + + private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); + public interface RotationListener { void onRotationChanged(int rotation); } - private final ServiceManager serviceManager = new ServiceManager(); + public interface ClipboardListener { + void onClipboardTextChanged(String text); + } private ScreenInfo screenInfo; private RotationListener rotationListener; + private ClipboardListener clipboardListener; + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + + /** + * Logical display identifier + */ + private final int displayId; + + /** + * The surface flinger layer stack associated with this logical display + */ + private final int layerStack; + + private final boolean supportsInputEvents; public Device(Options options) { - screenInfo = computeScreenInfo(options.getCrop(), options.getMaxSize()); - registerRotationWatcher(new IRotationWatcher.Stub() { + displayId = options.getDisplayId(); + DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); + throw new InvalidDisplayIdException(displayId, displayIds); + } + + int displayInfoFlags = displayInfo.getFlags(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockedVideoOrientation()); + layerStack = displayInfo.getLayerStack(); + + SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override - public void onRotationChanged(int rotation) throws RemoteException { + public void onRotationChanged(int rotation) { synchronized (Device.this) { - screenInfo = screenInfo.withRotation(rotation); + screenInfo = screenInfo.withDeviceRotation(rotation); // notify if (rotationListener != null) { @@ -39,143 +80,206 @@ public final class Device { } } } - }); + }, displayId); + + if (options.getControl()) { + // If control is enabled, synchronize Android clipboard to the computer automatically + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + synchronized (Device.this) { + if (clipboardListener != null) { + String text = getClipboardText(); + if (text != null) { + clipboardListener.onClipboardTextChanged(text); + } + } + } + } + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } + } + + if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } + + // main display or any display on Android >= Q + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } - private ScreenInfo computeScreenInfo(Rect crop, int maxSize) { - DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); - boolean rotated = (displayInfo.getRotation() & 1) != 0; - Size deviceSize = displayInfo.getSize(); - Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); - if (crop != null) { - if (rotated) { - // the crop (provided by the user) is expressed in the natural orientation - crop = flipRect(crop); - } - if (!contentRect.intersect(crop)) { - // intersect() changes contentRect so that it is intersected with crop - Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); - contentRect = new Rect(); // empty - } - } - - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); - return new ScreenInfo(contentRect, videoSize, rotated); - } - - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; - } - - @SuppressWarnings("checkstyle:MagicNumber") - private static Size computeVideoSize(int w, int h, int maxSize) { - // Compute the video size and the padding of the content inside this video. - // Principle: - // - scale down the great side of the screen to maxSize (if necessary); - // - scale down the other side so that the aspect ratio is preserved; - // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - w &= ~7; // in case it's not a multiple of 8 - h &= ~7; - if (maxSize > 0) { - if (BuildConfig.DEBUG && maxSize % 8 != 0) { - throw new AssertionError("Max size must be a multiple of 8"); - } - boolean portrait = h > w; - int major = portrait ? h : w; - int minor = portrait ? w : h; - if (major > maxSize) { - int minorExact = minor * maxSize / major; - // +4 to round the value to the nearest multiple of 8 - minor = (minorExact + 4) & ~7; - major = maxSize; - } - w = portrait ? minor : major; - h = portrait ? major : minor; - } - return new Size(w, h); + public int getLayerStack() { + return layerStack; } public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization - Size videoSize = screenInfo.getVideoSize(); - Size clientVideoSize = position.getScreenSize(); - if (!videoSize.equals(clientVideoSize)) { + + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); + + int reverseVideoRotation = screenInfo.getReverseVideoRotation(); + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(reverseVideoRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!unlockedVideoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); - Point point = position.getPoint(); - int scaledX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int scaledY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); - return new Point(scaledX, scaledY); + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); + return new Point(convertedX, convertedY); } public static String getDeviceName() { return Build.MODEL; } - public boolean injectInputEvent(InputEvent inputEvent, int mode) { - return serviceManager.getInputManager().injectInputEvent(inputEvent, mode); + public static boolean supportsInputEvents(int displayId) { + return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; } - public boolean isScreenOn() { - return serviceManager.getPowerManager().isScreenOn(); + public boolean supportsInputEvents() { + return supportsInputEvents; } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { - serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + public static boolean injectEvent(InputEvent inputEvent, int displayId) { + if (!supportsInputEvents(displayId)) { + throw new AssertionError("Could not inject input event if !supportsInputEvents()"); + } + + if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { + return false; + } + + return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + } + + public boolean injectEvent(InputEvent event) { + return injectEvent(event, displayId); + } + + public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) { + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, + InputDevice.SOURCE_KEYBOARD); + return injectEvent(event, displayId); + } + + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { + return injectKeyEvent(action, keyCode, repeat, metaState, displayId); + } + + public static boolean pressReleaseKeycode(int keyCode, int displayId) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId); + } + + public boolean pressReleaseKeycode(int keyCode) { + return pressReleaseKeycode(keyCode, displayId); + } + + public static boolean isScreenOn() { + return SERVICE_MANAGER.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } - public void expandNotificationPanel() { - serviceManager.getStatusBarManager().expandNotificationsPanel(); + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { + this.clipboardListener = clipboardListener; } - public void collapsePanels() { - serviceManager.getStatusBarManager().collapsePanels(); + public static void expandNotificationPanel() { + SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); } - public String getClipboardText() { - CharSequence s = serviceManager.getClipboardManager().getText(); + public static void expandSettingsPanel() { + SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); + } + + public static void collapsePanels() { + SERVICE_MANAGER.getStatusBarManager().collapsePanels(); + } + + public static String getClipboardText() { + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (clipboardManager == null) { + return null; + } + CharSequence s = clipboardManager.getText(); if (s == null) { return null; } return s.toString(); } - public void setClipboardText(String text) { - serviceManager.getClipboardManager().setText(text); - Ln.i("Device clipboard set"); + public boolean setClipboardText(String text) { + ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (clipboardManager == null) { + return false; + } + + String currentClipboard = getClipboardText(); + if (currentClipboard != null && currentClipboard.equals(text)) { + // The clipboard already contains the requested text. + // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause + // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this + // problem, do not explicitly set the clipboard text if it already contains the expected content. + return false; + } + + isSettingClipboard.set(true); + boolean ok = clipboardManager.setText(text); + isSettingClipboard.set(false); + return ok; } /** - * @param mode one of the {@code SCREEN_POWER_MODE_*} constants + * @param mode one of the {@code POWER_MODE_*} constants */ - public void setScreenPowerMode(int mode) { + public static boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); - return; + return false; } - SurfaceControl.setDisplayPowerMode(d, mode); - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + return SurfaceControl.setDisplayPowerMode(d, mode); + } + + public static boolean powerOffScreen(int displayId) { + if (!isScreenOn()) { + return true; + } + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId); } /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public void rotateDevice() { - WindowManager wm = serviceManager.getWindowManager(); + public static void rotateDevice() { + WindowManager wm = SERVICE_MANAGER.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(); @@ -192,7 +296,7 @@ public final class Device { } } - static Rect flipRect(Rect crop) { - return new Rect(crop.top, crop.left, crop.bottom, crop.right); + public static ContentProvider createSettingsProvider() { + return SERVICE_MANAGER.getActivityManager().createSettingsProvider(); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java index e2a3a1a2..15d91a35 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java @@ -7,13 +7,12 @@ import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { - public static final int CLIPBOARD_TEXT_MAX_LENGTH = 4093; - private static final int MAX_EVENT_SIZE = CLIPBOARD_TEXT_MAX_LENGTH + 3; + private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes - private final byte[] rawBuffer = new byte[MAX_EVENT_SIZE]; + private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); - @SuppressWarnings("checkstyle:MagicNumber") public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); buffer.put((byte) DeviceMessage.TYPE_CLIPBOARD); @@ -22,7 +21,7 @@ public class DeviceMessageWriter { String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); - buffer.putShort((short) len); + buffer.putInt(len); buffer.put(raw, 0, len); output.write(rawBuffer, 0, buffer.position()); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java index 639869b5..4b8036f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java @@ -1,12 +1,24 @@ package com.genymobile.scrcpy; public final class DisplayInfo { + private final int displayId; private final Size size; private final int rotation; + private final int layerStack; + private final int flags; - public DisplayInfo(Size size, int rotation) { + public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; + + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + this.displayId = displayId; this.size = size; this.rotation = rotation; + this.layerStack = layerStack; + this.flags = flags; + } + + public int getDisplayId() { + return displayId; } public Size getSize() { @@ -16,5 +28,13 @@ public final class DisplayInfo { public int getRotation() { return rotation; } + + public int getLayerStack() { + return layerStack; + } + + public int getFlags() { + return flags; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java new file mode 100644 index 00000000..81e3b903 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy; + +public class InvalidDisplayIdException extends RuntimeException { + + private final int displayId; + private final int[] availableDisplayIds; + + public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) { + super("There is no display having id " + displayId); + this.displayId = displayId; + this.availableDisplayIds = availableDisplayIds; + } + + public int getDisplayId() { + return displayId; + } + + public int[] getAvailableDisplayIds() { + return availableDisplayIds; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java new file mode 100644 index 00000000..1efd2989 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; + +public class InvalidEncoderException extends RuntimeException { + + private final String name; + private final MediaCodecInfo[] availableEncoders; + + public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) { + super("There is no encoder having name '" + name + '"'); + this.name = name; + this.availableEncoders = availableEncoders; + } + + public String getName() { + return name; + } + + public MediaCodecInfo[] getAvailableEncoders() { + return availableEncoders; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Ln.java b/server/src/main/java/com/genymobile/scrcpy/Ln.java index 26f13a56..061cda95 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -12,17 +12,35 @@ public final class Ln { private static final String PREFIX = "[server] "; enum Level { - DEBUG, INFO, WARN, ERROR + VERBOSE, DEBUG, INFO, WARN, ERROR } - private static final Level THRESHOLD = BuildConfig.DEBUG ? Level.DEBUG : Level.INFO; + private static Level threshold = Level.INFO; private Ln() { // not instantiable } + /** + * Initialize the log level. + *

+ * Must be called before starting any new thread. + * + * @param level the log level + */ + public static void initLogLevel(Level level) { + threshold = level; + } + public static boolean isEnabled(Level level) { - return level.ordinal() >= THRESHOLD.ordinal(); + return level.ordinal() >= threshold.ordinal(); + } + + public static void v(String message) { + if (isEnabled(Level.VERBOSE)) { + Log.v(TAG, message); + System.out.println(PREFIX + "VERBOSE: " + message); + } } public static void d(String message) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 5b993f30..cf11df0f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -3,13 +3,29 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public class Options { + private Ln.Level logLevel; private int maxSize; private int bitRate; private int maxFps; + private int lockedVideoOrientation; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control; + private int displayId; + private boolean showTouches; + private boolean stayAwake; + private String codecOptions; + private String encoderName; + private boolean powerOffScreenOnClose; + + public Ln.Level getLogLevel() { + return logLevel; + } + + public void setLogLevel(Ln.Level logLevel) { + this.logLevel = logLevel; + } public int getMaxSize() { return maxSize; @@ -35,6 +51,14 @@ public class Options { this.maxFps = maxFps; } + public int getLockedVideoOrientation() { + return lockedVideoOrientation; + } + + public void setLockedVideoOrientation(int lockedVideoOrientation) { + this.lockedVideoOrientation = lockedVideoOrientation; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -66,4 +90,52 @@ public class Options { public void setControl(boolean control) { this.control = control; } + + public int getDisplayId() { + return displayId; + } + + public void setDisplayId(int displayId) { + this.displayId = displayId; + } + + public boolean getShowTouches() { + return showTouches; + } + + public void setShowTouches(boolean showTouches) { + this.showTouches = showTouches; + } + + public boolean getStayAwake() { + return stayAwake; + } + + public void setStayAwake(boolean stayAwake) { + this.stayAwake = stayAwake; + } + + public String getCodecOptions() { + return codecOptions; + } + + public void setCodecOptions(String codecOptions) { + this.codecOptions = codecOptions; + } + + public String getEncoderName() { + return encoderName; + } + + public void setEncoderName(String encoderName) { + this.encoderName = encoderName; + } + + public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { + this.powerOffScreenOnClose = powerOffScreenOnClose; + } + + public boolean getPowerOffScreenOnClose() { + return this.powerOffScreenOnClose; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index b46d2f73..e9b6d8a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -23,6 +23,19 @@ public class Position { return screenSize; } + public Position rotate(int rotation) { + switch (rotation) { + case 1: + return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); + case 2: + return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); + case 3: + return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); + default: + return this; + } + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index c9a37f84..2f7109c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -5,6 +5,7 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; @@ -13,33 +14,35 @@ import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms + private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + private String encoderName; + private List codecOptions; private int bitRate; private int maxFps; - private int iFrameInterval; private boolean sendFrameMeta; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) { + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; - this.iFrameInterval = iFrameInterval; - } - - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps) { - this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL); + this.codecOptions = codecOptions; + this.encoderName = encoderName; } @Override @@ -53,21 +56,40 @@ public class ScreenEncoder implements Device.RotationListener { public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); - Workarounds.fillAppInfo(); - MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval); + try { + internalStreamScreen(device, fd); + } catch (NullPointerException e) { + // Retry with workarounds enabled: + // + // + Ln.d("Applying workarounds to avoid NullPointerException"); + Workarounds.fillAppInfo(); + internalStreamScreen(device, fd); + } + } + + private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { + MediaFormat format = createFormat(bitRate, maxFps, codecOptions); device.setRotationListener(this); boolean alive; try { do { - MediaCodec codec = createCodec(); + MediaCodec codec = createCodec(encoderName); IBinder display = createDisplay(); - Rect contentRect = device.getScreenInfo().getContentRect(); - Rect videoRect = device.getScreenInfo().getVideoSize().toRect(); + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + // include the locked video orientation + Rect videoRect = screenInfo.getVideoSize().toRect(); + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); - setDisplaySurface(display, surface, contentRect, videoRect); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); @@ -134,33 +156,81 @@ public class ScreenEncoder implements Device.RotationListener { IO.writeFully(fd, headerBuffer); } - private static MediaCodec createCodec() throws IOException { - return MediaCodec.createEncoderByType("video/avc"); + private static MediaCodecInfo[] listEncoders() { + List result = new ArrayList<>(); + MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo codecInfo : list.getCodecInfos()) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { + result.add(codecInfo); + } + } + return result.toArray(new MediaCodecInfo[result.size()]); } - @SuppressWarnings("checkstyle:MagicNumber") - private static MediaFormat createFormat(int bitRate, int maxFps, int iFrameInterval) { + private static MediaCodec createCodec(String encoderName) throws IOException { + if (encoderName != null) { + Ln.d("Creating encoder by name: '" + encoderName + "'"); + try { + return MediaCodec.createByCodecName(encoderName); + } catch (IllegalArgumentException e) { + MediaCodecInfo[] encoders = listEncoders(); + throw new InvalidEncoderException(encoderName, encoders); + } + } + MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + Ln.d("Using encoder: '" + codec.getName() + "'"); + return codec; + } + + private static void setCodecOption(MediaFormat format, CodecOption codecOption) { + String key = codecOption.getKey(); + Object value = codecOption.getValue(); + + if (value instanceof Integer) { + format.setInteger(key, (Integer) value); + } else if (value instanceof Long) { + format.setLong(key, (Long) value); + } else if (value instanceof Float) { + format.setFloat(key, (Float) value); + } else if (value instanceof String) { + format.setString(key, (String) value); + } + + Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + + private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); - format.setString(MediaFormat.KEY_MIME, "video/avc"); + format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); // display the very first frame, and recover from bad quality when no new frames format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs if (maxFps > 0) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - format.setFloat(MediaFormat.KEY_MAX_FPS_TO_ENCODER, maxFps); - } else { - Ln.w("Max FPS is only supported since Android 10, the option has been ignored"); + // The key existed privately before Android 10: + // + // + format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); + } + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + setCodecOption(format, option); } } + return format; } private static IBinder createDisplay() { - return SurfaceControl.createDisplay("scrcpy", true); + // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. + // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". + boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" + .equals(Build.VERSION.CODENAME)); + return SurfaceControl.createDisplay("scrcpy", secure); } private static void configure(MediaCodec codec, MediaFormat format) { @@ -172,12 +242,12 @@ public class ScreenEncoder implements Device.RotationListener { format.setInteger(MediaFormat.KEY_HEIGHT, height); } - private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect) { + private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); - SurfaceControl.setDisplayLayerStack(display, 0); + SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); + SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index f2fce1d6..c27322ef 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -3,29 +3,167 @@ package com.genymobile.scrcpy; import android.graphics.Rect; public final class ScreenInfo { + /** + * Device (physical) size, possibly cropped + */ private final Rect contentRect; // device size, possibly cropped - private final Size videoSize; - private final boolean rotated; - public ScreenInfo(Rect contentRect, Size videoSize, boolean rotated) { + /** + * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. + *

+ * However, it does not include the locked video orientation. + */ + private final Size unlockedVideoSize; + + /** + * Device rotation, related to the natural device orientation (0, 1, 2 or 3) + */ + private final int deviceRotation; + + /** + * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) + */ + private final int lockedVideoOrientation; + + public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { this.contentRect = contentRect; - this.videoSize = videoSize; - this.rotated = rotated; + this.unlockedVideoSize = unlockedVideoSize; + this.deviceRotation = deviceRotation; + this.lockedVideoOrientation = lockedVideoOrientation; } public Rect getContentRect() { return contentRect; } - public Size getVideoSize() { - return videoSize; + /** + * Return the video size as if locked video orientation was not set. + * + * @return the unlocked video size + */ + public Size getUnlockedVideoSize() { + return unlockedVideoSize; } - public ScreenInfo withRotation(int rotation) { - boolean newRotated = (rotation & 1) != 0; - if (rotated == newRotated) { + /** + * Return the actual video size if locked video orientation is set. + * + * @return the actual video size + */ + public Size getVideoSize() { + if (getVideoRotation() % 2 == 0) { + return unlockedVideoSize; + } + + return unlockedVideoSize.rotate(); + } + + public int getDeviceRotation() { + return deviceRotation; + } + + public ScreenInfo withDeviceRotation(int newDeviceRotation) { + if (newDeviceRotation == deviceRotation) { return this; } - return new ScreenInfo(Device.flipRect(contentRect), videoSize.rotate(), newRotated); + // true if changed between portrait and landscape + boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; + Rect newContentRect; + Size newUnlockedVideoSize; + if (orientationChanged) { + newContentRect = flipRect(contentRect); + newUnlockedVideoSize = unlockedVideoSize.rotate(); + } else { + newContentRect = contentRect; + newUnlockedVideoSize = unlockedVideoSize; + } + return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); + } + + public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { + int rotation = displayInfo.getRotation(); + + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + // The user requested to lock the video orientation to the current orientation + lockedVideoOrientation = rotation; + } + + Size deviceSize = displayInfo.getSize(); + Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); + if (crop != null) { + if (rotation % 2 != 0) { // 180s preserve dimensions + // the crop (provided by the user) is expressed in the natural orientation + crop = flipRect(crop); + } + if (!contentRect.intersect(crop)) { + // intersect() changes contentRect so that it is intersected with crop + Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); + contentRect = new Rect(); // empty + } + } + + Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); + } + + private static String formatCrop(Rect rect) { + return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; + } + + private static Size computeVideoSize(int w, int h, int maxSize) { + // Compute the video size and the padding of the content inside this video. + // Principle: + // - scale down the great side of the screen to maxSize (if necessary); + // - scale down the other side so that the aspect ratio is preserved; + // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) + w &= ~7; // in case it's not a multiple of 8 + h &= ~7; + if (maxSize > 0) { + if (BuildConfig.DEBUG && maxSize % 8 != 0) { + throw new AssertionError("Max size must be a multiple of 8"); + } + boolean portrait = h > w; + int major = portrait ? h : w; + int minor = portrait ? w : h; + if (major > maxSize) { + int minorExact = minor * maxSize / major; + // +4 to round the value to the nearest multiple of 8 + minor = (minorExact + 4) & ~7; + major = maxSize; + } + w = portrait ? minor : major; + h = portrait ? major : minor; + } + return new Size(w, h); + } + + private static Rect flipRect(Rect crop) { + return new Rect(crop.top, crop.left, crop.bottom, crop.right); + } + + /** + * Return the rotation to apply to the device rotation to get the requested locked video orientation + * + * @return the rotation offset + */ + public int getVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (deviceRotation + 4 - lockedVideoOrientation) % 4; + } + + /** + * Return the rotation to apply to the requested locked video orientation to get the device rotation + * + * @return the (reverse) rotation offset + */ + public int getReverseVideoRotation() { + if (lockedVideoOrientation == -1) { + // no offset + return 0; + } + return (lockedVideoOrientation + 4 - deviceRotation) % 4; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 56b738fb..fdd9db88 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,32 +1,78 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ContentProvider; + import android.graphics.Rect; import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.os.BatteryManager; import android.os.Build; -import java.io.File; import java.io.IOException; +import java.util.List; +import java.util.Locale; public final class Server { - private static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; private Server() { // not instantiable } private static void scrcpy(Options options) throws IOException { + Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); - boolean tunnelForward = options.isTunnelForward(); - try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { - ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps()); + List codecOptions = CodecOption.parse(options.getCodecOptions()); + boolean mustDisableShowTouchesOnCleanUp = false; + int restoreStayOn = -1; + if (options.getShowTouches() || options.getStayAwake()) { + try (ContentProvider settings = Device.createSettingsProvider()) { + if (options.getShowTouches()) { + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); + } + + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + String oldValue = settings.getAndPutValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + restoreStayOn = Integer.parseInt(oldValue); + if (restoreStayOn == stayOn) { + // No need to restore + restoreStayOn = -1; + } + } catch (NumberFormatException e) { + restoreStayOn = 0; + } + } + } + } + + CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); + + boolean tunnelForward = options.isTunnelForward(); + + try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { + ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, + options.getEncoderName()); + + Thread controllerThread = null; + Thread deviceMessageSenderThread = null; if (options.getControl()) { - Controller controller = new Controller(device, connection); + final Controller controller = new Controller(device, connection); // asynchronous - startController(controller); - startDeviceMessageSender(controller.getSender()); + controllerThread = startController(controller); + deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); + + device.setClipboardListener(new Device.ClipboardListener() { + @Override + public void onClipboardTextChanged(String text) { + controller.getSender().pushClipboardText(text); + } + }); } try { @@ -35,12 +81,19 @@ public final class Server { } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); + } finally { + if (controllerThread != null) { + controllerThread.interrupt(); + } + if (deviceMessageSenderThread != null) { + deviceMessageSenderThread.interrupt(); + } } } } - private static void startController(final Controller controller) { - new Thread(new Runnable() { + private static Thread startController(final Controller controller) { + Thread thread = new Thread(new Runnable() { @Override public void run() { try { @@ -50,11 +103,13 @@ public final class Server { Ln.d("Controller stopped"); } } - }).start(); + }); + thread.start(); + return thread; } - private static void startDeviceMessageSender(final DeviceMessageSender sender) { - new Thread(new Runnable() { + private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { + Thread thread = new Thread(new Runnable() { @Override public void run() { try { @@ -64,10 +119,11 @@ public final class Server { Ln.d("Device message sender stopped"); } } - }).start(); + }); + thread.start(); + return thread; } - @SuppressWarnings("checkstyle:MagicNumber") private static Options createOptions(String... args) { if (args.length < 1) { throw new IllegalArgumentException("Missing client version"); @@ -76,41 +132,65 @@ public final class Server { String clientVersion = args[0]; if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { throw new IllegalArgumentException( - "The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")"); + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); } - if (args.length != 8) { - throw new IllegalArgumentException("Expecting 8 parameters"); + final int expectedParameters = 16; + if (args.length != expectedParameters) { + throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); } Options options = new Options(); - int maxSize = Integer.parseInt(args[1]) & ~7; // multiple of 8 + Ln.Level level = Ln.Level.valueOf(args[1].toUpperCase(Locale.ENGLISH)); + options.setLogLevel(level); + + int maxSize = Integer.parseInt(args[2]) & ~7; // multiple of 8 options.setMaxSize(maxSize); - int bitRate = Integer.parseInt(args[2]); + int bitRate = Integer.parseInt(args[3]); options.setBitRate(bitRate); - int maxFps = Integer.parseInt(args[3]); + int maxFps = Integer.parseInt(args[4]); options.setMaxFps(maxFps); + int lockedVideoOrientation = Integer.parseInt(args[5]); + options.setLockedVideoOrientation(lockedVideoOrientation); + // use "adb forward" instead of "adb tunnel"? (so the server must listen) - boolean tunnelForward = Boolean.parseBoolean(args[4]); + boolean tunnelForward = Boolean.parseBoolean(args[6]); options.setTunnelForward(tunnelForward); - Rect crop = parseCrop(args[5]); + Rect crop = parseCrop(args[7]); options.setCrop(crop); - boolean sendFrameMeta = Boolean.parseBoolean(args[6]); + boolean sendFrameMeta = Boolean.parseBoolean(args[8]); options.setSendFrameMeta(sendFrameMeta); - boolean control = Boolean.parseBoolean(args[7]); + boolean control = Boolean.parseBoolean(args[9]); options.setControl(control); + int displayId = Integer.parseInt(args[10]); + options.setDisplayId(displayId); + + boolean showTouches = Boolean.parseBoolean(args[11]); + options.setShowTouches(showTouches); + + boolean stayAwake = Boolean.parseBoolean(args[12]); + options.setStayAwake(stayAwake); + + String codecOptions = args[13]; + options.setCodecOptions(codecOptions); + + String encoderName = "-".equals(args[14]) ? null : args[14]; + options.setEncoderName(encoderName); + + boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]); + options.setPowerOffScreenOnClose(powerOffScreenOnClose); + return options; } - @SuppressWarnings("checkstyle:MagicNumber") private static Rect parseCrop(String crop) { if ("-".equals(crop)) { return null; @@ -127,15 +207,6 @@ public final class Server { return new Rect(x, y, x + width, y + height); } - private static void unlinkSelf() { - try { - new File(SERVER_PATH).delete(); - } catch (Exception e) { - Ln.e("Could not unlink server", e); - } - } - - @SuppressWarnings("checkstyle:MagicNumber") private static void suggestFix(Throwable e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (e instanceof MediaCodec.CodecException) { @@ -147,6 +218,25 @@ public final class Server { } } } + if (e instanceof InvalidDisplayIdException) { + InvalidDisplayIdException idie = (InvalidDisplayIdException) e; + int[] displayIds = idie.getAvailableDisplayIds(); + if (displayIds != null && displayIds.length > 0) { + Ln.e("Try to use one of the available display ids:"); + for (int id : displayIds) { + Ln.e(" scrcpy --display " + id); + } + } + } else if (e instanceof InvalidEncoderException) { + InvalidEncoderException iee = (InvalidEncoderException) e; + MediaCodecInfo[] encoders = iee.getAvailableEncoders(); + if (encoders != null && encoders.length > 0) { + Ln.e("Try to use one of the available encoders:"); + for (MediaCodecInfo encoder : encoders) { + Ln.e(" scrcpy --encoder '" + encoder.getName() + "'"); + } + } + } } public static void main(String... args) throws Exception { @@ -158,8 +248,10 @@ public final class Server { } }); - unlinkSelf(); Options options = createOptions(args); + + Ln.initLogLevel(options.getLogLevel()); + scrcpy(options); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java index 199fc8c1..dac05466 100644 --- a/server/src/main/java/com/genymobile/scrcpy/StringUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/StringUtils.java @@ -5,7 +5,6 @@ public final class StringUtils { // not instantiable } - @SuppressWarnings("checkstyle:MagicNumber") public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b1b81903..0f473bc1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -16,6 +16,7 @@ public final class Workarounds { // not instantiable } + @SuppressWarnings("deprecation") public static void prepareMainLooper() { // Some devices internally create a Handler when creating an input Surface, causing an exception: // "Can't create handler inside thread that has not called Looper.prepare()" @@ -28,7 +29,7 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi") + @SuppressLint("PrivateApi,DiscouragedPrivateApi") public static void fillAppInfo() { try { // ActivityThread activityThread = new ActivityThread(); @@ -73,7 +74,7 @@ public final class Workarounds { mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.w("Could not fill app info: " + throwable.getMessage()); + Ln.d("Could not fill app info: " + throwable.getMessage()); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java new file mode 100644 index 00000000..93ed4528 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -0,0 +1,87 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ActivityManager { + + private final IInterface manager; + private Method getContentProviderExternalMethod; + private boolean getContentProviderExternalMethodNewVersion = true; + private Method removeContentProviderExternalMethod; + + public ActivityManager(IInterface manager) { + this.manager = manager; + } + + private Method getGetContentProviderExternalMethod() throws NoSuchMethodException { + if (getContentProviderExternalMethod == null) { + try { + getContentProviderExternalMethod = manager.getClass() + .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class); + } catch (NoSuchMethodException e) { + // old version + getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); + getContentProviderExternalMethodNewVersion = false; + } + } + return getContentProviderExternalMethod; + } + + private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException { + if (removeContentProviderExternalMethod == null) { + removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class); + } + return removeContentProviderExternalMethod; + } + + private ContentProvider getContentProviderExternal(String name, IBinder token) { + try { + Method method = getGetContentProviderExternalMethod(); + Object[] args; + if (getContentProviderExternalMethodNewVersion) { + // new version + args = new Object[]{name, ServiceManager.USER_ID, token, null}; + } else { + // old version + args = new Object[]{name, ServiceManager.USER_ID, token}; + } + // ContentProviderHolder providerHolder = getContentProviderExternal(...); + Object providerHolder = method.invoke(manager, args); + if (providerHolder == null) { + return null; + } + // IContentProvider provider = providerHolder.provider; + Field providerField = providerHolder.getClass().getDeclaredField("provider"); + providerField.setAccessible(true); + Object provider = providerField.get(providerHolder); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, name, token); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + void removeContentProviderExternal(String name, IBinder token) { + try { + Method method = getRemoveContentProviderExternalMethod(); + method.invoke(manager, name, token); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + + public ContentProvider createSettingsProvider() { + return getContentProviderExternal("settings", new Binder()); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 592bdf6b..e25b6e99 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.content.ClipData; +import android.content.IOnPrimaryClipChangedListener; import android.os.Build; import android.os.IInterface; @@ -10,13 +11,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClipboardManager { - - private static final String PACKAGE_NAME = "com.android.shell"; - private static final int USER_ID = 0; - private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; + private Method addPrimaryClipChangedListener; public ClipboardManager(IInterface manager) { this.manager = manager; @@ -46,17 +44,17 @@ public class ClipboardManager { private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (ClipData) method.invoke(manager, PACKAGE_NAME); + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); } - return (ClipData) method.invoke(manager, PACKAGE_NAME, USER_ID); + return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - method.invoke(manager, clipData, PACKAGE_NAME); + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); } else { - method.invoke(manager, clipData, PACKAGE_NAME, USER_ID); + method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } } @@ -74,13 +72,48 @@ public class ClipboardManager { } } - public void setText(CharSequence text) { + public boolean setText(CharSequence text) { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; + } + } + + private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) + throws InvocationTargetException, IllegalAccessException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); + } else { + method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + } + } + + private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { + if (addPrimaryClipChangedListener == null) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); + } else { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + } + } + return addPrimaryClipChangedListener; + } + + public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + try { + Method method = getAddPrimaryClipChangedListener(); + addPrimaryClipChangedListener(method, manager, listener); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return false; } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java new file mode 100644 index 00000000..387c7a60 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -0,0 +1,171 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.IBinder; + +import java.io.Closeable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ContentProvider implements Closeable { + + public static final String TABLE_SYSTEM = "system"; + public static final String TABLE_SECURE = "secure"; + public static final String TABLE_GLOBAL = "global"; + + // See android/providerHolder/Settings.java + private static final String CALL_METHOD_GET_SYSTEM = "GET_system"; + private static final String CALL_METHOD_GET_SECURE = "GET_secure"; + private static final String CALL_METHOD_GET_GLOBAL = "GET_global"; + + private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; + private static final String CALL_METHOD_PUT_SECURE = "PUT_secure"; + private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global"; + + private static final String CALL_METHOD_USER_KEY = "_user"; + + private static final String NAME_VALUE_TABLE_VALUE = "value"; + + private final ActivityManager manager; + // android.content.IContentProvider + private final Object provider; + private final String name; + private final IBinder token; + + private Method callMethod; + private int callMethodVersion; + + private Object attributionSource; + + ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { + this.manager = manager; + this.provider = provider; + this.name = name; + this.token = token; + } + + @SuppressLint("PrivateApi") + private Method getCallMethod() throws NoSuchMethodException { + if (callMethod == null) { + try { + Class attributionSourceClass = Class.forName("android.content.AttributionSource"); + callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 0; + } catch (NoSuchMethodException | ClassNotFoundException e0) { + // old versions + try { + callMethod = provider.getClass() + .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 1; + } catch (NoSuchMethodException e1) { + try { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); + callMethodVersion = 2; + } catch (NoSuchMethodException e2) { + callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); + callMethodVersion = 3; + } + } + } + } + return callMethod; + } + + @SuppressLint("PrivateApi") + private Object getAttributionSource() + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + if (attributionSource == null) { + Class cl = Class.forName("android.content.AttributionSource$Builder"); + Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID); + cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME); + attributionSource = cl.getDeclaredMethod("build").invoke(builder); + } + + return attributionSource; + } + + private Bundle call(String callMethod, String arg, Bundle extras) { + try { + Method method = getCallMethod(); + Object[] args; + switch (callMethodVersion) { + case 0: + args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras}; + break; + case 1: + args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 2: + args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; + break; + default: + args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; + break; + } + return (Bundle) method.invoke(provider, args); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + public void close() { + manager.removeContentProviderExternal(name, token); + } + + private static String getGetMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_GET_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_GET_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_GET_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + private static String getPutMethod(String table) { + switch (table) { + case TABLE_SECURE: + return CALL_METHOD_PUT_SECURE; + case TABLE_SYSTEM: + return CALL_METHOD_PUT_SYSTEM; + case TABLE_GLOBAL: + return CALL_METHOD_PUT_GLOBAL; + default: + throw new IllegalArgumentException("Invalid table: " + table); + } + } + + public String getValue(String table, String key) { + String method = getGetMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } + + public void putValue(String table, String key, String value) { + String method = getPutMethod(table); + Bundle arg = new Bundle(); + arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + arg.putString(NAME_VALUE_TABLE_VALUE, value); + call(method, key, arg); + } + + public String getAndPutValue(String table, String key, String value) { + String oldValue = getValue(table, key); + if (!value.equals(oldValue)) { + putValue(table, key, value); + } + return oldValue; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 568afacd..cedb3f47 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -12,15 +12,28 @@ public final class DisplayManager { this.manager = manager; } - public DisplayInfo getDisplayInfo() { + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + if (displayInfo == null) { + return null; + } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); - return new DisplayInfo(new Size(width, height), rotation); + int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); + int flags = cls.getDeclaredField("flags").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public int[] getDisplayIds() { + try { + return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 44fa613b..e17b5a17 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -17,6 +17,8 @@ public final class InputManager { private final IInterface manager; private Method injectInputEventMethod; + private static Method setDisplayIdMethod; + public InputManager(IInterface manager) { this.manager = manager; } @@ -37,4 +39,22 @@ public final class InputManager { return false; } } + + private static Method getSetDisplayIdMethod() throws NoSuchMethodException { + if (setDisplayIdMethod == null) { + setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); + } + return setDisplayIdMethod; + } + + public static boolean setDisplayId(InputEvent inputEvent, int displayId) { + try { + Method method = getSetDisplayIdMethod(); + method.invoke(inputEvent, displayId); + return true; + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Cannot associate a display id to the input event", e); + return false; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index 0b625c92..6f4b9c04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -6,8 +6,12 @@ import android.os.IInterface; import java.lang.reflect.Method; -@SuppressLint("PrivateApi") +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { + + public static final String PACKAGE_NAME = "com.android.shell"; + public static final int USER_ID = 0; + private final Method getServiceMethod; private WindowManager windowManager; @@ -16,6 +20,7 @@ public final class ServiceManager { private PowerManager powerManager; private StatusBarManager statusBarManager; private ClipboardManager clipboardManager; + private ActivityManager activityManager; public ServiceManager() { try { @@ -72,8 +77,32 @@ public final class ServiceManager { public ClipboardManager getClipboardManager() { if (clipboardManager == null) { - clipboardManager = new ClipboardManager(getService("clipboard", "android.content.IClipboard")); + IInterface clipboard = getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { + // Some devices have no clipboard manager + // + // + return null; + } + clipboardManager = new ClipboardManager(clipboard); } return clipboardManager; } + + public ActivityManager getActivityManager() { + if (activityManager == null) { + try { + // On old Android versions, the ActivityManager is not exposed via AIDL, + // so use ActivityManagerNative.getDefault() + Class cls = Class.forName("android.app.ActivityManagerNative"); + Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); + IInterface am = (IInterface) getDefaultMethod.invoke(null); + activityManager = new ActivityManager(am); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + return activityManager; + } } 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 6f8941bd..5b1e5f5e 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,8 @@ public class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; + private Method expandSettingsPanelMethod; + private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; public StatusBarManager(IInterface manager) { @@ -24,6 +26,20 @@ public class StatusBarManager { return expandNotificationsPanelMethod; } + private Method getExpandSettingsPanel() throws NoSuchMethodException { + if (expandSettingsPanelMethod == null) { + try { + // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); + } catch (NoSuchMethodException e) { + // old version + expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); + expandSettingsPanelMethodNewVersion = false; + } + } + return expandSettingsPanelMethod; + } + private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); @@ -40,6 +56,21 @@ public class StatusBarManager { } } + public void expandSettingsPanel() { + try { + Method method = getExpandSettingsPanel(); + if (expandSettingsPanelMethodNewVersion) { + // new version + method.invoke(manager, (Object) null); + } else { + // old version + method.invoke(manager); + } + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + } + } + public void collapsePanels() { try { Method method = getCollapsePanelsMethod(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 227bbc85..8fbb860b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -121,12 +121,14 @@ public final class SurfaceControl { return setDisplayPowerModeMethod; } - public static void setDisplayPowerMode(IBinder displayToken, int mode) { + public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { try { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); + return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); + return false; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index cc687cd5..faa366a5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -93,13 +93,13 @@ public final class WindowManager { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); diff --git a/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java new file mode 100644 index 00000000..ad802258 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java @@ -0,0 +1,114 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class CodecOptionsTest { + + @Test + public void testIntegerImplicit() { + List codecOptions = CodecOption.parse("some_key=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testInteger() { + List codecOptions = CodecOption.parse("some_key:int=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(5, option.getValue()); + } + + @Test + public void testLong() { + List codecOptions = CodecOption.parse("some_key:long=5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(5L, option.getValue()); + } + + @Test + public void testFloat() { + List codecOptions = CodecOption.parse("some_key:float=4.5"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + } + + @Test + public void testString() { + List codecOptions = CodecOption.parse("some_key:string=some_value"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("some_value", option.getValue()); + } + + @Test + public void testStringEscaped() { + List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); + + Assert.assertEquals(1, codecOptions.size()); + + CodecOption option = codecOptions.get(0); + Assert.assertEquals("some_key", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); + } + + @Test + public void testList() { + List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); + + Assert.assertEquals(5, codecOptions.size()); + + CodecOption option; + + option = codecOptions.get(0); + Assert.assertEquals("a", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(1, option.getValue()); + + option = codecOptions.get(1); + Assert.assertEquals("b", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Integer); + Assert.assertEquals(2, option.getValue()); + + option = codecOptions.get(2); + Assert.assertEquals("c", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Long); + Assert.assertEquals(3L, option.getValue()); + + option = codecOptions.get(3); + Assert.assertEquals("d", option.getKey()); + Assert.assertTrue(option.getValue() instanceof Float); + Assert.assertEquals(4.5f, option.getValue()); + + option = codecOptions.get(4); + Assert.assertEquals("e", option.getKey()); + Assert.assertTrue(option.getValue() instanceof String); + Assert.assertEquals("a,b=c", option.getValue()); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index 5e663bb9..da568486 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -25,15 +25,20 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @@ -45,7 +50,7 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -63,9 +68,9 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); - byte[] text = new byte[ControlMessageReader.TEXT_MAX_LENGTH]; + byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -77,7 +82,6 @@ public class ControlMessageReaderTest { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseTouchEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -95,6 +99,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -110,7 +117,6 @@ public class ControlMessageReaderTest { } @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testParseScrollEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); @@ -126,6 +132,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -145,6 +154,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); + dos.writeByte(KeyEvent.ACTION_UP); byte[] packet = bos.toByteArray(); @@ -152,6 +162,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); + Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); } @Test @@ -171,19 +182,35 @@ public class ControlMessageReaderTest { } @Test - public void testParseCollapseNotificationPanelEvent() throws IOException { + public void testParseExpandSettingsPanelEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL); + dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); - Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType()); + Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); + } + + @Test + public void testParseCollapsePanelsEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); } @Test @@ -209,8 +236,9 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); - dos.writeShort(text.length); + dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); @@ -220,6 +248,33 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals("testé", event.getText()); + Assert.assertTrue(event.getPaste()); + } + + @Test + public void testParseBigSetClipboardEvent() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + + byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeByte(1); // paste + Arrays.fill(rawText, (byte) 'a'); + String text = new String(rawText, 0, rawText.length); + + dos.writeInt(rawText.length); + dos.write(rawText); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(text, event.getText()); + Assert.assertTrue(event.getPaste()); } @Test @@ -233,6 +288,9 @@ public class ControlMessageReaderTest { byte[] packet = bos.toByteArray(); + // The message type (1 byte) does not count + Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); + reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); @@ -266,11 +324,13 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(0); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(1); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); @@ -280,12 +340,14 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @@ -299,6 +361,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); + dos.writeInt(4); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); @@ -311,6 +374,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); + Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); @@ -318,6 +382,7 @@ public class ControlMessageReaderTest { bos.reset(); dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); @@ -327,6 +392,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); + Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index df12f647..88bf2af9 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -19,7 +19,7 @@ public class DeviceMessageWriterTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); - dos.writeShort(data.length); + dos.writeInt(data.length); dos.write(data); byte[] expected = bos.toByteArray(); diff --git a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java index 7d89ee64..89799c5e 100644 --- a/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java @@ -8,7 +8,6 @@ import java.nio.charset.StandardCharsets; public class StringUtilsTest { @Test - @SuppressWarnings("checkstyle:MagicNumber") public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);