diff --git a/.gitignore b/.gitignore index 2829d835..26d977ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ .gradle/ /x/ local.properties +/scrcpy-server diff --git a/DEVELOP.md b/DEVELOP.md deleted file mode 100644 index d200c3fd..00000000 --- a/DEVELOP.md +++ /dev/null @@ -1,309 +0,0 @@ -# scrcpy for developers - -## Overview - -This application is composed of two parts: - - the server (`scrcpy-server`), to be executed on the device, - - the client (the `scrcpy` binary), executed on the host computer. - -The client is responsible to push the server to the device and start its -execution. - -Once the client and the server are connected to each other, the server initially -sends device information (name and initial screen dimensions), then starts to -send a raw H.264 video stream of the device screen. The client decodes the video -frames, and display them as soon as possible, without buffering, to minimize -latency. The client is not aware of the device rotation (which is handled by the -server), it just knows the dimensions of the video frames. - -The client captures relevant keyboard and mouse events, that it transmits to the -server, which injects them to the device. - - - -## Server - - -### Privileges - -Capturing the screen requires some privileges, which are granted to `shell`. - -The server is a Java application (with a [`public static void main(String... -args)`][main] method), compiled against the Android framework, and executed as -`shell` on the Android device. - -[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 - -To run such a Java application, the classes must be [_dexed_][dex] (typically, -to `classes.dex`). If `my.package.MainClass` is the main class, compiled to -`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run -with: - - adb shell CLASSPATH=/data/local/tmp/classes.dex \ - app_process / my.package.MainClass - -_The path `/data/local/tmp` is a good candidate to push the server, since it's -readable and writable by `shell`, but not world-writable, so a malicious -application may not replace the server just before the client executes it._ - -Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing -`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle -build system, the server is built to an (unsigned) APK (renamed to -`scrcpy-server`). - -[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) -[apk]: https://en.wikipedia.org/wiki/Android_application_package - - -### Hidden methods - -Although compiled against the Android framework, [hidden] methods and classes are -not directly accessible (and they may differ from one Android version to -another). - -They can be called using reflection though. The communication with hidden -components is provided by [_wrappers_ classes][wrappers] and [aidl]. - -[hidden]: https://stackoverflow.com/a/31908373/1987178 -[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers -[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view - - -### Threading - -The server uses 3 threads: - - - the **main** thread, encoding and streaming the video to the client; - - the **controller** thread, listening for _control messages_ (typically, - keyboard and mouse events) from the client; - - the **receiver** thread (managed by the controller), sending _device messages_ - to the clients (currently, it is only used to send the device clipboard - content). - -Since the video encoding is typically hardware, there would be no benefit in -encoding and streaming in two different threads. - - -### Screen video encoding - -The encoding is managed by [`ScreenEncoder`]. - -The video is encoded using the [`MediaCodec`] API. The codec takes its input -from a [surface] associated to the display, and writes the resulting H.264 -stream to the provided output stream (the socket connected to the client). - -[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java -[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html -[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69 - -On device [rotation], the codec, surface and display are reinitialized, and a -new video stream is produced. - -New frames are produced only when changes occur on the surface. This is good -because it avoids to send unnecessary frames, but there are drawbacks: - - - it does not send any frame on start if the device screen does not change, - - after fast motion changes, the last frame may have poor quality. - -Both problems are [solved][repeat] by the flag -[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. - -[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 -[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 -[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER - - -### Input events injection - -_Control messages_ are received from the client by the [`Controller`] (run in a -separate thread). There are several types of input events: - - keycode (cf [`KeyEvent`]), - - text (special characters may not be handled by keycodes directly), - - mouse motion/click, - - mouse scroll, - - other commands (e.g. to switch the screen on or to copy the clipboard). - -Some of them need to inject input events to the system. To do so, they use the -_hidden_ method [`InputManager.injectInputEvent`] (exposed by our -[`InputManager` wrapper][inject-wrapper]). - -[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81 -[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html -[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html -[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 -[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 - - - -## Client - -The client relies on [SDL], which provides cross-platform API for UI, input -events, threading, etc. - -The video stream is decoded by [libav] (FFmpeg). - -[SDL]: https://www.libsdl.org -[libav]: https://www.libav.org/ - -### Initialization - -On startup, in addition to _libav_ and _SDL_ initialization, the client must -push and start the server on the device, and open two sockets (one for the video -stream, one for control) so that they may communicate. - -Note that the client-server roles are expressed at the application level: - - - the server _serves_ video stream and handle requests from the client, - - the client _controls_ the device through the server. - -However, the roles are reversed at the network level: - - - the client opens a server socket and listen on a port before starting the - server, - - the server connects to the client. - -This role inversion guarantees that the connection will not fail due to race -conditions, and avoids polling. - -_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb -reverse`. See commit [1038bad] and [issue #5].)_ - -Once the server is connected, it sends the device information (name and initial -screen dimensions). Thus, the client may init the window and renderer, before -the first frame is available. - -To minimize startup time, SDL initialization is performed while listening for -the connection from the server (see commit [90a46b4]). - -[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172 -[issue #5]: https://github.com/Genymobile/scrcpy/issues/5 -[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e - - -### Threading - -The client uses 4 threads: - - - the **main** thread, executing the SDL event loop, - - the **stream** thread, receiving the video and used for decoding and - recording, - - the **controller** thread, sending _control messages_ to the server, - - the **receiver** thread (managed by the controller), receiving _device - messages_ from the server. - -In addition, another thread can be started if necessary to handle APK -installation or file push requests (via drag&drop on the main window) or to -print the framerate regularly in the console. - - - -### Stream - -The video [stream] is received from the socket (connected to the server on the -device) in a separate thread. - -If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_ -to decode the H.264 stream from the socket, and notifies the main thread when a -new frame is available. - -There are two [frames][video_buffer] simultaneously in memory: - - the **decoding** frame, written by the decoder from the decoder thread, - - 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 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 -H.264 packet to the output video file. - -[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h -[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h -[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h -[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h - -``` - +----------+ +----------+ - ---> | decoder | ---> | screen | - +---------+ / +----------+ +----------+ - socket ---> | stream | ---- - +---------+ \ +----------+ - ---> | recorder | - +----------+ -``` - -### Controller - -The [controller] is responsible to send _control messages_ to the device. It -runs in a separate thread, to avoid I/O on the main thread. - -On SDL event, received on the main thread, the [input manager][inputmanager] -creates appropriate [_control messages_][controlmsg]. It is responsible to -convert SDL events to Android events (using [convert]). It pushes the _control -messages_ to a queue hold by the controller. On its own thread, the controller -takes messages from the queue, that it serializes and sends to the client. - -[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h -[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h -[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h -[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h - - -### UI and event loop - -Initialization, input events and rendering are all [managed][scrcpy] in the main -thread. - -Events are handled in the [event loop], which either updates the [screen] or -delegates to the [input manager][inputmanager]. - -[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c -[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201 -[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h - - -## Hack - -For more details, go read the code! - -If you find a bug, or have an awesome idea to implement, please discuss and -contribute ;-) - - -### Debug the server - -The server is pushed to the device by the client on startup. - -To debug it, enable the server debugger during configuration: - -```bash -meson x -Dserver_debugger=true -# or, if x is already configured -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. -Redirect that port to the computer: - -```bash -adb forward tcp:5005 tcp:5005 -``` - -In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on -`+`, _Remote_, and fill the form: - - - Host: `localhost` - - Port: `5005` - -Then click on _Debug_. diff --git a/FAQ.it.md b/FAQ.it.md deleted file mode 100644 index 5c5830ce..00000000 --- a/FAQ.it.md +++ /dev/null @@ -1,217 +0,0 @@ -_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 deleted file mode 100644 index c9e06e24..00000000 --- a/FAQ.ko.md +++ /dev/null @@ -1,84 +0,0 @@ -# 자주하는 질문 (FAQ) - -다음은 자주 제보되는 문제들과 그들의 현황입니다. - - -### Windows 운영체제에서, 디바이스가 발견되지 않습니다. - -가장 흔한 제보는 `adb`에 발견되지 않는 디바이스 혹은 권한 관련 문제입니다. -다음 명령어를 호출하여 모든 것들에 이상이 없는지 확인하세요: - - adb devices - -Windows는 당신의 디바이스를 감지하기 위해 [드라이버]가 필요할 수도 있습니다. - -[드라이버]: https://developer.android.com/studio/run/oem-usb.html - - -### 내 디바이스의 미러링만 가능하고, 디바이스와 상호작용을 할 수 없습니다. - -일부 디바이스에서는, [simulating input]을 허용하기 위해서 한가지 옵션을 활성화해야 할 수도 있습니다. -개발자 옵션에서 (developer options) 다음을 활성화 하세요: - -> **USB debugging (Security settings)** -> _권한 부여와 USB 디버깅을 통한 simulating input을 허용한다_ - -[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -### 마우스 클릭이 다른 곳에 적용됩니다. - -Mac 운영체제에서, HiDPI support 와 여러 스크린 창이 있는 경우, 입력 위치가 잘못 파악될 수 있습니다. -[issue 15]를 참고하세요. - -[issue 15]: https://github.com/Genymobile/scrcpy/issues/15 - -차선책은 HiDPI support을 비활성화 하고 build하는 방법입니다: - -```bash -meson x --buildtype release -Dhidpi_support=false -``` - -하지만, 동영상은 낮은 해상도로 재생될 것 입니다. - - -### HiDPI display의 화질이 낮습니다. - -Windows에서는, [scaling behavior] 환경을 설정해야 할 수도 있습니다. - -> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > -> Override high DPI scaling behavior > Scaling performed by: _Application_. - -[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 - - -### KWin compositor가 실행되지 않습니다 - -Plasma Desktop에서는,_scrcpy_ 가 실행중에는 compositor가 비활성화 됩니다. - -차석책으로는, ["Block compositing"를 비활성화하세요][kwin]. - -[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 - - -###비디오 스트림을 열 수 없는 에러가 발생합니다.(Could not open video stream). - -여러가지 원인이 있을 수 있습니다. 가장 흔한 원인은 디바이스의 하드웨어 인코더(hardware encoder)가 -주어진 해상도를 인코딩할 수 없는 경우입니다. - -``` -ERROR: Exception on thread Thread[main,5,main] -android.media.MediaCodec$CodecException: Error 0xfffffc0e -... -Exit due to uncaughtException in main thread: -ERROR: Could not open video stream -INFO: Initial texture: 1080x2336 -``` - -더 낮은 해상도로 시도 해보세요: - -``` -scrcpy -m 1920 -scrcpy -m 1024 -scrcpy -m 800 -``` diff --git a/FAQ.md b/FAQ.md index f74845a5..5f089cd7 100644 --- a/FAQ.md +++ b/FAQ.md @@ -4,23 +4,16 @@ Here are the common reported problems and their status. +If you encounter any error, the first step is to upgrade to the latest version. -## `adb` issues + +## `adb` and USB issues `scrcpy` execute `adb` commands to initialize the connection with the device. If `adb` fails, then scrcpy will not work. -In that case, it will print this error: - -> ERROR: "adb push" returned with value 1 - This is typically not a bug in _scrcpy_, but a problem in your environment. -To find out the cause, execute: - -```bash -adb devices -``` ### `adb` not found @@ -30,38 +23,63 @@ On Windows, the current directory is in your `PATH`, and `adb.exe` is included in the release, so it should work out-of-the-box. -### Device unauthorized - -Check [stackoverflow][device-unauthorized]. - -[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized - - ### Device not detected -> adb: error: failed to get feature set: no devices/emulators found +> ERROR: Could not find any ADB device Check that you correctly enabled [adb debugging][enable-adb]. -If your device is not detected, you may need some [drivers] (on Windows). +Your device must be detected by `adb`: + +``` +adb devices +``` + +If your device is not detected, you may need some [drivers] (on Windows). There is a separate [USB driver for Google devices][google-usb-driver]. [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html +[google-usb-driver]: https://developer.android.com/studio/run/win-usb + + +### Device unauthorized + +> ERROR: Device is unauthorized: +> ERROR: --> (usb) 0123456789abcdef unauthorized +> ERROR: A popup should open on the device to request authorization. + +When connecting, a popup should open on the device. You must authorize USB +debugging. + +If it does not open, check [stackoverflow][device-unauthorized]. + +[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized ### Several devices connected If several devices are connected, you will encounter this error: -> adb: error: failed to get feature set: more than one device/emulator +> ERROR: Multiple (2) ADB devices: +> ERROR: --> (usb) 0123456789abcdef device Nexus_5 +> ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913 +> ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip) + +In that case, you can either provide the identifier of the device you want to +mirror: + +```bash +scrcpy -s 0123456789abcdef +``` -the identifier of the device you want to mirror must be provided: +Or request the single USB (or TCP/IP) device: ```bash -scrcpy -s 01234567890abcdef +scrcpy -d # USB device +scrcpy -e # TCP/IP device ``` -Note that if your device is connected over TCP/IP, you'll get this message: +Note that if your device is connected over TCP/IP, you might get this message: > adb: error: more than one device/emulator > ERROR: "adb reverse" returned with value 1 @@ -85,7 +103,20 @@ You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to use a specific `adb` binary, by setting the `ADB` environment variable: ```bash -set ADB=/path/to/your/adb +# in bash +export ADB=/path/to/your/adb +scrcpy +``` + +```cmd +:: in cmd +set ADB=C:\path\to\your\adb.exe +scrcpy +``` + +```powershell +# in PowerShell +$env:ADB = 'C:\path\to\your\adb.exe' scrcpy ``` @@ -102,6 +133,21 @@ Try with another USB cable or plug it into another USB port. See [#281] and [#283]: https://github.com/Genymobile/scrcpy/issues/283 +## OTG issues on Windows + +On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in: + +> ERROR: Could not find any USB device + +(or if only unrelated USB devices are detected), there might be drivers issues. + +Please read [#3654], in particular [this comment][#3654-comment1] and [the next +one][#3654-comment2]. + +[#3654]: https://github.com/Genymobile/scrcpy/issues/3654 +[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232 +[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011 + ## Control issues @@ -113,46 +159,28 @@ In developer options, enable: > **USB debugging (Security settings)** > _Allow granting permissions and simulating input via USB debugging_ +Rebooting the device is necessary once this option is set. + [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 ### Special characters do not work -Injecting text input is [limited to ASCII characters][text-input]. A trick -allows to also inject some [accented characters][accented-characters], but -that's all. See [#37]. +The default text injection method is [limited to ASCII characters][text-input]. +A trick allows to also inject some [accented characters][accented-characters], +but that's all. See [#37]. + +To avoid the problem, [change the keyboard mode to simulate a physical +keyboard][hid]. [text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [#37]: https://github.com/Genymobile/scrcpy/issues/37 +[hid]: doc/keyboard.md#physical-keyboard-simulation ## Client issues -### The quality is low - -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 - - ### Issue with Wayland By default, SDL uses x11 on Linux. The [video driver] can be changed via the @@ -187,77 +215,21 @@ As a workaround, [disable "Block compositing"][kwin]. ### Exception -There may be many reasons. One common cause is that the hardware encoder of your -device is not able to encode at the given definition: +If you get any exception related to `MediaCodec`: -> ``` -> 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 -> ``` - -or - -> ``` -> ERROR: Exception on thread Thread[main,5,main] -> java.lang.IllegalStateException -> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) -> ``` - -Just try with a lower definition: - -``` -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 +ERROR: Exception on thread Thread[main,5,main] +java.lang.IllegalStateException + at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) ``` -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/ +then try with another [encoder](doc/video.md#encoder). ## Translations -This FAQ is available in other languages: +Translations of this FAQ in other languages are available in the [wiki]. + +[wiki]: https://github.com/Genymobile/scrcpy/wiki - - [Italiano (Italiano, `it`) - v1.17](FAQ.it.md) - - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) - - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.18](FAQ.zh-Hans.md) +Only this FAQ file is guaranteed to be up-to-date. diff --git a/FAQ.zh-Hans.md b/FAQ.zh-Hans.md deleted file mode 100644 index 136b5f2e..00000000 --- a/FAQ.zh-Hans.md +++ /dev/null @@ -1,240 +0,0 @@ -只有原版的[FAQ](FAQ.md)会保持更新。 -本文根据[d6aaa5]翻译。 - -[d6aaa5]:https://github.com/Genymobile/scrcpy/blob/d6aaa5bf9aa3710660c683b6e3e0ed971ee44af5/FAQ.md - -# 常见问题 - -这里是一些常见的问题以及他们的状态。 - -## `adb` 相关问题 - -`scrcpy` 执行 `adb` 命令来初始化和设备之间的连接。如果`adb` 执行失败了, scrcpy 就无法工作。 - -在这种情况中,将会输出这个错误: - -> ERROR: "adb push" returned with value 1 - -这通常不是 _scrcpy_ 的bug,而是你的环境的问题。 - -要找出原因,请执行以下操作: - -```bash -adb devices -``` - -### 找不到`adb` - - -你的`PATH`中需要能访问到`adb`。 - -在Windows上,当前目录会包含在`PATH`中,并且`adb.exe`也包含在发行版中,因此它应该是开箱即用(直接解压就可以)的。 - - -### 设备未授权 - -参见这里 [stackoverflow][device-unauthorized]. - -[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized - - -### 未检测到设备 - -> adb: error: failed to get feature set: no devices/emulators found - -确认已经正确启用 [adb debugging][enable-adb]. - -如果你的设备没有被检测到,你可能需要一些[驱动][drivers] (在 Windows上). - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling -[drivers]: https://developer.android.com/studio/run/oem-usb.html - - -### 已连接多个设备 - -如果连接了多个设备,您将遇到以下错误: - -> adb: error: failed to get feature set: more than one device/emulator - -必须提供要镜像的设备的标识符: - -```bash -scrcpy -s 01234567890abcdef -``` - -注意,如果你的设备是通过 TCP/IP 连接的, 你将会收到以下消息: - -> adb: error: more than one device/emulator -> ERROR: "adb reverse" returned with value 1 -> WARN: 'adb reverse' failed, fallback to 'adb forward' - -这是意料之中的 (由于旧版安卓的一个bug, 请参见 [#5]),但是在这种情况下,scrcpy会退回到另一种方法,这种方法应该可以起作用。 - -[#5]: https://github.com/Genymobile/scrcpy/issues/5 - - -### adb版本之间冲突 - -> adb server version (41) doesn't match this client (39); killing... - -同时使用多个版本的`adb`时会发生此错误。你必须查找使用不同`adb`版本的程序,并在所有地方使用相同版本的`adb`。 - -你可以覆盖另一个程序中的`adb`二进制文件,或者通过设置`ADB`环境变量来让 _scrcpy_ 使用特定的`adb`二进制文件。 - -```bash -set ADB=/path/to/your/adb -scrcpy -``` - - -### 设备断开连接 - -如果 _scrcpy_ 在警告“设备连接断开”的情况下自动中止,那就意味着`adb`连接已经断开了。 -请尝试使用另一条USB线或者电脑上的另一个USB接口。 -请参看 [#281] 和 [#283]。 - -[#281]: https://github.com/Genymobile/scrcpy/issues/281 -[#283]: https://github.com/Genymobile/scrcpy/issues/283 - -## 控制相关问题 - -### 鼠标和键盘不起作用 - - -在某些设备上,您可能需要启用一个选项以允许 [模拟输入][simulating input]。 - -在开发者选项中,打开: - -> **USB调试 (安全设置)** -> _允许通过USB调试修改权限或模拟点击_ - -[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -### 特殊字符不起作用 - -可输入的文本[被限制为ASCII字符][text-input]。也可以用一些小技巧输入一些[带重音符号的字符][accented-characters],但是仅此而已。参见[#37]。 - - -[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode -[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters -[#37]: https://github.com/Genymobile/scrcpy/issues/37 - - -## 客户端相关问题 - -### 效果很差 - -如果你的客户端窗口分辨率比你的设备屏幕小,则可能出现效果差的问题,尤其是在文本上(参见 [#40])。 - -[#40]: https://github.com/Genymobile/scrcpy/issues/40 - - -为了提升降尺度的质量,如果渲染器是OpenGL并且支持mip映射,就会自动开启三线性过滤。 - -在Windows上,你可能希望强制使用OpenGL: - -``` -scrcpy --render-driver=opengl -``` - -你可能还需要配置[缩放行为][scaling behavior]: - -> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings > -> Override high DPI scaling behavior > Scaling performed by: _Application_. - -[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723 - - -### Wayland相关的问题 - -在Linux上,SDL默认使用x11。可以通过`SDL_VIDEODRIVER`环境变量来更改[视频驱动][video driver]: - -[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver - -```bash -export SDL_VIDEODRIVER=wayland -scrcpy -``` - -在一些发行版上 (至少包括 Fedora), `libdecor` 包必须手动安装。 - -参见 [#2554] 和 [#2559]。 - -[#2554]: https://github.com/Genymobile/scrcpy/issues/2554 -[#2559]: https://github.com/Genymobile/scrcpy/issues/2559 - - -### KWin compositor 崩溃 - -在Plasma桌面中,当 _scrcpy_ 运行时,会禁用compositor。 - -一种解决方法是, [禁用 "Block compositing"][kwin]. - -[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 - - -## 崩溃 - -### 异常 -可能有很多原因。一个常见的原因是您的设备无法按给定清晰度进行编码: - -> ``` -> ERROR: Exception on thread Thread[main,5,main] -> android.media.MediaCodec$CodecException: Error 0xfffffc0e -> ... -> Exit due to uncaughtException in main thread: -> ERROR: Could not open video stream -> INFO: Initial texture: 1080x2336 -> ``` - -或者 - -> ``` -> ERROR: Exception on thread Thread[main,5,main] -> java.lang.IllegalStateException -> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) -> ``` - -请尝试使用更低的清晰度: - -``` -scrcpy -m 1920 -scrcpy -m 1024 -scrcpy -m 800 -``` - -你也可以尝试另一种 [编码器](README.md#encoder)。 - - -## Windows命令行 - -一些Windows用户不熟悉命令行。以下是如何打开终端并带参数执行`scrcpy`: - - 1. 按下 Windows+r,打开一个对话框。 - 2. 输入 `cmd` 并按 Enter,这样就打开了一个终端。 - 3. 通过输入以下命令,切换到你的 _scrcpy_ 所在的目录 (根据你的实际位置修改路径): - - ```bat - cd C:\Users\user\Downloads\scrcpy-win64-xxx - ``` - - 然后按 Enter - 4. 输入你的命令。比如: - - ```bat - scrcpy --record file.mkv - ``` - -如果你打算总是使用相同的参数,在`scrcpy`目录创建一个文件 `myscrcpy.bat` -(启用 [显示文件拓展名][show file extensions] 避免混淆),文件中包含你的命令。例如: - -```bat -scrcpy --prefer-text --turn-screen-off --stay-awake -``` - -然后双击刚刚创建的文件。 - -你也可以编辑 `scrcpy-console.bat` 或者 `scrcpy-noconsole.vbs`(的副本)来添加参数。 - -[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ diff --git a/LICENSE b/LICENSE index b320f699..d9326a74 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2024 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 deleted file mode 100644 index b4b16735..00000000 --- a/README.id.md +++ /dev/null @@ -1,696 +0,0 @@ -_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 deleted file mode 100644 index 37416f1d..00000000 --- a/README.it.md +++ /dev/null @@ -1,742 +0,0 @@ -_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 deleted file mode 100644 index e42c528e..00000000 --- a/README.jp.md +++ /dev/null @@ -1,725 +0,0 @@ -_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 deleted file mode 100644 index 31e38c6f..00000000 --- a/README.ko.md +++ /dev/null @@ -1,498 +0,0 @@ -_Only the original [README](README.md) is guaranteed to be up-to-date._ - -# scrcpy (v1.11) - -This document will be updated frequently along with the original Readme file -이 문서는 원어 리드미 파일의 업데이트에 따라 종종 업데이트 될 것입니다 - - 이 어플리케이션은 UBS ( 혹은 [TCP/IP][article-tcpip] ) 로 연결된 Android 디바이스를 화면에 보여주고 관리하는 것을 제공합니다. - _GNU/Linux_, _Windows_ 와 _macOS_ 상에서 작동합니다. - (아래 설명에서 디바이스는 안드로이드 핸드폰을 의미합니다.) - -[article-tcpip]:https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ - -![screenshot](https://github.com/Genymobile/scrcpy/blob/master/assets/screenshot-debian-600.jpg?raw=true) - -주요 기능은 다음과 같습니다. - - - **가벼움** (기본적이며 디바이스의 화면만을 보여줌) - - **뛰어난 성능** (30~60fps) - - **높은 품질** (1920×1080 이상의 해상도) - - **빠른 반응 속도** ([35~70ms][lowlatency]) - - **짧은 부팅 시간** (첫 사진을 보여주는데 최대 1초 소요됨) - - **장치 설치와는 무관함** (디바이스에 설치하지 않아도 됨) - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - - -## 요구사항 - -안드로이드 장치는 최소 API 21 (Android 5.0) 을 필요로 합니다. - -디바이스에 [adb debugging][enable-adb]이 가능한지 확인하십시오. - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -어떤 디바이스에서는, 키보드와 마우스를 사용하기 위해서 [추가 옵션][control] 이 필요하기도 합니다. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -## 앱 설치하기 - - -### Linux (리눅스) - -리눅스 상에서는 보통 [어플을 직접 설치][BUILD] 해야합니다. 어렵지 않으므로 걱정하지 않아도 됩니다. - -[BUILD]:https://github.com/Genymobile/scrcpy/blob/master/BUILD.md - -[Snap] 패키지가 가능합니다 : [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy - -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) - -Arch Linux에서, [AUR] 패키지가 가능합니다 : [`scrcpy`][aur-link]. - -[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ - -Gentoo에서 ,[Ebuild] 가 가능합니다 : [`scrcpy/`][ebuild-link]. - -[Ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - - -### Windows (윈도우) - -윈도우 상에서, 간단하게 설치하기 위해 종속성이 있는 사전 구축된 아카이브가 제공됩니다 (`adb` 포함) : -해당 파일은 Readme원본 링크를 통해서 다운로드가 가능합니다. - - [README](README.md#windows) - - -[어플을 직접 설치][BUILD] 할 수도 있습니다. - - -### macOS (맥 OS) - -이 어플리케이션은 아래 사항을 따라 설치한다면 [Homebrew] 에서도 사용 가능합니다 : - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -`PATH` 로부터 접근 가능한 `adb` 가 필요합니다. 아직 설치하지 않았다면 다음을 따라 설치해야 합니다 : - -```bash -brew cask install android-platform-tools -``` - -[어플을 직접 설치][BUILD] 할 수도 있습니다. - - -## 실행 - -안드로이드 디바이스를 연결하고 실행하십시오: - -```bash -scrcpy -``` - -다음과 같이 명령창 옵션 기능도 제공합니다. - -```bash -scrcpy --help -``` - -## 기능 - -### 캡쳐 환경 설정 - - -### 사이즈 재정의 - -가끔씩 성능을 향상시키기위해 안드로이드 디바이스를 낮은 해상도에서 미러링하는 것이 유용할 때도 있습니다. - -너비와 높이를 제한하기 위해 특정 값으로 지정할 수 있습니다 (e.g. 1024) : - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # 축약 버전 -``` - -이 외의 크기도 디바이스의 가로 세로 비율이 유지된 상태에서 계산됩니다. -이러한 방식으로 디바이스 상에서 1920×1080 는 모니터 상에서1024×576로 미러링될 것 입니다. - - -### bit-rate 변경 - -기본 bit-rate 는 8 Mbps입니다. 비디오 bit-rate 를 변경하기 위해선 다음과 같이 입력하십시오 (e.g. 2 Mbps로 변경): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # 축약 버전 -``` - -### 프레임 비율 제한 - -안드로이드 버전 10이상의 디바이스에서는, 다음의 명령어로 캡쳐 화면의 프레임 비율을 제한할 수 있습니다: - -```bash -scrcpy --max-fps 15 -``` - - -### Crop (잘라내기) - -디바이스 화면은 화면의 일부만 미러링하기 위해 잘라질 것입니다. - -예를 들어, *Oculus Go* 의 한 쪽 눈만 미러링할 때 유용합니다 : - -```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -scrcpy -c 1224:1440:0:0 # 축약 버전 -``` - -만약 `--max-size` 도 지정하는 경우, 잘라낸 다음에 재정의된 크기가 적용될 것입니다. - - -### 화면 녹화 - -미러링하는 동안 화면 녹화를 할 수 있습니다 : - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -녹화하는 동안 미러링을 멈출 수 있습니다 : - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# Ctrl+C 로 녹화를 중단할 수 있습니다. -# 윈도우 상에서 Ctrl+C 는 정상정으로 종료되지 않을 수 있으므로, 디바이스 연결을 해제하십시오. -``` - -"skipped frames" 은 모니터 화면에 보여지지 않았지만 녹화되었습니다 ( 성능 문제로 인해 ). 프레임은 디바이스 상에서 _타임 스탬프 ( 어느 시점에 데이터가 존재했다는 사실을 증명하기 위해 특정 위치에 시각을 표시 )_ 되었으므로, [packet delay -variation] 은 녹화된 파일에 영향을 끼치지 않습니다. - -[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation - -## 연결 - -### 무선연결 - -_Scrcpy_ 장치와 정보를 주고받기 위해 `adb` 를 사용합니다. `adb` 는 TCIP/IP 를 통해 디바이스와 [연결][connect] 할 수 있습니다 : - -1. 컴퓨터와 디바이스를 동일한 Wi-Fi 에 연결합니다. -2. 디바이스의 IP address 를 확인합니다 (설정 → 내 기기 → 상태 / 혹은 인터넷에 '내 IP'검색 시 확인 가능합니다. ). -3. TCP/IP 를 통해 디바이스에서 adb 를 사용할 수 있게 합니다: `adb tcpip 5555`. -4. 디바이스 연결을 해제합니다. -5. adb 를 통해 디바이스에 연결을 합니다\: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` 대신)_. -6. `scrcpy` 실행합니다. - -다음은 bit-rate 와 해상도를 줄이는데 유용합니다 : - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # 축약 버전 -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - - -### 여러 디바이스 사용 가능 - -만약에 여러 디바이스들이 `adb devices` 목록에 표시되었다면, _serial_ 을 명시해야합니다: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # 축약 버전 -``` - -_scrcpy_ 로 여러 디바이스를 연결해 사용할 수 있습니다. - - -#### SSH tunnel - -떨어져 있는 디바이스와 연결하기 위해서는, 로컬 `adb` client와 떨어져 있는 `adb` 서버를 연결해야 합니다. (디바이스와 클라이언트가 동일한 버전의 _adb_ protocol을 사용할 경우에 제공됩니다.): - -```bash -adb kill-server # 5037의 로컬 local adb server를 중단 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# 실행 유지 -``` - -다른 터미널에서는 : - -```bash -scrcpy -``` - -무선 연결과 동일하게, 화질을 줄이는 것이 나을 수 있습니다: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -## Window에서의 배치 - -### 맞춤형 window 제목 - -기본적으로, window의 이름은 디바이스의 모델명 입니다. -다음의 명령어를 통해 변경하세요. - -```bash -scrcpy --window-title 'My device' -``` - - -### 배치와 크기 - -초기 window창의 배치와 크기는 다음과 같이 설정할 수 있습니다: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - - -### 경계 없애기 - -윈도우 장식(경계선 등)을 다음과 같이 제거할 수 있습니다: - -```bash -scrcpy --window-borderless -``` - -### 항상 모든 윈도우 위에 실행창 고정 - -이 어플리케이션의 윈도우 창은 다음의 명령어로 다른 window 위에 디스플레이 할 수 있습니다: - -```bash -scrcpy --always-on-top -scrcpy -T # 축약 버전 -``` - -### 전체 화면 - -이 어플리케이션은 전체화면으로 바로 시작할 수 있습니다. - -```bash -scrcpy --fullscreen -scrcpy -f # short version -``` - -전체 화면은 `Ctrl`+`f`키로 끄거나 켤 수 있습니다. - - -## 다른 미러링 옵션 - -### 읽기 전용(Read-only) - -권한을 제한하기 위해서는 (디바이스와 관련된 모든 것: 입력 키, 마우스 이벤트 , 파일의 드래그 앤 드랍(drag&drop)): - -```bash -scrcpy --no-control -scrcpy -n -``` - -### 화면 끄기 - -미러링을 실행하는 와중에 디바이스의 화면을 끌 수 있게 하기 위해서는 -다음의 커맨드 라인 옵션을(command line option) 입력하세요: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -혹은 `Ctrl`+`o`을 눌러 언제든지 디바이스의 화면을 끌 수 있습니다. - -다시 화면을 켜기 위해서는`POWER` (혹은 `Ctrl`+`p`)를 누르세요. - - -### 유효기간이 지난 프레임 제공 (Render expired frames) - -디폴트로, 대기시간을 최소화하기 위해 _scrcpy_ 는 항상 마지막으로 디코딩된 프레임을 제공합니다 -과거의 프레임은 하나씩 삭제합니다. - -모든 프레임을 강제로 렌더링하기 위해서는 (대기 시간이 증가될 수 있습니다) -다음의 명령어를 사용하세요: - -```bash -scrcpy --render-expired-frames -``` - - -### 화면에 터치 나타내기 - -발표를 할 때, 물리적인 기기에 한 물리적 터치를 나타내는 것이 유용할 수 있습니다. - -안드로이드 운영체제는 이런 기능을 _Developers options_에서 제공합니다. - -_Scrcpy_ 는 이런 기능을 시작할 때와 종료할 때 옵션으로 제공합니다. - -```bash -scrcpy --show-touches -scrcpy -t -``` - -화면에 _물리적인 터치만_ 나타나는 것에 유의하세요 (손가락을 디바이스에 대는 행위). - - -### 입력 제어 - -#### 복사-붙여넣기 - -컴퓨터와 디바이스 양방향으로 클립보드를 복사하는 것이 가능합니다: - - - `Ctrl`+`c` 디바이스의 클립보드를 컴퓨터로 복사합니다; - - `Ctrl`+`Shift`+`v` 컴퓨터의 클립보드를 디바이스로 복사합니다; - - `Ctrl`+`v` 컴퓨터의 클립보드를 text event 로써 _붙여넣습니다_ ( 그러나, ASCII 코드가 아닌 경우 실행되지 않습니다 ) - -#### 텍스트 삽입 우선 순위 - -텍스트를 입력할 때 생성되는 두 가지의 [events][textevents] 가 있습니다: - - _key events_, 키가 눌려있는 지에 대한 신호; - - _text events_, 텍스트가 입력되었는지에 대한 신호. - -기본적으로, 글자들은 key event 를 이용해 입력되기 때문에, 키보드는 게임에서처럼 처리합니다 ( 보통 WASD 키에 대해서 ). - -그러나 이는 [issues 를 발생][prefertext]시킵니다. 이와 관련된 문제를 접할 경우, 아래와 같이 피할 수 있습니다: - -```bash -scrcpy --prefer-text -``` - -( 그러나 이는 게임에서의 처리를 중단할 수 있습니다 ) - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - - -### 파일 드랍 - -### APK 실행하기 - -APK를 실행하기 위해서는, APK file(파일명이`.apk`로 끝나는 파일)을 드래그하고 _scrcpy_ window에 드랍하세요 (drag and drop) - -시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. - -### 디바이스에 파일 push하기 - -디바이스의`/sdcard/`에 파일을 push하기 위해서는, -APK파일이 아닌 파일을_scrcpy_ window에 드래그하고 드랍하세요.(drag and drop). - -시각적인 피드백은 없고,log 하나가 콘솔에 출력될 것입니다. - -해당 디렉토리는 시작할 때 변경이 가능합니다: - -```bash -scrcpy --push-target /sdcard/foo/bar/ -``` - -### 오디오의 전달 - -_scrcpy_는 오디오를 직접 전달해주지 않습니다. [USBaudio] (Linux-only)를 사용하세요. - -추가적으로 [issue #14]를 참고하세요. - -[USBaudio]: https://github.com/rom1v/usbaudio -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - -## 단축키 - - | 실행내용 | 단축키 | 단축키 (macOS) - | -------------------------------------- |:----------------------------- |:----------------------------- - | 전체화면 모드로 전환 | `Ctrl`+`f` | `Cmd`+`f` - | window를 1:1비율로 전환하기(픽셀 맞춤) | `Ctrl`+`g` | `Cmd`+`g` - | 검은 공백 제거 위한 window 크기 조정 | `Ctrl`+`x` \| _Double-click¹_ | `Cmd`+`x` \| _Double-click¹_ - |`HOME` 클릭 | `Ctrl`+`h` \| _Middle-click_ | `Ctrl`+`h` \| _Middle-click_ - | `BACK` 클릭 | `Ctrl`+`b` \| _Right-click²_ | `Cmd`+`b` \| _Right-click²_ - | `APP_SWITCH` 클릭 | `Ctrl`+`s` | `Cmd`+`s` - | `MENU` 클릭 | `Ctrl`+`m` | `Ctrl`+`m` - | `VOLUME_UP` 클릭 | `Ctrl`+`↑` _(up)_ | `Cmd`+`↑` _(up)_ - | `VOLUME_DOWN` 클릭 | `Ctrl`+`↓` _(down)_ | `Cmd`+`↓` _(down)_ - | `POWER` 클릭 | `Ctrl`+`p` | `Cmd`+`p` - | 전원 켜기 | _Right-click²_ | _Right-click²_ - | 미러링 중 디바이스 화면 끄기 | `Ctrl`+`o` | `Cmd`+`o` - | 알림 패널 늘리기 | `Ctrl`+`n` | `Cmd`+`n` - | 알림 패널 닫기 | `Ctrl`+`Shift`+`n` | `Cmd`+`Shift`+`n` - | 디바이스의 clipboard 컴퓨터로 복사하기 | `Ctrl`+`c` | `Cmd`+`c` - | 컴퓨터의 clipboard 디바이스에 붙여넣기 | `Ctrl`+`v` | `Cmd`+`v` - | Copy computer clipboard to device | `Ctrl`+`Shift`+`v` | `Cmd`+`Shift`+`v` - | Enable/disable FPS counter (on stdout) | `Ctrl`+`i` | `Cmd`+`i` - -_¹검은 공백을 제거하기 위해서는 그 부분을 더블 클릭하세요_ -_²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상태에서는 뒤로 돌아갑니다. - -## 맞춤 경로 (custom path) - -특정한 _adb_ binary를 사용하기 위해서는, 그것의 경로를 환경변수로 설정하세요. -`ADB`: - - ADB=/path/to/adb scrcpy - -`scrcpy-server.jar`파일의 경로에 오버라이드 하기 위해서는, 그것의 경로를 `SCRCPY_SERVER_PATH`에 저장하세요. - -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 - - -## _scrcpy_ 인 이유? - -한 동료가 [gnirehtet]와 같이 발음하기 어려운 이름을 찾을 수 있는지 도발했습니다. - -[`strcpy`] 는 **str**ing을 copy하고; `scrcpy`는 **scr**een을 copy합니다. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - - - -## 빌드하는 방법? - -[BUILD]을 참고하세요. - -[BUILD]: BUILD.md - -## 흔한 issue - -[FAQ](FAQ.md)을 참고하세요. - - -## 개발자들 - -[developers page]를 참고하세요. - -[developers page]: DEVELOP.md - - -## 라이선스 - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-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. - -## 관련 글 (articles) - -- [scrcpy 소개][article-intro] -- [무선으로 연결하는 Scrcpy][article-tcpip] - -[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ -[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/README.md b/README.md index 1cd70a83..a672b327 100644 --- a/README.md +++ b/README.md @@ -1,865 +1,179 @@ -# scrcpy (v1.18) +**This GitHub repo () is the only official +source for the project. Do not download releases from random websites, even if +their name contains `scrcpy`.** -[Read in another language](#translations) +# scrcpy (v2.4) -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. -It works on _GNU/Linux_, _Windows_ and _macOS_. +scrcpy + +_pronounced "**scr**een **c**o**py**"_ + +This application mirrors Android devices (video and audio) connected via +USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the +device with the keyboard and the mouse of the computer. It does not require any +_root_ access. It works on _Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) It focuses on: - - **lightness** (native, displays only the device screen) - - **performance** (30~60fps) - - **quality** (1920×1080 or above) - - **low latency** ([35~70ms][lowlatency]) - - **low startup time** (~1 second to display the first image) - - **non-intrusiveness** (nothing is left installed on the device) + - **lightness**: native, displays only the device screen + - **performance**: 30~120fps, depending on the device + - **quality**: 1920×1080 or above + - **low latency**: [35~70ms][lowlatency] + - **low startup time**: ~1 second to display the first image + - **non-intrusiveness**: nothing is left installed on the Android device + - **user benefits**: no account, no ads, no internet required + - **freedom**: free and open source software [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 +Its features include: + - [audio forwarding](doc/audio.md) (Android 11+) + - [recording](doc/recording.md) + - mirroring with [Android device screen off](doc/device.md#turn-screen-off) + - [copy-paste](doc/control.md#copy-paste) in both directions + - [configurable quality](doc/video.md) + - [camera mirroring](doc/camera.md) (Android 12+) + - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) + - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID) + - [OTG mode](doc/otg.md) + - and more… -## Requirements - -The Android device requires at least API 21 (Android 5.0). - -Make sure you [enabled adb debugging][enable-adb] on your device(s). - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -On some devices, you also need to enable [an additional option][control] to -control it using keyboard and mouse. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - - -## 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 - -On Debian (_testing_ and _sid_ for now) and Ubuntu (20.04): - -``` -apt install scrcpy -``` - -A [Snap] package is available: [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy - -[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 -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ - -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] ([simplified -process][BUILD_simple]). - - -### Windows - -For Windows, for simplicity, a prebuilt archive with all the dependencies -(including `adb`) is available: - - - [`scrcpy-win64-v1.18.zip`][direct-win64] - _(SHA-256: 37212f5087fe6f3e258f1d44fa5c02207496b30e1d7ec442cbcf8358910a5c63)_ - -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-win64-v1.18.zip - -It is also available in [Chocolatey]: - -[Chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # if you don't have it yet -``` - -And in [Scoop]: - -```bash -scoop install scrcpy -scoop install adb # if you don't have it yet -``` - -[Scoop]: https://scoop.sh - -You can also [build the app manually][BUILD]. - - -### macOS - -The application is available in [Homebrew]. Just install it: - -[Homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -You need `adb`, accessible from your `PATH`. If you don't have it yet: - -```bash -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]. - - -## Run - -Plug an Android device, and execute: - -```bash -scrcpy -``` - -It accepts command-line arguments, listed by: - -```bash -scrcpy --help -``` - -## Features - -### Capture configuration - -#### Reduce size - -Sometimes, it is useful to mirror an Android device at a lower definition to -increase performance. - -To limit both the width and height to some value (e.g. 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # short version -``` - -The other dimension is computed to that the device aspect ratio is preserved. -That way, a device in 1920×1080 will be mirrored at 1024×576. - - -#### Change bit-rate - -The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # short version -``` - -#### Limit frame rate +[hid-keyboard]: doc/keyboard.md#physical-keyboard-simulation +[hid-mouse]: doc/mouse.md#physical-mouse-simulation -The capture frame rate can be limited: +## Prerequisites -```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. - -This is useful for example to mirror only one eye of the Oculus Go: - -```bash -scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0) -``` - -If `--max-size` is also specified, resizing is applied after cropping. - - -#### 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: - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -To disable mirroring while recording: - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# interrupt recording with 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. - -[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]. +The Android device requires at least API 21 (Android 5.0). -[OBS]: https://obsproject.com/ +[Audio forwarding](doc/audio.md) is supported for API >= 30 (Android 11+). +Make sure you [enabled USB debugging][enable-adb] on your device(s). -#### Buffering +[enable-adb]: https://developer.android.com/studio/debug/dev-options#enable -It is possible to add buffering. This increases latency but reduces jitter (see -#2464). +On some devices, you also need to enable [an additional option][control] `USB +debugging (Security Settings)` (this is an item different from `USB debugging`) +to control it using a keyboard and mouse. Rebooting the device is necessary once +this option is set. -The option is available for display buffering: +[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 -```bash -scrcpy --display-buffer=50 # add 50 ms buffering for display -``` +Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). -and V4L2 sink: -```bash -scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink -``` +## Get the app + - [Linux](doc/linux.md) + - [Windows](doc/windows.md) + - [macOS](doc/macos.md) -### Connection -#### Wireless +## Usage examples -_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a -device over TCP/IP: +There are a lot of options, [documented](#user-documentation) in separate pages. +Here are just some common examples. -1. Connect the device to the same Wi-Fi as your computer. -2. Get your device IP address, in Settings → About phone → Status, or by - executing this command: + - Capture the screen in H.265 (better quality), limit the size to 1920, limit + the frame rate to 60fps, disable audio, and control the device by simulating + a physical keyboard: ```bash - adb shell ip route | awk '{print $9}' + scrcpy --video-codec=h265 --max-size=1920 --max-fps=60 --no-audio --keyboard=uhid + scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version ``` -3. Enable adb over TCP/IP on your device: `adb tcpip 5555`. -4. Unplug your device. -5. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`)_. -6. Run `scrcpy` as usual. - -It may be useful to decrease the bit-rate and the definition: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # short version -``` - -[connect]: https://developer.android.com/studio/command-line/adb.html#wireless - - -#### Multi-devices - -If several devices are listed in `adb devices`, you must specify the _serial_: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # short version -``` - -If the device is connected over TCP/IP: - -```bash -scrcpy --serial 192.168.0.1:5555 -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 -a remote `adb` server (provided they use the same version of the _adb_ -protocol): - -```bash -adb kill-server # kill the local adb server on 5037 -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# keep this open -``` - -From another terminal: - -```bash -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: + - Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4 + file: -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Window configuration - -#### Title - -By default, the window title is the device model. It can be changed: - -```bash -scrcpy --window-title 'My device' -``` - -#### Position and size - -The initial window position and size may be specified: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - -#### Borderless - -To disable window decorations: - -```bash -scrcpy --window-borderless -``` - -#### Always on top - -To keep the scrcpy window always on top: - -```bash -scrcpy --always-on-top -``` - -#### Fullscreen - -The app may be started directly in fullscreen: - -```bash -scrcpy --fullscreen -scrcpy -f # short version -``` - -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 - -#### Read-only - -To disable controls (everything which can interact with the device: input keys, -mouse events, drag&drop files): - -```bash -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 -command-line option: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Or by pressing MOD+o at any time. - -To turn it back on, press MOD+Shift+o. - -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. - -It can also be useful to prevent the device from sleeping: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - - -#### Show touches - -For presentations, it may be useful to show physical touches (on the physical -device). - -Android provides this feature in _Developers options_. - -_Scrcpy_ provides an option to enable this feature on start and restore the -initial value on exit: - -```bash -scrcpy --show-touches -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 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 - -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. - - -#### Text injection preference - -There are two kinds of [events][textevents] generated when typing text: - - _key events_, signaling that a key is pressed or released; - - _text events_, signaling that a text has been entered. - -By default, letters are injected using key events, so that the keyboard behaves -as expected in games (typically for WASD keys). - -But this may [cause issues][prefertext]. If you encounter such a problem, you -can avoid it by: - -```bash -scrcpy --prefer-text -``` - -(but this will break keyboard behavior in games) - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[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 - -To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ -window. - -There is no visual feedback, a log is printed to the console. - - -#### Push file to device - -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/Movies/ -``` - - -### Audio forwarding - -Audio is not forwarded by _scrcpy_. Use [sndcpy]. - -Also see [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - - -## Shortcuts - -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._ -_³4th and 5th mouse buttons, if your mouse has them._ -_⁴Only on Android >= 7._ + ```bash + scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --record=file.mp4 + ``` -Shortcuts with repeated keys are executted by releasing and pressing the key a -second time. For example, to execute "Expand settings panel": + - Capture the device front camera and expose it as a webcam on the computer (on + Linux): - 1. Press and keep pressing MOD. - 2. Then double-press n. - 3. Finally, release MOD. + ```bash + scrcpy --video-source=camera --camera-size=1920x1080 --camera-facing=front --v4l2-sink=/dev/video2 --no-playback + ``` -All Ctrl+_key_ shortcuts are forwarded to the device, so they are -handled by the active application. + - Control the device without mirroring by simulating a physical keyboard and + mouse (USB debugging not required): + ```bash + scrcpy --otg + ``` -## Custom paths +## User documentation -To use a specific _adb_ binary, configure its path in the environment variable -`ADB`: +The application provides a lot of features and configuration options. They are +documented in the following pages: -```bash -ADB=/path/to/adb scrcpy -``` + - [Connection](doc/connection.md) + - [Video](doc/video.md) + - [Audio](doc/audio.md) + - [Control](doc/control.md) + - [Keyboard](doc/keyboard.md) + - [Mouse](doc/mouse.md) + - [Device](doc/device.md) + - [Window](doc/window.md) + - [Recording](doc/recording.md) + - [Tunnels](doc/tunnels.md) + - [OTG](doc/otg.md) + - [Camera](doc/camera.md) + - [Video4Linux](doc/v4l2.md) + - [Shortcuts](doc/shortcuts.md) -To override the path of the `scrcpy-server` file, configure its path in -`SCRCPY_SERVER_PATH`. -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 +## Resources + - [FAQ](FAQ.md) + - [Translations][wiki] (not necessarily up to date) + - [Build instructions](doc/build.md) + - [Developers](doc/develop.md) -## Why _scrcpy_? +[wiki]: https://github.com/Genymobile/scrcpy/wiki -A colleague challenged me to find a name as unpronounceable as [gnirehtet]. -[`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een. +## Articles -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html +- [Introducing scrcpy][article-intro] +- [Scrcpy now works wirelessly][article-tcpip] +- [Scrcpy 2.0, with audio][article-scrcpy2] +[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ +[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ +[article-scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ -## How to build? +## Contact -See [BUILD]. +If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue]. +[issue]: https://github.com/Genymobile/scrcpy/issues -## Common issues +For general questions or discussions, you can also use: -See the [FAQ](FAQ.md). + - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) + - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) -## Developers +## Donate -Read the [developers page]. +I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_. -[developers page]: DEVELOP.md +If you appreciate this application, you can [support my open source +work][donate]: + - [GitHub Sponsors](https://github.com/sponsors/rom1v) + - [Liberapay](https://liberapay.com/rom1v/) + - [PayPal](https://paypal.me/rom2v) +[donate]: https://blog.rom1v.com/about/#support-my-open-source-work ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont + Copyright (C) 2018-2024 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -872,27 +186,3 @@ Read the [developers page]. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -## Articles - -- [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/ - -## 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) -- [Turkish (Turkish, `tr`) - v1.18](README.tr.md) - -Only this README file is guaranteed to be up-to-date. diff --git a/README.pt-br.md b/README.pt-br.md deleted file mode 100644 index 3549f0fb..00000000 --- a/README.pt-br.md +++ /dev/null @@ -1,792 +0,0 @@ -_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 deleted file mode 100644 index 6f76a7be..00000000 --- a/README.sp.md +++ /dev/null @@ -1,743 +0,0 @@ -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.tr.md b/README.tr.md deleted file mode 100644 index 15c56b27..00000000 --- a/README.tr.md +++ /dev/null @@ -1,824 +0,0 @@ -# scrcpy (v1.18) - -Bu uygulama Android cihazların USB (ya da [TCP/IP][article-tcpip]) üzerinden -görüntülenmesini ve kontrol edilmesini sağlar. _root_ erişimine ihtiyaç duymaz. -_GNU/Linux_, _Windows_ ve _macOS_ sistemlerinde çalışabilir. - -![screenshot](assets/screenshot-debian-600.jpg) - -Öne çıkan özellikler: - -- **hafiflik** (doğal, sadece cihazın ekranını gösterir) -- **performans** (30~60fps) -- **kalite** (1920×1080 ya da üzeri) -- **düşük gecikme süresi** ([35~70ms][lowlatency]) -- **düşük başlangıç süresi** (~1 saniye ilk kareyi gösterme süresi) -- **müdaheleci olmama** (cihazda kurulu yazılım kalmaz) - -[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 - -## Gereksinimler - -Android cihaz en düşük API 21 (Android 5.0) olmalıdır. - -[Adb hata ayıklamasının][enable-adb] cihazınızda aktif olduğundan emin olun. - -[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling - -Bazı cihazlarda klavye ve fare ile kontrol için [ilave bir seçenek][control] daha -etkinleştirmeniz gerekebilir. - -[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 - -## Uygulamayı indirin - -Packaging status - -### Özet - -- Linux: `apt install scrcpy` -- Windows: [indir][direct-win64] -- macOS: `brew install scrcpy` - -Kaynak kodu derle: [BUILD] ([basitleştirilmiş süreç][build_simple]) - -[build]: BUILD.md -[build_simple]: BUILD.md#simple - -### Linux - -Debian (şimdilik _testing_ ve _sid_) ve Ubuntu (20.04) için: - -``` -apt install scrcpy -``` - -[Snap] paketi: [`scrcpy`][snap-link]. - -[snap-link]: https://snapstats.org/snaps/scrcpy -[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) - -Fedora için, [COPR] paketi: [`scrcpy`][copr-link]. - -[copr]: https://fedoraproject.org/wiki/Category:Copr -[copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ - -Arch Linux için, [AUR] paketi: [`scrcpy`][aur-link]. - -[aur]: https://wiki.archlinux.org/index.php/Arch_User_Repository -[aur-link]: https://aur.archlinux.org/packages/scrcpy/ - -Gentoo için, [Ebuild] mevcut: [`scrcpy/`][ebuild-link]. - -[ebuild]: https://wiki.gentoo.org/wiki/Ebuild -[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy - -Ayrıca [uygulamayı el ile de derleyebilirsiniz][build] ([basitleştirilmiş süreç][build_simple]). - -### Windows - -Windows için (`adb` dahil) tüm gereksinimleri ile derlenmiş bir arşiv mevcut: - - - [README](README.md#windows) - -[Chocolatey] ile kurulum: - -[chocolatey]: https://chocolatey.org/ - -```bash -choco install scrcpy -choco install adb # if you don't have it yet -``` - -[Scoop] ile kurulum: - -```bash -scoop install scrcpy -scoop install adb # if you don't have it yet -``` - -[scoop]: https://scoop.sh - -Ayrıca [uygulamayı el ile de derleyebilirsiniz][build]. - -### macOS - -Uygulama [Homebrew] içerisinde mevcut. Sadece kurun: - -[homebrew]: https://brew.sh/ - -```bash -brew install scrcpy -``` - -`adb`, `PATH` içerisinden erişilebilir olmalıdır. Eğer değilse: - -```bash -brew install android-platform-tools -``` - -[MacPorts] kullanılarak adb ve uygulamanın birlikte kurulumu yapılabilir: - -```bash -sudo port install scrcpy -``` - -[macports]: https://www.macports.org/ - -Ayrıca [uygulamayı el ile de derleyebilirsiniz][build]. - -## Çalıştırma - -Android cihazınızı bağlayın ve aşağıdaki komutu çalıştırın: - -```bash -scrcpy -``` - -Komut satırı argümanları aşağıdaki komut ile listelenebilir: - -```bash -scrcpy --help -``` - -## Özellikler - -### Ekran yakalama ayarları - -#### Boyut azaltma - -Bazen, Android cihaz ekranını daha düşük seviyede göstermek performansı artırabilir. - -Hem genişliği hem de yüksekliği bir değere sabitlemek için (ör. 1024): - -```bash -scrcpy --max-size 1024 -scrcpy -m 1024 # kısa versiyon -``` - -Diğer boyut en-boy oranı korunacak şekilde hesaplanır. -Bu şekilde ekran boyutu 1920x1080 olan bir cihaz 1024x576 olarak görünür. - -#### Bit-oranı değiştirme - -Varsayılan bit-oranı 8 Mbps'dir. Değiştirmek için (ör. 2 Mbps): - -```bash -scrcpy --bit-rate 2M -scrcpy -b 2M # kısa versiyon -``` - -#### Çerçeve oranı sınırlama - -Ekran yakalama için maksimum çerçeve oranı için sınır koyulabilir: - -```bash -scrcpy --max-fps 15 -``` - -Bu özellik Android 10 ve sonrası sürümlerde resmi olarak desteklenmektedir, -ancak daha önceki sürümlerde çalışmayabilir. - -#### Kesme - -Cihaz ekranının sadece bir kısmı görünecek şekilde kesilebilir. - -Bu özellik Oculus Go'nun bir gözünü yakalamak gibi durumlarda kullanışlı olur: - -```bash -scrcpy --crop 1224:1440:0:0 # (0,0) noktasından 1224x1440 -``` - -Eğer `--max-size` belirtilmişse yeniden boyutlandırma kesme işleminden sonra yapılır. - -#### Video yönünü kilitleme - -Videonun yönünü kilitlemek için: - -```bash -scrcpy --lock-video-orientation # başlangıç yönü -scrcpy --lock-video-orientation=0 # doğal yön -scrcpy --lock-video-orientation=1 # 90° saatin tersi yönü -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° saat yönü -``` - -Bu özellik kaydetme yönünü de etkiler. - -[Pencere ayrı olarak döndürülmüş](#rotation) olabilir. - -#### Kodlayıcı - -Bazı cihazlar birden fazla kodlayıcıya sahiptir, ve bunların bazıları programın -kapanmasına sebep olabilir. Bu durumda farklı bir kodlayıcı seçilebilir: - -```bash -scrcpy --encoder OMX.qcom.video.encoder.avc -``` - -Mevcut kodlayıcıları listelemek için geçerli olmayan bir kodlayıcı ismi girebilirsiniz, -hata mesajı mevcut kodlayıcıları listeleyecektir: - -```bash -scrcpy --encoder _ -``` - -### Yakalama - -#### Kaydetme - -Ekran yakalama sırasında kaydedilebilir: - -```bash -scrcpy --record file.mp4 -scrcpy -r file.mkv -``` - -Yakalama olmadan kayıt için: - -```bash -scrcpy --no-display --record file.mp4 -scrcpy -Nr file.mkv -# Ctrl+C ile kayıt kesilebilir -``` - -"Atlanan kareler" gerçek zamanlı olarak gösterilmese (performans sebeplerinden ötürü) dahi kaydedilir. -Kareler cihazda _zamandamgası_ ile saklanır, bu sayede [paket gecikme varyasyonu] -kayıt edilen dosyayı etkilemez. - -[paket gecikme varyasyonu]: https://en.wikipedia.org/wiki/Packet_delay_variation - -#### v4l2loopback - -Linux'ta video akışı bir v4l2 loopback cihazına gönderilebilir. Bu sayede Android -cihaz bir web kamerası gibi davranabilir. - -Bu işlem için `v4l2loopback` modülü kurulu olmalıdır: - -```bash -sudo apt install v4l2loopback-dkms -``` - -v4l2 cihazı oluşturmak için: - -```bash -sudo modprobe v4l2loopback -``` - -Bu komut `/dev/videoN` adresinde `N` yerine bir tamsayı koyarak yeni bir video -cihazı oluşturacaktır. -(birden fazla cihaz oluşturmak veya spesifik ID'ye sahip cihazlar için -diğer [seçenekleri](https://github.com/umlaeute/v4l2loopback#options) inceleyebilirsiniz.) - -Aktif cihazları listelemek için: - -```bash -# v4l-utils paketi ile -v4l2-ctl --list-devices - -# daha basit ama yeterli olabilecek şekilde -ls /dev/video* -``` - -v4l2 kullanarak scrpy kullanmaya başlamak için: - -```bash -scrcpy --v4l2-sink=/dev/videoN -scrcpy --v4l2-sink=/dev/videoN --no-display # ayna penceresini kapatarak -scrcpy --v4l2-sink=/dev/videoN -N # kısa versiyon -``` - -(`N` harfini oluşturulan cihaz ID numarası ile değiştirin. `ls /dev/video*` cihaz ID'lerini görebilirsiniz.) - -Aktifleştirildikten sonra video akışını herhangi bir v4l2 özellikli araçla açabilirsiniz: - -```bash -ffplay -i /dev/videoN -vlc v4l2:///dev/videoN # VLC kullanırken yükleme gecikmesi olabilir -``` - -Örneğin, [OBS] ile video akışını kullanabilirsiniz. - -[obs]: https://obsproject.com/ - -### Bağlantı - -#### Kablosuz - -_Scrcpy_ cihazla iletişim kurmak için `adb`'yi kullanır, Ve `adb` -bir cihaza TCP/IP kullanarak [bağlanabilir]. - -1. Cihazınızı bilgisayarınızla aynı Wi-Fi ağına bağlayın. -2. Cihazınızın IP adresini bulun. Ayarlar → Telefon Hakkında → Durum sekmesinden veya - aşağıdaki komutu çalıştırarak öğrenebilirsiniz: - - ```bash - adb shell ip route | awk '{print $9}' - ``` - -3. Cihazınızda TCP/IP üzerinden adb kullanımını etkinleştirin: `adb tcpip 5555`. -4. Cihazınızı bilgisayarınızdan sökün. -5. Cihazınıza bağlanın: `adb connect DEVICE_IP:5555` _(`DEVICE_IP` değerini değiştirin)_. -6. `scrcpy` komutunu normal olarak çalıştırın. - -Bit-oranını ve büyüklüğü azaltmak yararlı olabilir: - -```bash -scrcpy --bit-rate 2M --max-size 800 -scrcpy -b2M -m800 # kısa version -``` - -[bağlanabilir]: https://developer.android.com/studio/command-line/adb.html#wireless - -#### Birden fazla cihaz - -Eğer `adb devices` komutu birden fazla cihaz listeliyorsa _serial_ değerini belirtmeniz gerekir: - -```bash -scrcpy --serial 0123456789abcdef -scrcpy -s 0123456789abcdef # kısa versiyon -``` - -Eğer cihaz TCP/IP üzerinden bağlanmışsa: - -```bash -scrcpy --serial 192.168.0.1:5555 -scrcpy -s 192.168.0.1:5555 # kısa version -``` - -Birden fazla cihaz için birden fazla _scrcpy_ uygulaması çalıştırabilirsiniz. - -#### Cihaz bağlantısı ile otomatik başlatma - -[AutoAdb] ile yapılabilir: - -```bash -autoadb scrcpy -s '{}' -``` - -[autoadb]: https://github.com/rom1v/autoadb - -#### SSH Tünel - -Uzaktaki bir cihaza erişmek için lokal `adb` istemcisi, uzaktaki bir `adb` sunucusuna -(aynı _adb_ sürümünü kullanmak şartı ile) bağlanabilir : - -```bash -adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat -ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer -# bunu açık tutun -``` - -Başka bir terminalde: - -```bash -scrcpy -``` - -Uzaktan port yönlendirme ileri yönlü bağlantı kullanabilirsiniz -(`-R` yerine `-L` olduğuna dikkat edin): - -```bash -adb kill-server # 5037 portunda çalışan lokal adb sunucusunu kapat -ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer -# bunu açık tutun -``` - -Başka bir terminalde: - -```bash -scrcpy --force-adb-forward -``` - -Kablosuz bağlantı gibi burada da kalite düşürmek faydalı olabilir: - -``` -scrcpy -b2M -m800 --max-fps 15 -``` - -### Pencere ayarları - -#### İsim - -Cihaz modeli varsayılan pencere ismidir. Değiştirmek için: - -```bash -scrcpy --window-title 'Benim cihazım' -``` - -#### Konum ve - -Pencerenin başlangıç konumu ve boyutu belirtilebilir: - -```bash -scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 -``` - -#### Kenarlıklar - -Pencere dekorasyonunu kapatmak için: - -```bash -scrcpy --window-borderless -``` - -#### Her zaman üstte - -Scrcpy penceresini her zaman üstte tutmak için: - -```bash -scrcpy --always-on-top -``` - -#### Tam ekran - -Uygulamayı tam ekran başlatmak için: - -```bash -scrcpy --fullscreen -scrcpy -f # kısa versiyon -``` - -Tam ekran MOD+f ile dinamik olarak değiştirilebilir. - -#### Döndürme - -Pencere döndürülebilir: - -```bash -scrcpy --rotation 1 -``` - -Seçilebilecek değerler: - -- `0`: döndürme yok -- `1`: 90 derece saat yönünün tersi -- `2`: 180 derece -- `3`: 90 derece saat yönü - -Döndürme MOD+_(sol)_ ve -MOD+ _(sağ)_ ile dinamik olarak değiştirilebilir. - -_scrcpy_'de 3 farklı döndürme olduğuna dikkat edin: - -- MOD+r cihazın yatay veya dikey modda çalışmasını sağlar. - (çalışan uygulama istenilen oryantasyonda çalışmayı desteklemiyorsa döndürme - işlemini reddedebilir.) -- [`--lock-video-orientation`](#lock-video-orientation) görüntü yakalama oryantasyonunu - (cihazdan bilgisayara gelen video akışının oryantasyonu) değiştirir. Bu kayıt işlemini - etkiler. -- `--rotation` (or MOD+/MOD+) - pencere içeriğini dönderir. Bu sadece canlı görüntüyü etkiler, kayıt işlemini etkilemez. - -### Diğer ekran yakalama seçenekleri - -#### Yazma korumalı - -Kontrolleri devre dışı bırakmak için (cihazla etkileşime geçebilecek her şey: klavye ve -fare girdileri, dosya sürükleyip bırakma): - -```bash -scrcpy --no-control -scrcpy -n -``` - -#### Ekran - -Eğer cihazın birden fazla ekranı varsa hangi ekranın kullanılacağını seçebilirsiniz: - -```bash -scrcpy --display 1 -``` - -Kullanılabilecek ekranları listelemek için: - -```bash -adb shell dumpsys display # çıktı içerisinde "mDisplayId=" terimini arayın -``` - -İkinci ekran ancak cihaz Android sürümü 10 veya üzeri olmalıdır (değilse yazma korumalı -olarak görüntülenir). - -#### Uyanık kalma - -Cihazın uyku moduna girmesini engellemek için: - -```bash -scrcpy --stay-awake -scrcpy -w -``` - -scrcpy kapandığında cihaz başlangıç durumuna geri döner. - -#### Ekranı kapatma - -Ekran yakalama sırasında cihazın ekranı kapatılabilir: - -```bash -scrcpy --turn-screen-off -scrcpy -S -``` - -Ya da MOD+o kısayolunu kullanabilirsiniz. - -Tekrar açmak için ise MOD+Shift+o tuşlarına basın. - -Android'de, `GÜÇ` tuşu her zaman ekranı açar. Eğer `GÜÇ` sinyali scrcpy ile -gönderilsiyse (sağ tık veya MOD+p), ekran kısa bir gecikme -ile kapanacaktır. Fiziksel `GÜÇ` tuşuna basmak hala ekranın açılmasına sebep olacaktır. - -Bu cihazın uykuya geçmesini engellemek için kullanılabilir: - -```bash -scrcpy --turn-screen-off --stay-awake -scrcpy -Sw -``` - -#### Dokunuşları gösterme - -Sunumlar sırasında fiziksel dokunuşları (fiziksel cihazdaki) göstermek -faydalı olabilir. - -Android'de bu özellik _Geliştici seçenekleri_ içerisinde bulunur. - -_Scrcpy_ bu özelliği çalışırken etkinleştirebilir ve kapanırken eski -haline geri getirebilir: - -```bash -scrcpy --show-touches -scrcpy -t -``` - -Bu opsiyon sadece _fiziksel_ dokunuşları (cihaz ekranındaki) gösterir. - -#### Ekran koruyucuyu devre dışı bırakma - -Scrcpy varsayılan ayarlarında ekran koruyucuyu devre dışı bırakmaz. - -Bırakmak için: - -```bash -scrcpy --disable-screensaver -``` - -### Girdi kontrolü - -#### Cihaz ekranını dönderme - -MOD+r tuşları ile yatay ve dikey modlar arasında -geçiş yapabilirsiniz. - -Bu kısayol ancak çalışan uygulama desteklediği takdirde ekranı döndürecektir. - -#### Kopyala yapıştır - -Ne zaman Android cihazdaki pano değişse bilgisayardaki pano otomatik olarak -senkronize edilir. - -Tüm Ctrl kısayolları cihaza iletilir: - -- Ctrl+c genelde kopyalar -- Ctrl+x genelde keser -- Ctrl+v genelde yapıştırır (bilgisayar ve cihaz arasındaki - pano senkronizasyonundan sonra) - -Bu kısayollar genelde beklediğiniz gibi çalışır. - -Ancak kısayolun gerçekten yaptığı eylemi açık olan uygulama belirler. -Örneğin, _Termux_ Ctrl+c ile kopyalama yerine -SIGINT sinyali gönderir, _K-9 Mail_ ise yeni mesaj oluşturur. - -Bu tip durumlarda kopyalama, kesme ve yapıştırma için (Android versiyon 7 ve -üstü): - -- MOD+c `KOPYALA` -- MOD+x `KES` -- MOD+v `YAPIŞTIR` (bilgisayar ve cihaz arasındaki - pano senkronizasyonundan sonra) - -Bunlara ek olarak, MOD+Shift+v tuşları -bilgisayar pano içeriğini tuş basma eylemleri şeklinde gönderir. Bu metin -yapıştırmayı desteklemeyen (_Termux_ gibi) uygulamar için kullanışlıdır, -ancak ASCII olmayan içerikleri bozabilir. - -**UYARI:** Bilgisayar pano içeriğini cihaza yapıştırmak -(Ctrl+v ya da MOD+v tuşları ile) -içeriği cihaz panosuna kopyalar. Sonuç olarak, herhangi bir Android uygulaması -içeriğe erişebilir. Hassas içerikler (parolalar gibi) için bu özelliği kullanmaktan -kaçının. - -Bazı cihazlar pano değişikleri konusunda beklenilen şekilde çalışmayabilir. -Bu durumlarda `--legacy-paste` argümanı kullanılabilir. Bu sayede -Ctrl+v ve MOD+v tuşları da -pano içeriğini tuş basma eylemleri şeklinde gönderir -(MOD+Shift+v ile aynı şekilde). - -#### İki parmak ile yakınlaştırma - -"İki parmak ile yakınlaştırma" için: Ctrl+_tıkla-ve-sürükle_. - -Daha açıklayıcı şekilde, Ctrl tuşuna sol-tık ile birlikte basılı -tutun. Sol-tık serbest bırakılıncaya kadar yapılan tüm fare hareketleri -ekran içeriğini ekranın merkezini baz alarak dönderir, büyütür veya küçültür -(eğer uygulama destekliyorsa). - -Scrcpy ekranın merkezinde bir "sanal parmak" varmış gibi davranır. - -#### Metin gönderme tercihi - -Metin girilirken ili çeşit [eylem][textevents] gerçekleştirilir: - -- _tuş eylemleri_, bir tuşa basıldığı sinyalini verir; -- _metin eylemleri_, bir metin girildiği sinyalini verir. - -Varsayılan olarak, harfler tuş eylemleri kullanılarak gönderilir. Bu sayede -klavye oyunlarda beklenilene uygun olarak çalışır (Genelde WASD tuşları). - -Ancak bu [bazı problemlere][prefertext] yol açabilir. Eğer bu problemler ile -karşılaşırsanız metin eylemlerini tercih edebilirsiniz: - -```bash -scrcpy --prefer-text -``` - -(Ama bu oyunlardaki klavye davranışlarını bozacaktır) - -[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input -[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 - -#### Tuş tekrarı - -Varsayılan olarak, bir tuşa basılı tutmak tuş eylemini tekrarlar. Bu durum -bazı oyunlarda problemlere yol açabilir. - -Tuş eylemlerinin tekrarını kapatmak için: - -```bash -scrcpy --no-key-repeat -``` - -#### Sağ-tık ve Orta-tık - -Varsayılan olarak, sağ-tık GERİ (ya da GÜÇ açma) eylemlerini, orta-tık ise -ANA EKRAN eylemini tetikler. Bu kısayolları devre dışı bırakmak için: - -```bash -scrcpy --forward-all-clicks -``` - -### Dosya bırakma - -#### APK kurulumu - -APK kurmak için, bilgisayarınızdaki APK dosyasını (`.apk` ile biten) _scrcpy_ -penceresine sürükleyip bırakın. - -Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır. - -#### Dosyayı cihaza gönderme - -Bir dosyayı cihazdaki `/sdcard/Download/` dizinine atmak için, (APK olmayan) -bir dosyayı _scrcpy_ penceresine sürükleyip bırakın. - -Bu eylem görsel bir geri dönüt oluşturmaz, konsola log yazılır. - -Hedef dizin uygulama başlatılırken değiştirilebilir: - -```bash -scrcpy --push-target=/sdcard/Movies/ -``` - -### Ses iletimi - -_Scrcpy_ ses iletimi yapmaz. Yerine [sndcpy] kullanabilirsiniz. - -Ayrıca bakınız [issue #14]. - -[sndcpy]: https://github.com/rom1v/sndcpy -[issue #14]: https://github.com/Genymobile/scrcpy/issues/14 - -## Kısayollar - -Aşağıdaki listede, MOD kısayol tamamlayıcısıdır. Varsayılan olarak -(sol) Alt veya (sol) Super tuşudur. - -Bu tuş `--shortcut-mod` argümanı kullanılarak `lctrl`, `rctrl`, -`lalt`, `ralt`, `lsuper` ve `rsuper` tuşlarından biri ile değiştirilebilir. -Örneğin: - -```bash -# Sağ Ctrl kullanmak için -scrcpy --shortcut-mod=rctrl - -# Sol Ctrl, Sol Alt veya Sol Super tuşlarından birini kullanmak için -scrcpy --shortcut-mod=lctrl+lalt,lsuper -``` - -_[Super] tuşu genelde Windows veya Cmd tuşudur._ - -[super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button) - -| Action | Shortcut | -| ------------------------------------------------ | :-------------------------------------------------------- | -| Tam ekran modunu değiştirme | MOD+f | -| Ekranı sola çevirme | MOD+ _(sol)_ | -| Ekranı sağa çevirme | MOD+ _(sağ)_ | -| Pencereyi 1:1 oranına çevirme (pixel-perfect) | MOD+g | -| Penceredeki siyah kenarlıkları kaldırma | MOD+w \| _Çift-sol-tık¹_ | -| `ANA EKRAN` tuşu | MOD+h \| _Orta-tık_ | -| `GERİ` tuşu | MOD+b \| _Sağ-tık²_ | -| `UYGULAMA_DEĞİŞTİR` tuşu | MOD+s \| _4.tık³_ | -| `MENÜ` tuşu (ekran kilidini açma) | MOD+m | -| `SES_AÇ` tuşu | MOD+ _(yukarı)_ | -| `SES_KIS` tuşu | MOD+ _(aşağı)_ | -| `GÜÇ` tuşu | MOD+p | -| Gücü açma | _Sağ-tık²_ | -| Cihaz ekranını kapatma (ekran yakalama durmadan) | MOD+o | -| Cihaz ekranını açma | MOD+Shift+o | -| Cihaz ekranını dönderme | MOD+r | -| Bildirim panelini genişletme | MOD+n \| _5.tık³_ | -| Ayarlar panelini genişletme | MOD+n+n \| _Çift-5.tık³_ | -| Panelleri kapatma | MOD+Shift+n | -| Panoya kopyalama⁴ | MOD+c | -| Panoya kesme⁴ | MOD+x | -| Panoları senkronize ederek yapıştırma⁴ | MOD+v | -| Bilgisayar panosundaki metini girme | MOD+Shift+v | -| FPS sayacını açma/kapatma (terminalde) | MOD+i | -| İki parmakla yakınlaştırma | Ctrl+_tıkla-ve-sürükle_ | - -_¹Siyah kenarlıkları silmek için üzerine çift tıklayın._ -_²Sağ-tık ekran kapalıysa açar, değilse GERİ sinyali gönderir._ -_³4. ve 5. fare tuşları (eğer varsa)._ -_⁴Sadece Android 7 ve üzeri versiyonlarda._ - -Tekrarlı tuşu olan kısayollar tuş bırakılıp tekrar basılarak tekrar çalıştırılır. -Örneğin, "Ayarlar panelini genişletmek" için: - -1. MOD tuşuna basın ve basılı tutun. -2. n tuşuna iki defa basın. -3. MOD tuşuna basmayı bırakın. - -Tüm Ctrl+_tuş_ kısayolları cihaza gönderilir. Bu sayede istenilen komut -uygulama tarafından çalıştırılır. - -## Özel dizinler - -Varsayılandan farklı bir _adb_ programı çalıştırmak için `ADB` ortam değişkenini -ayarlayın: - -```bash -ADB=/path/to/adb scrcpy -``` - -`scrcpy-server` programının dizinini değiştirmek için `SCRCPY_SERVER_PATH` -değişkenini ayarlayın. - -[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 - -## Neden _scrcpy_? - -Bir meslektaşım [gnirehtet] gibi söylenmesi zor bir isim bulmam için bana meydan okudu. - -[`strcpy`] **str**ing kopyalıyor; `scrcpy` **scr**een kopyalıyor. - -[gnirehtet]: https://github.com/Genymobile/gnirehtet -[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html - -## Nasıl derlenir? - -Bakınız [BUILD]. - -## Yaygın problemler - -Bakınız [FAQ](FAQ.md). - -## Geliştiriciler - -[Geliştiriciler sayfası]nı okuyun. - -[geliştiriciler sayfası]: DEVELOP.md - -## Lisans - - Copyright (C) 2018 Genymobile - Copyright (C) 2018-2021 Romain Vimont - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -## Makaleler - -- [Introducing scrcpy][article-intro] -- [Scrcpy now works wirelessly][article-tcpip] - -[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/ -[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/ diff --git a/README.zh-Hans.md b/README.zh-Hans.md deleted file mode 100644 index bdd8023c..00000000 --- a/README.zh-Hans.md +++ /dev/null @@ -1,732 +0,0 @@ -_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 deleted file mode 100644 index c0e30254..00000000 --- a/README.zh-Hant.md +++ /dev/null @@ -1,702 +0,0 @@ -_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/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy new file mode 100644 index 00000000..e6b2c91a --- /dev/null +++ b/app/data/bash-completion/scrcpy @@ -0,0 +1,205 @@ +_scrcpy() { + local cur prev words cword + local opts=" + --always-on-top + --audio-bit-rate= + --audio-buffer= + --audio-codec= + --audio-codec-options= + --audio-encoder= + --audio-source= + --audio-output-buffer= + -b --video-bit-rate= + --camera-ar= + --camera-id= + --camera-facing= + --camera-fps= + --camera-high-speed + --camera-size= + --crop= + -d --select-usb + --disable-screensaver + --display-buffer= + --display-id= + --display-orientation= + -e --select-tcpip + -f --fullscreen + --force-adb-forward + --forward-all-clicks + -h --help + -K + --keyboard= + --kill-adb-on-close + --legacy-paste + --list-camera-sizes + --list-cameras + --list-displays + --list-encoders + --lock-video-orientation + --lock-video-orientation= + -m --max-size= + -M + --max-fps= + --mouse= + -n --no-control + -N --no-playback + --no-audio + --no-audio-playback + --no-cleanup + --no-clipboard-autosync + --no-downsize-on-error + --no-key-repeat + --no-mipmaps + --no-power-on + --no-video + --no-video-playback + --orientation= + --otg + -p --port= + --pause-on-exit + --pause-on-exit= + --power-off-on-close + --prefer-text + --print-fps + --push-target= + -r --record= + --raw-key-events + --record-format= + --record-orientation= + --render-driver= + --require-audio + --rotation= + -s --serial= + -S --turn-screen-off + --shortcut-mod= + -t --show-touches + --tcpip + --tcpip= + --time-limit= + --tunnel-host= + --tunnel-port= + --v4l2-buffer= + --v4l2-sink= + -v --version + -V --verbosity= + --video-codec= + --video-codec-options= + --video-encoder= + --video-source= + -w --stay-awake + --window-borderless + --window-title= + --window-x= + --window-y= + --window-width= + --window-height=" + + _init_completion -s || return + + case "$prev" in + --video-codec) + COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur")) + return + ;; + --audio-codec) + COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur")) + return + ;; + --video-source) + COMPREPLY=($(compgen -W 'display camera' -- "$cur")) + return + ;; + --audio-source) + COMPREPLY=($(compgen -W 'output mic' -- "$cur")) + return + ;; + --camera-facing) + COMPREPLY=($(compgen -W 'front back external' -- "$cur")) + return + ;; + --keyboard) + COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) + return + ;; + --mouse) + COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur")) + return + ;; + --orientation|--display-orientation) + COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) + return + ;; + --record-orientation) + COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) + return + ;; + --lock-video-orientation) + COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) + return + ;; + --pause-on-exit) + COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) + return + ;; + -r|--record) + COMPREPLY=($(compgen -f -- "$cur")) + return + ;; + --record-format) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur")) + return + ;; + --render-driver) + COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) + return + ;; + --shortcut-mod) + # Only auto-complete a single key + COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) + return + ;; + -V|--verbosity) + COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur")) + return + ;; + -s|--serial) + # Use 'adb devices' to list serial numbers + COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) + return + ;; + --audio-bit-rate \ + |--audio-buffer \ + |-b|--video-bit-rate \ + |--audio-codec-options \ + |--audio-encoder \ + |--audio-output-buffer \ + |--camera-ar \ + |--camera-id \ + |--camera-fps \ + |--camera-size \ + |--crop \ + |--display-id \ + |--display-buffer \ + |--max-fps \ + |-m|--max-size \ + |-p|--port \ + |--push-target \ + |--rotation \ + |--tunnel-host \ + |--tunnel-port \ + |--v4l2-buffer \ + |--v4l2-sink \ + |--video-codec-options \ + |--video-encoder \ + |--tcpip \ + |--window-*) + # Option accepting an argument, but nothing to auto-complete + return + ;; + esac + + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + [[ $COMPREPLY == *= ]] && compopt -o nospace +} + +complete -F _scrcpy scrcpy diff --git a/app/data/icon.ico b/app/data/icon.ico new file mode 100644 index 00000000..b3238778 Binary files /dev/null and b/app/data/icon.ico differ diff --git a/app/data/icon.png b/app/data/icon.png new file mode 100644 index 00000000..b96a1aff Binary files /dev/null and b/app/data/icon.png differ diff --git a/app/data/icon.svg b/app/data/icon.svg new file mode 100644 index 00000000..0ab92c2a --- /dev/null +++ b/app/data/icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/data/open_a_terminal_here.bat b/app/data/open_a_terminal_here.bat new file mode 100644 index 00000000..24d557f3 --- /dev/null +++ b/app/data/open_a_terminal_here.bat @@ -0,0 +1 @@ +@cmd diff --git a/app/data/scrcpy-console.bat b/app/data/scrcpy-console.bat new file mode 100644 index 00000000..0ea7619f --- /dev/null +++ b/app/data/scrcpy-console.bat @@ -0,0 +1,2 @@ +@echo off +scrcpy.exe --pause-on-exit=if-error %* diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop new file mode 100644 index 00000000..fccd42b7 --- /dev/null +++ b/app/data/scrcpy-console.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=scrcpy (console) +GenericName=Android Remote Control +Comment=Display and control your Android device +# For some users, the PATH or ADB environment variables are set from the shell +# startup file, like .bashrc or .zshrc… Run an interactive shell to get +# environment correctly initialized. +Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'" +Icon=scrcpy +Terminal=true +Type=Application +Categories=Utility;RemoteAccess; +StartupNotify=false diff --git a/data/scrcpy-noconsole.vbs b/app/data/scrcpy-noconsole.vbs similarity index 100% rename from data/scrcpy-noconsole.vbs rename to app/data/scrcpy-noconsole.vbs diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop new file mode 100644 index 00000000..9fb81d47 --- /dev/null +++ b/app/data/scrcpy.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Name=scrcpy +GenericName=Android Remote Control +Comment=Display and control your Android device +# For some users, the PATH or ADB environment variables are set from the shell +# startup file, like .bashrc or .zshrc… Run an interactive shell to get +# environment correctly initialized. +Exec=/bin/sh -c "\\$SHELL -i -c scrcpy" +Icon=scrcpy +Terminal=false +Type=Application +Categories=Utility;RemoteAccess; +StartupNotify=false diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy new file mode 100644 index 00000000..a23240ec --- /dev/null +++ b/app/data/zsh-completion/_scrcpy @@ -0,0 +1,101 @@ +#compdef -N scrcpy -N scrcpy.exe +# +# name: scrcpy +# auth: hltdev [hltdev8642@gmail.com] +# desc: completion file for scrcpy (all OSes) +# + +local arguments + +arguments=( + '--always-on-top[Make scrcpy window always on top \(above other windows\)]' + '--audio-bit-rate=[Encode the audio at the given bit-rate]' + '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' + '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' + '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' + '--audio-encoder=[Use a specific MediaCodec audio encoder]' + '--audio-source=[Select the audio source]:source:(output mic)' + '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' + {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' + '--camera-ar=[Select the camera size by its aspect ratio]' + '--camera-high-speed=[Enable high-speed camera capture mode]' + '--camera-id=[Specify the camera id to mirror]' + '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' + '--camera-fps=[Specify the camera capture frame rate]' + '--camera-size=[Specify an explicit camera capture size]' + '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' + {-d,--select-usb}'[Use USB device]' + '--disable-screensaver[Disable screensaver while scrcpy is running]' + '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' + '--display-id=[Specify the display id to mirror]' + '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' + {-e,--select-tcpip}'[Use TCP/IP device]' + {-f,--fullscreen}'[Start in fullscreen]' + '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' + '--forward-all-clicks[Forward clicks to device]' + {-h,--help}'[Print the help]' + '-K[Use UHID keyboard (same as --keyboard=uhid)]' + '--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' + '--kill-adb-on-close[Kill adb when scrcpy terminates]' + '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-camera-sizes[List the valid camera capture sizes]' + '--list-cameras[List cameras available on the device]' + '--list-displays[List displays available on the device]' + '--list-encoders[List video and audio encoders available on the device]' + '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' + {-m,--max-size=}'[Limit both the width and height of the video to value]' + '-M[Use UHID mouse (same as --mouse=uhid)]' + '--max-fps=[Limit the frame rate of screen capture]' + '--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' + {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' + {-N,--no-playback}'[Disable video and audio playback]' + '--no-audio[Disable audio forwarding]' + '--no-audio-playback[Disable audio playback]' + '--no-cleanup[Disable device cleanup actions on exit]' + '--no-clipboard-autosync[Disable automatic clipboard synchronization]' + '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' + '--no-key-repeat[Do not forward repeated key events when a key is held down]' + '--no-mipmaps[Disable the generation of mipmaps]' + '--no-power-on[Do not power on the device on start]' + '--no-video[Disable video forwarding]' + '--no-video-playback[Disable video playback]' + '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' + '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' + {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' + '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)' + '--power-off-on-close[Turn the device screen off when closing scrcpy]' + '--prefer-text[Inject alpha characters and space as text events instead of key events]' + '--print-fps[Start FPS counter, to print frame logs to the console]' + '--push-target=[Set the target directory for pushing files to the device by drag and drop]' + {-r,--record=}'[Record screen to file]:record file:_files' + '--raw-key-events[Inject key events for all input keys, and ignore text events]' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' + '--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)' + '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' + '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' + {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' + {-S,--turn-screen-off}'[Turn the device screen off immediately]' + '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' + {-t,--show-touches}'[Show physical touches]' + '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' + '--time-limit=[Set the maximum mirroring time, in seconds]' + '--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]' + '--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]' + '--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]' + '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' + {-v,--version}'[Print the version of scrcpy]' + {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' + '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' + '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' + '--video-encoder=[Use a specific MediaCodec video encoder]' + '--video-source=[Select the video source]:source:(display camera)' + {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' + '--window-borderless[Disable window decorations \(display borderless window\)]' + '--window-title=[Set a custom window title]' + '--window-x=[Set the initial window horizontal position]' + '--window-y=[Set the initial window vertical position]' + '--window-width=[Set the initial window width]' + '--window-height=[Set the initial window height]' +) + +_arguments -s $arguments diff --git a/app/deps/.gitignore b/app/deps/.gitignore new file mode 100644 index 00000000..ccf6a49e --- /dev/null +++ b/app/deps/.gitignore @@ -0,0 +1 @@ +/work diff --git a/app/deps/README b/app/deps/README new file mode 100644 index 00000000..9cfb5c06 --- /dev/null +++ b/app/deps/README @@ -0,0 +1,27 @@ +This directory (app/deps/) contains: + +*.sh : shell scripts to download and build dependencies + +patches/ : patches to fix dependencies (used by scripts) + +work/sources/ : downloaded tarballs and extracted folders + ffmpeg-6.1.1.tar.xz + ffmpeg-6.1.1/ + libusb-1.0.27.tar.gz + libusb-1.0.27/ + ... +work/build/ : build dirs for each dependency/version/architecture + ffmpeg-6.1.1/win32/ + ffmpeg-6.1.1/win64/ + libusb-1.0.27/win32/ + libusb-1.0.27/win64/ + ... +work/install/ : install dirs for each architexture + win32/bin/ + win32/include/ + win32/lib/ + win32/share/ + win64/bin/ + win64/include/ + win64/lib/ + win64/share/ diff --git a/app/deps/adb.sh b/app/deps/adb.sh new file mode 100755 index 00000000..e2408216 --- /dev/null +++ b/app/deps/adb.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=34.0.5 +FILENAME=platform-tools_r$VERSION-windows.zip +PROJECT_DIR=platform-tools-$VERSION +SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + ZIP_PREFIX=platform-tools + unzip "../$FILENAME" \ + "$ZIP_PREFIX"/AdbWinApi.dll \ + "$ZIP_PREFIX"/AdbWinUsbApi.dll \ + "$ZIP_PREFIX"/adb.exe + mv "$ZIP_PREFIX"/* . + rmdir "$ZIP_PREFIX" +fi + +mkdir -p "$INSTALL_DIR/$HOST/bin" +cd "$INSTALL_DIR/$HOST/bin" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/" diff --git a/app/deps/common b/app/deps/common new file mode 100644 index 00000000..c1cc7729 --- /dev/null +++ b/app/deps/common @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# This file is intended to be sourced by other scripts, not executed + +if [[ $# != 1 ]] +then + # : win32 or win64 + echo "Syntax: $0 " >&2 + exit 1 +fi + +HOST="$1" + +if [[ "$HOST" = win32 ]] +then + HOST_TRIPLET=i686-w64-mingw32 +elif [[ "$HOST" = win64 ]] +then + HOST_TRIPLET=x86_64-w64-mingw32 +else + echo "Unsupported host: $HOST" >&2 + exit 1 +fi + +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" + +PATCHES_DIR="$PWD/patches" + +WORK_DIR="$PWD/work" +SOURCES_DIR="$WORK_DIR/sources" +BUILD_DIR="$WORK_DIR/build" +INSTALL_DIR="$WORK_DIR/install" + +mkdir -p "$INSTALL_DIR" "$SOURCES_DIR" "$WORK_DIR" + +checksum() { + local file="$1" + local sum="$2" + echo "$file: verifying checksum..." + echo "$sum $file" | sha256sum -c +} + +get_file() { + local url="$1" + local file="$2" + local sum="$3" + if [[ -f "$file" ]] + then + echo "$file: found" + else + echo "$file: not found, downloading..." + wget "$url" -O "$file" + fi + checksum "$file" "$sum" +} diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh new file mode 100755 index 00000000..19fb2991 --- /dev/null +++ b/app/deps/ffmpeg.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=6.1.1 +FILENAME=ffmpeg-$VERSION.tar.xz +PROJECT_DIR=ffmpeg-$VERSION +SHA256SUM=8684f4b00f94b85461884c3719382f1261f0d9eb3d59640a1f4ac0873616f968 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" + patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/ffmpeg-6.1-fix-build.patch +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +if [[ "$HOST" = win32 ]] +then + ARCH=x86 +elif [[ "$HOST" = win64 ]] +then + ARCH=x86_64 +else + echo "Unsupported host: $HOST" >&2 + exit 1 +fi + +# -static-libgcc to avoid missing libgcc_s_dw2-1.dll +# -static to avoid dynamic dependency to zlib +export CFLAGS='-static-libgcc -static' +export CXXFLAGS="$CFLAGS" +export LDFLAGS='-static-libgcc -static' + +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" + + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --enable-cross-compile \ + --target-os=mingw32 \ + --arch="$ARCH" \ + --cross-prefix="${HOST_TRIPLET}-" \ + --cc="${HOST_TRIPLET}-gcc" \ + --extra-cflags="-O2 -fPIC" \ + --enable-shared \ + --disable-static \ + --disable-programs \ + --disable-doc \ + --disable-swscale \ + --disable-postproc \ + --disable-avfilter \ + --disable-avdevice \ + --disable-network \ + --disable-everything \ + --enable-swresample \ + --enable-decoder=h264 \ + --enable-decoder=hevc \ + --enable-decoder=av1 \ + --enable-decoder=pcm_s16le \ + --enable-decoder=opus \ + --enable-decoder=aac \ + --enable-decoder=flac \ + --enable-decoder=png \ + --enable-protocol=file \ + --enable-demuxer=image2 \ + --enable-parser=png \ + --enable-zlib \ + --enable-muxer=matroska \ + --enable-muxer=mp4 \ + --enable-muxer=opus \ + --enable-muxer=flac \ + --enable-muxer=wav \ + --disable-vulkan +fi + +make -j +make install diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh new file mode 100755 index 00000000..97fc3c72 --- /dev/null +++ b/app/deps/libusb.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=1.0.27 +FILENAME=libusb-$VERSION.tar.bz2 +PROJECT_DIR=libusb-$VERSION +SHA256SUM=ffaa41d741a8a3bee244ac8e54a72ea05bf2879663c098c82fc5757853441575 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/libusb-$VERSION.tar.bz2" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +export CFLAGS='-O2' +export CXXFLAGS="$CFLAGS" + +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" + + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --host="$HOST_TRIPLET" \ + --enable-shared \ + --disable-static +fi + +make -j +make install-strip diff --git a/app/deps/patches/ffmpeg-6.1-fix-build.patch b/app/deps/patches/ffmpeg-6.1-fix-build.patch new file mode 100644 index 00000000..ed4df48d --- /dev/null +++ b/app/deps/patches/ffmpeg-6.1-fix-build.patch @@ -0,0 +1,27 @@ +From 03c80197afb324da38c9b70254231e3fdcfa68fc Mon Sep 17 00:00:00 2001 +From: Romain Vimont +Date: Sun, 12 Nov 2023 17:58:50 +0100 +Subject: [PATCH] Fix FFmpeg 6.1 build + +Build failed on tag n6.1 With --enable-decoder=av1 but without +--enable-muxer=av1. +--- + libavcodec/Makefile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/libavcodec/Makefile b/libavcodec/Makefile +index 580a8d6b54..aff19b670c 100644 +--- a/libavcodec/Makefile ++++ b/libavcodec/Makefile +@@ -249,7 +249,7 @@ OBJS-$(CONFIG_ATRAC3PAL_DECODER) += atrac3plusdec.o atrac3plus.o \ + OBJS-$(CONFIG_ATRAC9_DECODER) += atrac9dec.o + OBJS-$(CONFIG_AURA_DECODER) += cyuv.o + OBJS-$(CONFIG_AURA2_DECODER) += aura.o +-OBJS-$(CONFIG_AV1_DECODER) += av1dec.o ++OBJS-$(CONFIG_AV1_DECODER) += av1dec.o av1_parse.o + OBJS-$(CONFIG_AV1_CUVID_DECODER) += cuviddec.o + OBJS-$(CONFIG_AV1_MEDIACODEC_DECODER) += mediacodecdec.o + OBJS-$(CONFIG_AV1_MEDIACODEC_ENCODER) += mediacodecenc.o +-- +2.42.0 + diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh new file mode 100755 index 00000000..36c7ab1c --- /dev/null +++ b/app/deps/sdl.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=2.28.5 +FILENAME=SDL-$VERSION.tar.gz +PROJECT_DIR=SDL-release-$VERSION +SHA256SUM=9f0556e4a24ef5b267010038ad9e9948b62f236d5bcc4b22179f95ef62d84023 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +export CFLAGS='-O2' +export CXXFLAGS="$CFLAGS" + +if [[ -d "$HOST" ]] +then + echo "'$PWD/$HOST' already exists, not reconfigured" + cd "$HOST" +else + mkdir "$HOST" + cd "$HOST" + + "$SOURCES_DIR/$PROJECT_DIR"/configure \ + --prefix="$INSTALL_DIR/$HOST" \ + --host="$HOST_TRIPLET" \ + --enable-shared \ + --disable-static +fi + +make -j +# There is no "make install-strip" +make install +# Strip manually +${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll" diff --git a/app/meson.build b/app/meson.build index f5345803..b0a6aadb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,108 +1,137 @@ src = [ 'src/main.c', - 'src/adb.c', + 'src/adb/adb.c', + 'src/adb/adb_device.c', + 'src/adb/adb_parser.c', + 'src/adb/adb_tunnel.c', + 'src/audio_player.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', + 'src/delay_buffer.c', + 'src/demuxer.c', 'src/device_msg.c', - 'src/event_converter.c', - 'src/file_handler.c', + 'src/display.c', + 'src/icon.c', + 'src/file_pusher.c', 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', + 'src/keyboard_sdk.c', + 'src/mouse_sdk.c', 'src/opengl.c', + 'src/options.c', + 'src/packet_merger.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', - 'src/stream.c', - 'src/tiny_xpm.c', - 'src/video_buffer.c', + 'src/version.c', + 'src/hid/hid_keyboard.c', + 'src/hid/hid_mouse.c', + 'src/trait/frame_source.c', + 'src/trait/packet_source.c', + 'src/uhid/keyboard_uhid.c', + 'src/uhid/mouse_uhid.c', + 'src/uhid/uhid_output.c', + 'src/util/acksync.c', + 'src/util/audiobuf.c', + 'src/util/average.c', + 'src/util/file.c', + 'src/util/intmap.c', + 'src/util/intr.c', 'src/util/log.c', + 'src/util/memory.c', 'src/util/net.c', + 'src/util/net_intr.c', 'src/util/process.c', - 'src/util/str_util.c', + 'src/util/process_intr.c', + 'src/util/rand.c', + 'src/util/strbuf.c', + 'src/util/str.c', + 'src/util/term.c', 'src/util/thread.c', 'src/util/tick.c', + 'src/util/timeout.c', ] +conf = configuration_data() + +conf.set('_POSIX_C_SOURCE', '200809L') +conf.set('_XOPEN_SOURCE', '700') +conf.set('_GNU_SOURCE', true) + if host_machine.system() == 'windows' - src += [ 'src/sys/win/process.c' ] + windows = import('windows') + src += [ + 'src/sys/win/file.c', + 'src/sys/win/process.c', + windows.compile_resources('scrcpy-windows.rc'), + ] + conf.set('_WIN32_WINNT', '0x0600') + conf.set('WINVER', '0x0600') else - src += [ 'src/sys/unix/process.c' ] + src += [ + 'src/sys/unix/file.c', + 'src/sys/unix/process.c', + ] + if host_machine.system() == 'darwin' + conf.set('_DARWIN_C_SOURCE', true) + endif endif -v4l2_support = host_machine.system() == 'linux' +v4l2_support = get_option('v4l2') and 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 - dependencies = [ - dependency('libavformat'), - dependency('libavcodec'), - dependency('libavutil'), - dependency('sdl2'), +usb_support = get_option('usb') +if usb_support + src += [ + 'src/usb/aoa_hid.c', + 'src/usb/keyboard_aoa.c', + 'src/usb/mouse_aoa.c', + 'src/usb/scrcpy_otg.c', + 'src/usb/screen_otg.c', + 'src/usb/usb.c', ] +endif - if v4l2_support - dependencies += dependency('libavdevice') - endif +cc = meson.get_compiler('c') -else +dependencies = [ + dependency('libavformat', version: '>= 57.33'), + dependency('libavcodec', version: '>= 57.37'), + dependency('libavutil'), + dependency('libswresample'), + dependency('sdl2', version: '>= 2.0.5'), +] - # cross-compile mingw32 build (from Linux to Windows) - prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') - sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' - sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' - sdl2_include_dir = '../prebuilt-deps/' + prebuilt_sdl2 + '/include' - - sdl2 = declare_dependency( - dependencies: [ - cc.find_library('SDL2', dirs: sdl2_bin_dir), - cc.find_library('SDL2main', dirs: sdl2_lib_dir), - ], - include_directories: include_directories(sdl2_include_dir) - ) - - prebuilt_ffmpeg_shared = meson.get_cross_property('prebuilt_ffmpeg_shared') - prebuilt_ffmpeg_dev = meson.get_cross_property('prebuilt_ffmpeg_dev') - ffmpeg_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_ffmpeg_shared + '/bin' - ffmpeg_include_dir = '../prebuilt-deps/' + prebuilt_ffmpeg_dev + '/include' - ffmpeg = declare_dependency( - dependencies: [ - cc.find_library('avcodec-58', dirs: ffmpeg_bin_dir), - cc.find_library('avformat-58', dirs: ffmpeg_bin_dir), - cc.find_library('avutil-56', dirs: ffmpeg_bin_dir), - ], - include_directories: include_directories(ffmpeg_include_dir) - ) - - dependencies = [ - ffmpeg, - sdl2, - cc.find_library('mingw32') - ] +if v4l2_support + dependencies += dependency('libavdevice') +endif +if usb_support + dependencies += dependency('libusb-1.0') endif if host_machine.system() == 'windows' + dependencies += cc.find_library('mingw32') dependencies += cc.find_library('ws2_32') endif -conf = configuration_data() +check_functions = [ + 'strdup', + 'asprintf', + 'vasprintf', + 'nrand48', + 'jrand48', + 'reallocarray', +] foreach f : check_functions if cc.has_function(f) @@ -111,6 +140,9 @@ foreach f : check_functions endif endforeach +conf.set('HAVE_SOCK_CLOEXEC', host_machine.system() != 'windows' and + cc.has_header_symbol('sys/socket.h', 'SOCK_CLOEXEC')) + # the version, updated on release conf.set_quoted('SCRCPY_VERSION', meson.project_version()) @@ -126,10 +158,6 @@ conf.set('PORTABLE', get_option('portable')) conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') -# the default video bitrate, in bits/second -# overridden by option --bit-rate -conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps - # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) @@ -139,6 +167,9 @@ conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == ' # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) +# enable HID over AOA support (linux only) +conf.set('HAVE_USB', usb_support) + configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') @@ -149,7 +180,26 @@ executable('scrcpy', src, install: true, c_args: []) +# +datadir = get_option('datadir') # by default 'share' + install_man('scrcpy.1') +install_data('data/icon.png', + rename: 'scrcpy.png', + install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) +install_data('data/zsh-completion/_scrcpy', + install_dir: join_paths(datadir, 'zsh/site-functions')) +install_data('data/bash-completion/scrcpy', + install_dir: join_paths(datadir, 'bash-completion/completions')) + +# Desktop entry file for application launchers +if host_machine.system() == 'linux' + # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) + install_data('data/scrcpy.desktop', + install_dir: join_paths(datadir, 'applications')) + install_data('data/scrcpy-console.desktop', + install_dir: join_paths(datadir, 'applications')) +endif ### TESTS @@ -157,41 +207,66 @@ install_man('scrcpy.1') # do not build tests in release (assertions would not be executed at all) if get_option('buildtype') == 'debug' tests = [ - ['test_buffer_util', [ - 'tests/test_buffer_util.c' + ['test_adb_parser', [ + 'tests/test_adb_parser.c', + 'src/adb/adb_device.c', + 'src/adb/adb_parser.c', + 'src/util/str.c', + 'src/util/strbuf.c', + ]], + ['test_binary', [ + 'tests/test_binary.c', ]], - ['test_cbuf', [ - 'tests/test_cbuf.c', + ['test_audiobuf', [ + 'tests/test_audiobuf.c', + 'src/util/audiobuf.c', + 'src/util/memory.c', ]], ['test_cli', [ 'tests/test_cli.c', 'src/cli.c', - 'src/util/str_util.c', - ]], - ['test_clock', [ - 'tests/test_clock.c', - 'src/clock.c', + 'src/options.c', + 'src/util/log.c', + 'src/util/net.c', + 'src/util/str.c', + 'src/util/strbuf.c', + 'src/util/term.c', ]], ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', - 'src/util/str_util.c', + 'src/util/str.c', + 'src/util/strbuf.c', ]], ['test_device_msg_deserialize', [ 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], - ['test_queue', [ - 'tests/test_queue.c', + ['test_orientation', [ + 'tests/test_orientation.c', + 'src/options.c', + ]], + ['test_strbuf', [ + 'tests/test_strbuf.c', + 'src/util/strbuf.c', + ]], + ['test_str', [ + 'tests/test_str.c', + 'src/util/str.c', + 'src/util/strbuf.c', + ]], + ['test_vecdeque', [ + 'tests/test_vecdeque.c', + 'src/util/memory.c', ]], - ['test_strutil', [ - 'tests/test_strutil.c', - 'src/util/str_util.c', + ['test_vector', [ + 'tests/test_vector.c', ]], ] foreach t : tests - exe = executable(t[0], t[1], + sources = t[1] + ['src/compat.c'] + exe = executable(t[0], sources, include_directories: src_dir, dependencies: dependencies, c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) diff --git a/app/scrcpy-windows.manifest b/app/scrcpy-windows.manifest new file mode 100644 index 00000000..f2708ecb --- /dev/null +++ b/app/scrcpy-windows.manifest @@ -0,0 +1,9 @@ + + + + + true + PerMonitorV2 + + + diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc new file mode 100644 index 00000000..059e91d4 --- /dev/null +++ b/app/scrcpy-windows.rc @@ -0,0 +1,23 @@ +#include + +0 ICON "data/icon.ico" +1 RT_MANIFEST "scrcpy-windows.manifest" +2 VERSIONINFO +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "FileDescription", "Display and control your Android device" + VALUE "InternalName", "scrcpy" + VALUE "LegalCopyright", "Romain Vimont, Genymobile" + VALUE "OriginalFilename", "scrcpy.exe" + VALUE "ProductName", "scrcpy" + VALUE "ProductVersion", "2.4" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1b69a065..1e3c91b1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -20,20 +20,94 @@ provides display and control of Android devices connected on USB (or over TCP/IP Make scrcpy window always on top (above other windows). .TP -.BI "\-b, \-\-bit\-rate " value -Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). +.BI "\-\-audio\-bit\-rate " value +Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). -Default is 8000000. +Default is 128K (128000). .TP -.BI "\-\-codec\-options " key[:type]=value[,...] -Set a list of comma-separated key:type=value options for the device encoder. +.BI "\-\-audio\-buffer " ms +Configure the audio buffering delay (in milliseconds). + +Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). + +Default is 50. + +.TP +.BI "\-\-audio\-codec " name +Select an audio codec (opus, aac, flac or raw). + +Default is opus. + +.TP +.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device audio 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 . +The list of possible codec options is available in the Android documentation: + + + +.TP +.BI "\-\-audio\-encoder " name +Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). + +The available encoders can be listed by \fB\-\-list\-encoders\fR. + +.TP +.BI "\-\-audio\-source " source +Select the audio source (output or mic). + +Default is output. + +.TP +.BI "\-\-audio\-output\-buffer " ms +Configure the size of the SDL audio output buffer (in milliseconds). + +If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. + +Default is 5. + +.TP +.BI "\-b, \-\-video\-bit\-rate " value +Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). + +Default is 8M (8000000). + +.TP +.BI "\-\-camera\-ar " ar +Select the camera size by its aspect ratio (+/- 10%). + +Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). + +.TP +.B \-\-camera\-high\-speed +Enable high-speed camera capture mode. + +This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. + +.TP +.BI "\-\-camera\-id " id +Specify the device camera id to mirror. + +The available camera ids can be listed by \fB\-\-list\-cameras\fR. + +.TP +.BI "\-\-camera\-facing " facing +Select the device camera by its facing direction. + +Possible values are "front", "back" and "external". + +.TP +.BI "\-\-camera\-fps " fps +Specify the camera capture frame rate. + +If not specified, Android's default frame rate (30 fps) is used. + +.TP +.BI "\-\-camera\-size " width\fRx\fIheight +Specify an explicit camera capture size. .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy @@ -44,27 +118,46 @@ The values are expressed in the device natural orientation (typically, portrait value is computed on the cropped size. .TP -.BI "\-\-disable-screensaver" +.B \-d, \-\-select\-usb +Use USB device (if there is exactly one, like adb -d). + +Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). + +.TP +.BI "\-\-disable\-screensaver" Disable screensaver while scrcpy is running. .TP -.BI "\-\-display " id -Specify the display id to mirror. +.BI "\-\-display\-buffer " ms +Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. + +Default is 0 (no buffering). + +.TP +.BI "\-\-display\-id " id +Specify the device display id to mirror. -The list of possible display ids can be listed by "adb shell dumpsys display" -(search "mDisplayId=" in the output). +The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. .TP -.BI "\-\-display\-buffer ms -Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. +.BI "\-\-display\-orientation " value +Set the initial display orientation. -Default is 0 (no buffering). +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +Default is 0. + +.TP +.B \-e, \-\-select\-tcpip +Use TCP/IP device (if there is exactly one, like adb -e). + +Also see \fB\-d\fR (\fB\-\-select\-usb\fR). .TP -.BI "\-\-encoder " name -Use a specific MediaCodec encoder (must be a H.264 encoder). +.B \-f, \-\-fullscreen +Start in fullscreen. .TP .B \-\-force\-adb\-forward @@ -74,14 +167,37 @@ Do not attempt to use "adb reverse" to connect to the device. .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. - .TP .B \-h, \-\-help Print this help. +.TP +.B \-K +Same as \fB\-\-keyboard=uhid\fR. + +.TP +.BI "\-\-keyboard " mode +Select how to send keyboard inputs to the device. + +Possible values are "disabled", "sdk", "uhid" and "aoa": + + - "disabled" does not send keyboard inputs to the device. + - "sdk" uses the Android system API to deliver keyboard events to applications. + - "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device. + - "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB. + +For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing: + + adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS + +This option is only available when the HID keyboard is enabled (or a physical keyboard is connected). + +Also see \fB\-\-mouse\fR. + +.TP +.B \-\-kill\-adb\-on\-close +Kill adb when scrcpy terminates. + .TP .B \-\-legacy\-paste Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). @@ -89,30 +205,96 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. .TP -.BI "\-\-lock\-video\-orientation[=value] -Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. +.B \-\-list\-camera\-sizes +List the valid camera capture sizes. + +.TP +.B \-\-list\-cameras +List cameras available on the device. + +.TP +.B \-\-list\-encoders +List video and audio encoders available on the device. + +.TP +.B \-\-list\-displays +List displays available on the device. + +.TP +\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] +Lock capture video orientation to \fIvalue\fR. + +Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees. Default is "unlocked". Passing the option without argument is equivalent to passing "initial". +.TP +.BI "\-m, \-\-max\-size " value +Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. + +Default is 0 (unlimited). + +.TP +.B \-M +Same as \fB\-\-mouse=uhid\fR. + .TP .BI "\-\-max\-fps " value Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). .TP -.BI "\-m, \-\-max\-size " value -Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. +.BI "\-\-mouse " mode +Select how to send mouse inputs to the device. + +Possible values are "disabled", "sdk", "uhid" and "aoa": + + - "disabled" does not send mouse inputs to the device. + - "sdk" uses the Android system API to deliver mouse events to applications. + - "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device. + - "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB. + +In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode). + +LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. + +Also see \fB\-\-keyboard\fR. -Default is 0 (unlimited). .TP .B \-n, \-\-no\-control Disable device control (mirror the device in read\-only). .TP -.B \-N, \-\-no\-display -Do not display device (only when screen recording is enabled). +.B \-N, \-\-no\-playback +Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). + +.TP +.B \-\-no\-audio +Disable audio forwarding. + +.TP +.B \-\-no\-audio\-playback +Disable audio playback on the computer. + +.TP +.B \-\-no\-cleanup +By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. + +This option disables this cleanup. + +.TP +.B \-\-no\-clipboard\-autosync +By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. + +This option disables this automatic synchronization. + +.TP +.B \-\-no\-downsize\-on\-error +By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. + +This option disables this behavior. .TP .B \-\-no\-key\-repeat @@ -123,11 +305,55 @@ Do not forward repeated key events when a key is held down. 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] +.B \-\-no\-power\-on +Do not power on the device on start. + +.TP +.B \-\-no\-video +Disable video forwarding. + +.TP +.B \-\-no\-video\-playback +Disable video playback on the computer. + +.TP +.BI "\-\-orientation " value +Same as --display-orientation=value --record-orientation=value. + +.TP +.B \-\-otg +Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. + +In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. + +LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. + +If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. + +It may only work over USB. + +See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. + +.TP +.BI "\-p, \-\-port " port\fR[:\fIport\fR] Set the TCP port (range) used by the client to listen. Default is 27183:27199. +.TP +\fB\-\-pause\-on\-exit\fR[=\fImode\fR] +Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured). + +This is useful to prevent the terminal window from automatically closing, so that error messages can be read. + +Default is "false". + +Passing the option without argument is equivalent to passing "true". + +.TP +.B \-\-power\-off\-on\-close +Turn the device screen off when closing scrcpy. + .TP .B \-\-prefer\-text Inject alpha characters and space as text events instead of key events. @@ -135,6 +361,10 @@ Inject alpha characters and space as text events instead of key events. This avoids issues when combining multiple keys to enter special characters, but breaks the expected behavior of alpha keys in games (typically WASD). +.TP +.B "\-\-print\-fps +Start FPS counter, to print framerate logs to the console. It can be started or stopped at any time with MOD+i. + .TP .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". @@ -148,11 +378,23 @@ Record screen to The format is determined by the .B \-\-record\-format -option if set, or by the file extension (.mp4 or .mkv). +option if set, or by the file extension. + +.TP +.B \-\-raw\-key\-events +Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (either mp4 or mkv). +Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). + +.TP +.BI "\-\-record\-orientation " value +Set the record orientation. + +Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. + +Default is 0. .TP .BI "\-\-render\-driver " name @@ -160,19 +402,22 @@ 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. +.B \-\-require\-audio +By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. .TP -.BI "\-\-shortcut\-mod " key[+...]][,...] +.B \-S, \-\-turn\-screen\-off +Turn the device screen off immediately. + +.TP +.BI "\-\-shortcut\-mod " key\fR[+...]][,...] 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 ','. @@ -181,16 +426,46 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr 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, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). +.TP +.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] +Configure and reconnect the device over TCP/IP. + +If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). + +If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. + +.TP +.BI "\-\-time\-limit " seconds +Set the maximum mirroring time, in seconds. + +.TP +.BI "\-\-tunnel\-host " ip +Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. + +Default is localhost. + +.TP +.BI "\-\-tunnel\-port " port +Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR. + +Default is 0 (not forced): the local port used for establishing the tunnel will be used. + +.TP +.B \-v, \-\-version +Print the version of scrcpy. + +.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 .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. @@ -206,14 +481,34 @@ This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. Default is 0 (no buffering). .TP -.BI "\-V, \-\-verbosity " value -Set the log level ("verbose", "debug", "info", "warn" or "error"). +.BI "\-\-video\-codec " name +Select a video codec (h264, h265 or av1). -Default is "info" for release builds, "debug" for debug builds. +Default is h264. .TP -.B \-v, \-\-version -Print the version of scrcpy. +.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...] +Set a list of comma-separated key:type=value options for the device video 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: + + + +.TP +.BI "\-\-video\-encoder " name +Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). + +The available encoders can be listed by \fB\-\-list\-encoders\fR. + +.TP +.BI "\-\-video\-source " source +Select the video source (display or camera). + +Camera mirroring requires Android 12+. + +Default is display. .TP .B \-w, \-\-stay-awake @@ -251,6 +546,12 @@ Set the initial window height. Default is 0 (automatic). +.SH EXIT STATUS +.B scrcpy +will exit with code 0 on normal program termination. If an initial +connection cannot be established, the exit code 1 will be returned. If the +device disconnects while a session is active, exit code 2 will be returned. + .SH SHORTCUTS In the following list, MOD is the shortcut modifier. By default, it's (left) @@ -268,6 +569,14 @@ Rotate display left .B MOD+Right Rotate display right +.TP +.B MOD+Shift+Left, MOD+Shift+Right +Flip display horizontally + +.TP +.B MOD+Shift+Up, MOD+Shift+Down +Flip display vertically + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) @@ -344,28 +653,48 @@ Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= .B MOD+Shift+v Inject computer clipboard text as a sequence of key events +.TP +.B MOD+k +Open keyboard settings on the device (for HID keyboard only) + .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 +Pinch-to-zoom and rotate from the center of the screen + +.TP +.B Shift+click-and-move +Tilt (slide vertically with two fingers) .TP .B Drag & drop APK file Install APK from computer +.TP +.B Drag & drop non-APK file +Push file to device (see \fB\-\-push\-target\fR) + .SH Environment variables .TP .B ADB -Specify the path to adb. +Path to adb. + +.TP +.B ANDROID_SERIAL +Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified. + +.TP +.B SCRCPY_ICON_PATH +Path to the program icon. .TP .B SCRCPY_SERVER_PATH -Specify the path to server binary. +Path to the server binary. .SH AUTHORS @@ -380,23 +709,14 @@ for the Debian Project (and may be used by others). .SH "REPORTING BUGS" -Report bugs to -.UR https://github.com/Genymobile/scrcpy/issues -.UE . +Report bugs to . .SH COPYRIGHT -Copyright \(co 2018 Genymobile -.UR https://www.genymobile.com -Genymobile -.UE - -Copyright \(co 2018\-2020 -.MT rom@rom1v.com -Romain Vimont -.ME +Copyright \(co 2018 Genymobile + +Copyright \(co 2018\-2024 Romain Vimont Licensed under the Apache License, Version 2.0. .SH WWW -.UR https://github.com/Genymobile/scrcpy -.UE + diff --git a/app/src/adb.c b/app/src/adb.c deleted file mode 100644 index 5bb9df30..00000000 --- a/app/src/adb.c +++ /dev/null @@ -1,226 +0,0 @@ -#include "adb.h" - -#include -#include -#include -#include - -#include "util/log.h" -#include "util/str_util.h" - -static const char *adb_command; - -static inline const char * -get_adb_command(void) { - if (!adb_command) { - adb_command = getenv("ADB"); - if (!adb_command) - adb_command = "adb"; - } - return adb_command; -} - -// serialize argv to string "[arg1], [arg2], [arg3]" -static size_t -argv_to_string(const char *const *argv, char *buf, size_t bufsize) { - size_t idx = 0; - bool first = true; - while (*argv) { - const char *arg = *argv; - size_t len = strlen(arg); - // count space for "[], ...\0" - if (idx + len + 8 >= bufsize) { - // not enough space, truncate - assert(idx < bufsize - 4); - memcpy(&buf[idx], "...", 3); - idx += 3; - break; - } - if (first) { - first = false; - } else { - buf[idx++] = ','; - buf[idx++] = ' '; - } - buf[idx++] = '['; - memcpy(&buf[idx], arg, len); - idx += len; - buf[idx++] = ']'; - argv++; - } - assert(idx < bufsize); - buf[idx] = '\0'; - 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[]) { -#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, MAX_COMMAND_STRING_LEN); - LOGE("Failed to execute: %s", buf); - break; - case PROCESS_ERROR_MISSING_BINARY: - argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); - LOGE("Command not found: %s", buf); - LOGE("(make 'adb' accessible from your PATH or define its full" - "path in the ADB environment variable)"); - show_adb_installation_msg(); - break; - case PROCESS_SUCCESS: - // do nothing - break; - } - - free(buf); -} - -process_t -adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { - int i; - process_t process; - - const char **argv = malloc((len + 4) * sizeof(*argv)); - if (!argv) { - return PROCESS_NONE; - } - - argv[0] = get_adb_command(); - if (serial) { - argv[1] = "-s"; - argv[2] = serial; - i = 3; - } else { - i = 1; - } - - 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, argv); - process = PROCESS_NONE; - } - - free(argv); - return process; -} - -process_t -adb_forward(const char *serial, uint16_t local_port, - const char *device_socket_name) { - char local[4 + 5 + 1]; // tcp:PORT - char remote[108 + 14 + 1]; // localabstract:NAME - sprintf(local, "tcp:%" PRIu16, local_port); - snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); - const char *const adb_cmd[] = {"forward", local, remote}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); -} - -process_t -adb_forward_remove(const char *serial, uint16_t local_port) { - char local[4 + 5 + 1]; // tcp:PORT - sprintf(local, "tcp:%" PRIu16, local_port); - const char *const adb_cmd[] = {"forward", "--remove", local}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); -} - -process_t -adb_reverse(const char *serial, const char *device_socket_name, - uint16_t local_port) { - char local[4 + 5 + 1]; // tcp:PORT - char remote[108 + 14 + 1]; // localabstract:NAME - sprintf(local, "tcp:%" PRIu16, local_port); - snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); - const char *const adb_cmd[] = {"reverse", remote, local}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); -} - -process_t -adb_reverse_remove(const char *serial, const char *device_socket_name) { - char remote[108 + 14 + 1]; // localabstract:NAME - snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); - const char *const adb_cmd[] = {"reverse", "--remove", remote}; - return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); -} - -process_t -adb_push(const char *serial, const char *local, const char *remote) { -#ifdef __WINDOWS__ - // Windows will parse the string, so the paths must be quoted - // (see sys/win/command.c) - local = strquote(local); - if (!local) { - return PROCESS_NONE; - } - remote = strquote(remote); - if (!remote) { - free((void *) local); - return PROCESS_NONE; - } -#endif - - const char *const adb_cmd[] = {"push", local, remote}; - process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); - -#ifdef __WINDOWS__ - free((void *) remote); - free((void *) local); -#endif - - return proc; -} - -process_t -adb_install(const char *serial, const char *local) { -#ifdef __WINDOWS__ - // Windows will parse the string, so the local name must be quoted - // (see sys/win/command.c) - local = strquote(local); - if (!local) { - return PROCESS_NONE; - } -#endif - - const char *const adb_cmd[] = {"install", "-r", local}; - process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); - -#ifdef __WINDOWS__ - free((void *) local); -#endif - - return proc; -} diff --git a/app/src/adb.h b/app/src/adb.h deleted file mode 100644 index e27f34fa..00000000 --- a/app/src/adb.h +++ /dev/null @@ -1,34 +0,0 @@ -#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/adb/adb.c b/app/src/adb/adb.c new file mode 100644 index 00000000..15c9c85a --- /dev/null +++ b/app/src/adb/adb.c @@ -0,0 +1,741 @@ +#include "adb.h" + +#include +#include +#include +#include + +#include "adb_device.h" +#include "adb_parser.h" +#include "util/file.h" +#include "util/log.h" +#include "util/process_intr.h" +#include "util/str.h" + +/* Convenience macro to expand: + * + * const char *const argv[] = + * SC_ADB_COMMAND("shell", "echo", "hello"); + * + * to: + * + * const char *const argv[] = + * { sc_adb_get_executable(), "shell", "echo", "hello", NULL }; + */ +#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL } + +static const char *adb_executable; + +const char * +sc_adb_get_executable(void) { + if (!adb_executable) { + adb_executable = getenv("ADB"); + if (!adb_executable) + adb_executable = "adb"; + } + return adb_executable; +} + +// serialize argv to string "[arg1], [arg2], [arg3]" +static size_t +argv_to_string(const char *const *argv, char *buf, size_t bufsize) { + size_t idx = 0; + bool first = true; + while (*argv) { + const char *arg = *argv; + size_t len = strlen(arg); + // count space for "[], ...\0" + if (idx + len + 8 >= bufsize) { + // not enough space, truncate + assert(idx < bufsize - 4); + memcpy(&buf[idx], "...", 3); + idx += 3; + break; + } + if (first) { + first = false; + } else { + buf[idx++] = ','; + buf[idx++] = ' '; + } + buf[idx++] = '['; + memcpy(&buf[idx], arg, len); + idx += len; + buf[idx++] = ']'; + argv++; + } + assert(idx < bufsize); + buf[idx] = '\0'; + return idx; +} + +static void +show_adb_installation_msg(void) { +#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 (sc_file_executable_exists(pkg_managers[i].binary)) { + LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); + return; + } + } +#endif +} + +static void +show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { +#define MAX_COMMAND_STRING_LEN 1024 + char *buf = malloc(MAX_COMMAND_STRING_LEN); + if (!buf) { + LOG_OOM(); + LOGE("Failed to execute"); + return; + } + + switch (err) { + case SC_PROCESS_ERROR_GENERIC: + argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); + LOGE("Failed to execute: %s", buf); + break; + case SC_PROCESS_ERROR_MISSING_BINARY: + argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); + LOGE("Command not found: %s", buf); + LOGE("(make 'adb' accessible from your PATH or define its full" + "path in the ADB environment variable)"); + show_adb_installation_msg(); + break; + case SC_PROCESS_SUCCESS: + // do nothing + break; + } + + free(buf); +} + +static bool +process_check_success_internal(sc_pid pid, const char *name, bool close, + unsigned flags) { + bool log_errors = !(flags & SC_ADB_NO_LOGERR); + + if (pid == SC_PROCESS_NONE) { + if (log_errors) { + LOGE("Could not execute \"%s\"", name); + } + return false; + } + sc_exit_code exit_code = sc_process_wait(pid, close); + if (exit_code) { + if (log_errors) { + if (exit_code != SC_EXIT_CODE_NONE) { + LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, + exit_code); + } else { + LOGE("\"%s\" exited unexpectedly", name); + } + } + return false; + } + return true; +} + +static bool +process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, + unsigned flags) { + if (intr && !sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + // Always pass close=false, interrupting would be racy otherwise + bool ret = process_check_success_internal(pid, name, false, flags); + + if (intr) { + sc_intr_set_process(intr, SC_PROCESS_NONE); + } + + // Close separately + sc_process_close(pid); + + return ret; +} + +static sc_pid +sc_adb_execute_p(const char *const argv[], unsigned flags, sc_pipe *pout) { + unsigned process_flags = 0; + if (flags & SC_ADB_NO_STDOUT) { + process_flags |= SC_PROCESS_NO_STDOUT; + } + if (flags & SC_ADB_NO_STDERR) { + process_flags |= SC_PROCESS_NO_STDERR; + } + + sc_pid pid; + enum sc_process_result r = + sc_process_execute_p(argv, &pid, process_flags, NULL, pout, NULL); + if (r != SC_PROCESS_SUCCESS) { + // If the execution itself failed (not the command exit code), log the + // error in all cases + show_adb_err_msg(r, argv); + pid = SC_PROCESS_NONE; + } + + return pid; +} + +sc_pid +sc_adb_execute(const char *const argv[], unsigned flags) { + return sc_adb_execute_p(argv, flags, NULL); +} + +bool +sc_adb_start_server(struct sc_intr *intr, unsigned flags) { + const char *const argv[] = SC_ADB_COMMAND("start-server"); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb start-server", flags); +} + +bool +sc_adb_kill_server(struct sc_intr *intr, unsigned flags) { + const char *const argv[] = SC_ADB_COMMAND("kill-server"); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb kill-server", flags); +} + +bool +sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name, unsigned flags) { + char local[4 + 5 + 1]; // tcp:PORT + char remote[108 + 14 + 1]; // localabstract:NAME + + int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); + assert(r >= 0 && (size_t) r < sizeof(local)); + + r = snprintf(remote, sizeof(remote), "localabstract:%s", + device_socket_name); + if (r < 0 || (size_t) r >= sizeof(remote)) { + LOGE("Could not write socket name"); + return false; + } + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "forward", local, remote); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb forward", flags); +} + +bool +sc_adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port, unsigned flags) { + char local[4 + 5 + 1]; // tcp:PORT + int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); + assert(r >= 0 && (size_t) r < sizeof(local)); + (void) r; + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "forward", "--remove", local); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb forward --remove", flags); +} + +bool +sc_adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port, + unsigned flags) { + char local[4 + 5 + 1]; // tcp:PORT + char remote[108 + 14 + 1]; // localabstract:NAME + int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port); + assert(r >= 0 && (size_t) r < sizeof(local)); + + r = snprintf(remote, sizeof(remote), "localabstract:%s", + device_socket_name); + if (r < 0 || (size_t) r >= sizeof(remote)) { + LOGE("Could not write socket name"); + return false; + } + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "reverse", remote, local); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb reverse", flags); +} + +bool +sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name, unsigned flags) { + char remote[108 + 14 + 1]; // localabstract:NAME + int r = snprintf(remote, sizeof(remote), "localabstract:%s", + device_socket_name); + if (r < 0 || (size_t) r >= sizeof(remote)) { + LOGE("Device socket name too long"); + return false; + } + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "reverse", "--remove", remote); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb reverse --remove", flags); +} + +bool +sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote, unsigned flags) { +#ifdef __WINDOWS__ + // Windows will parse the string, so the paths must be quoted + // (see sys/win/command.c) + local = sc_str_quote(local); + if (!local) { + return SC_PROCESS_NONE; + } + remote = sc_str_quote(remote); + if (!remote) { + free((void *) local); + return SC_PROCESS_NONE; + } +#endif + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "push", local, remote); + + sc_pid pid = sc_adb_execute(argv, flags); + +#ifdef __WINDOWS__ + free((void *) remote); + free((void *) local); +#endif + + return process_check_success_intr(intr, pid, "adb push", flags); +} + +bool +sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, + unsigned flags) { +#ifdef __WINDOWS__ + // Windows will parse the string, so the local name must be quoted + // (see sys/win/command.c) + local = sc_str_quote(local); + if (!local) { + return SC_PROCESS_NONE; + } +#endif + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "install", "-r", local); + + sc_pid pid = sc_adb_execute(argv, flags); + +#ifdef __WINDOWS__ + free((void *) local); +#endif + + return process_check_success_intr(intr, pid, "adb install", flags); +} + +bool +sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, + unsigned flags) { + char port_string[5 + 1]; + int r = snprintf(port_string, sizeof(port_string), "%" PRIu16, port); + assert(r >= 0 && (size_t) r < sizeof(port_string)); + (void) r; + + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "tcpip", port_string); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb tcpip", flags); +} + +bool +sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { + const char *const argv[] = SC_ADB_COMMAND("connect", ip_port); + + sc_pipe pout; + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb connect\""); + return false; + } + + // "adb connect" always returns successfully (with exit code 0), even in + // case of failure. As a workaround, check if its output starts with + // "connected". + char buf[128]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "adb connect", flags); + if (!ok) { + return false; + } + + if (r == -1) { + return false; + } + + assert((size_t) r < sizeof(buf)); + buf[r] = '\0'; + + ok = !strncmp("connected", buf, sizeof("connected") - 1); + if (!ok && !(flags & SC_ADB_NO_STDERR)) { + // "adb connect" also prints errors to stdout. Since we capture it, + // re-print the error to stderr. + size_t len = strcspn(buf, "\r\n"); + buf[len] = '\0'; + fprintf(stderr, "%s\n", buf); + } + return ok; +} + +bool +sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { + assert(ip_port); + const char *const argv[] = SC_ADB_COMMAND("disconnect", ip_port); + + sc_pid pid = sc_adb_execute(argv, flags); + return process_check_success_intr(intr, pid, "adb disconnect", flags); +} + +static bool +sc_adb_list_devices(struct sc_intr *intr, unsigned flags, + struct sc_vec_adb_devices *out_vec) { + const char *const argv[] = SC_ADB_COMMAND("devices", "-l"); + +#define BUFSIZE 65536 + char *buf = malloc(BUFSIZE); + if (!buf) { + LOG_OOM(); + return false; + } + + sc_pipe pout; + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb devices -l\""); + free(buf); + return false; + } + + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, BUFSIZE - 1); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags); + if (!ok) { + free(buf); + return false; + } + + if (r == -1) { + free(buf); + return false; + } + + assert((size_t) r < BUFSIZE); + if (r == BUFSIZE - 1) { + // The implementation assumes that the output of "adb devices -l" fits + // in the buffer in a single pass + LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " + "Please report an issue."); + free(buf); + return false; + } + + // It is parsed as a NUL-terminated string + buf[r] = '\0'; + + // List all devices to the output list directly + ok = sc_adb_parse_devices(buf, out_vec); + free(buf); + return ok; +} + +static bool +sc_adb_accept_device(const struct sc_adb_device *device, + const struct sc_adb_device_selector *selector) { + switch (selector->type) { + case SC_ADB_DEVICE_SELECT_ALL: + return true; + case SC_ADB_DEVICE_SELECT_SERIAL: + assert(selector->serial); + char *device_serial_colon = strchr(device->serial, ':'); + if (device_serial_colon) { + // The device serial is an IP:port... + char *serial_colon = strchr(selector->serial, ':'); + if (!serial_colon) { + // But the requested serial has no ':', so only consider + // the IP part of the device serial. This allows to use + // "192.168.1.1" to match any "192.168.1.1:port". + size_t serial_len = strlen(selector->serial); + size_t device_ip_len = device_serial_colon - device->serial; + if (serial_len != device_ip_len) { + // They are not equal, they don't even have the same + // length + return false; + } + return !strncmp(selector->serial, device->serial, + device_ip_len); + } + } + return !strcmp(selector->serial, device->serial); + case SC_ADB_DEVICE_SELECT_USB: + return sc_adb_device_get_type(device->serial) == + SC_ADB_DEVICE_TYPE_USB; + case SC_ADB_DEVICE_SELECT_TCPIP: + // Both emulators and TCP/IP devices are selected via -e + return sc_adb_device_get_type(device->serial) != + SC_ADB_DEVICE_TYPE_USB; + default: + assert(!"Missing SC_ADB_DEVICE_SELECT_* handling"); + break; + } + + return false; +} + +static size_t +sc_adb_devices_select(struct sc_adb_device *devices, size_t len, + const struct sc_adb_device_selector *selector, + size_t *idx_out) { + size_t count = 0; + for (size_t i = 0; i < len; ++i) { + struct sc_adb_device *device = &devices[i]; + device->selected = sc_adb_accept_device(device, selector); + if (device->selected) { + if (idx_out && !count) { + *idx_out = i; + } + ++count; + } + } + + return count; +} + +static void +sc_adb_devices_log(enum sc_log_level level, struct sc_adb_device *devices, + size_t count) { + for (size_t i = 0; i < count; ++i) { + struct sc_adb_device *d = &devices[i]; + const char *selection = d->selected ? "-->" : " "; + bool is_usb = + sc_adb_device_get_type(d->serial) == SC_ADB_DEVICE_TYPE_USB; + const char *type = is_usb ? " (usb)" + : "(tcpip)"; + LOG(level, " %s %s %-20s %16s %s", + selection, type, d->serial, d->state, d->model ? d->model : ""); + } +} + +static bool +sc_adb_device_check_state(struct sc_adb_device *device, + struct sc_adb_device *devices, size_t count) { + const char *state = device->state; + + if (!strcmp("device", state)) { + return true; + } + + if (!strcmp("unauthorized", state)) { + LOGE("Device is unauthorized:"); + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); + LOGE("A popup should open on the device to request authorization."); + LOGE("Check the FAQ: " + ""); + } else { + LOGE("Device could not be connected (state=%s)", state); + } + + return false; +} + +bool +sc_adb_select_device(struct sc_intr *intr, + const struct sc_adb_device_selector *selector, + unsigned flags, struct sc_adb_device *out_device) { + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_list_devices(intr, flags, &vec); + if (!ok) { + LOGE("Could not list ADB devices"); + return false; + } + + if (vec.size == 0) { + LOGE("Could not find any ADB device"); + return false; + } + + size_t sel_idx; // index of the single matching device if sel_count == 1 + size_t sel_count = + sc_adb_devices_select(vec.data, vec.size, selector, &sel_idx); + + if (sel_count == 0) { + // if count > 0 && sel_count == 0, then necessarily a selection is + // requested + assert(selector->type != SC_ADB_DEVICE_SELECT_ALL); + + switch (selector->type) { + case SC_ADB_DEVICE_SELECT_SERIAL: + assert(selector->serial); + LOGE("Could not find ADB device %s:", selector->serial); + break; + case SC_ADB_DEVICE_SELECT_USB: + LOGE("Could not find any ADB device over USB:"); + break; + case SC_ADB_DEVICE_SELECT_TCPIP: + LOGE("Could not find any ADB device over TCP/IP:"); + break; + default: + assert(!"Unexpected selector type"); + break; + } + + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); + sc_adb_devices_destroy(&vec); + return false; + } + + if (sel_count > 1) { + switch (selector->type) { + case SC_ADB_DEVICE_SELECT_ALL: + LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count); + break; + case SC_ADB_DEVICE_SELECT_SERIAL: + assert(selector->serial); + LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", + sel_count, selector->serial); + break; + case SC_ADB_DEVICE_SELECT_USB: + LOGE("Multiple (%" SC_PRIsizet ") ADB devices over USB:", + sel_count); + break; + case SC_ADB_DEVICE_SELECT_TCPIP: + LOGE("Multiple (%" SC_PRIsizet ") ADB devices over TCP/IP:", + sel_count); + break; + default: + assert(!"Unexpected selector type"); + break; + } + sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); + LOGE("Select a device via -s (--serial), -d (--select-usb) or -e " + "(--select-tcpip)"); + sc_adb_devices_destroy(&vec); + return false; + } + + assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 + struct sc_adb_device *device = &vec.data[sel_idx]; + + ok = sc_adb_device_check_state(device, vec.data, vec.size); + if (!ok) { + sc_adb_devices_destroy(&vec); + return false; + } + + LOGI("ADB device found:"); + sc_adb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size); + + // Move devics into out_device (do not destroy device) + sc_adb_device_move(out_device, device); + sc_adb_devices_destroy(&vec); + return true; +} + +char * +sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, + unsigned flags) { + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "shell", "getprop", prop); + + sc_pipe pout; + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGE("Could not execute \"adb getprop\""); + return NULL; + } + + char buf[128]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "adb getprop", flags); + if (!ok) { + return NULL; + } + + if (r == -1) { + return NULL; + } + + assert((size_t) r < sizeof(buf)); + buf[r] = '\0'; + size_t len = strcspn(buf, " \r\n"); + buf[len] = '\0'; + + return strdup(buf); +} + +char * +sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { + assert(serial); + const char *const argv[] = + SC_ADB_COMMAND("-s", serial, "shell", "ip", "route"); + + sc_pipe pout; + sc_pid pid = sc_adb_execute_p(argv, flags, &pout); + if (pid == SC_PROCESS_NONE) { + LOGD("Could not execute \"ip route\""); + return NULL; + } + + // "adb shell ip route" output should contain only a few lines + char buf[1024]; + ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); + sc_pipe_close(pout); + + bool ok = process_check_success_intr(intr, pid, "ip route", flags); + if (!ok) { + return NULL; + } + + if (r == -1) { + return NULL; + } + + assert((size_t) r < sizeof(buf)); + if (r == sizeof(buf) - 1) { + // The implementation assumes that the output of "ip route" fits in the + // buffer in a single pass + LOGW("Result of \"ip route\" does not fit in 1Kb. " + "Please report an issue."); + return NULL; + } + + // It is parsed as a NUL-terminated string + buf[r] = '\0'; + + return sc_adb_parse_device_ip(buf); +} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h new file mode 100644 index 00000000..ffd532ea --- /dev/null +++ b/app/src/adb/adb.h @@ -0,0 +1,117 @@ +#ifndef SC_ADB_H +#define SC_ADB_H + +#include "common.h" + +#include +#include + +#include "adb_device.h" +#include "util/intr.h" + +#define SC_ADB_NO_STDOUT (1 << 0) +#define SC_ADB_NO_STDERR (1 << 1) +#define SC_ADB_NO_LOGERR (1 << 2) + +#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) + +const char * +sc_adb_get_executable(void); + +enum sc_adb_device_selector_type { + SC_ADB_DEVICE_SELECT_ALL, + SC_ADB_DEVICE_SELECT_SERIAL, + SC_ADB_DEVICE_SELECT_USB, + SC_ADB_DEVICE_SELECT_TCPIP, +}; + +struct sc_adb_device_selector { + enum sc_adb_device_selector_type type; + const char *serial; +}; + +sc_pid +sc_adb_execute(const char *const argv[], unsigned flags); + +bool +sc_adb_start_server(struct sc_intr *intr, unsigned flags); + +bool +sc_adb_kill_server(struct sc_intr *intr, unsigned flags); + +bool +sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, + const char *device_socket_name, unsigned flags); + +bool +sc_adb_forward_remove(struct sc_intr *intr, const char *serial, + uint16_t local_port, unsigned flags); + +bool +sc_adb_reverse(struct sc_intr *intr, const char *serial, + const char *device_socket_name, uint16_t local_port, + unsigned flags); + +bool +sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, + const char *device_socket_name, unsigned flags); + +bool +sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, + const char *remote, unsigned flags); + +bool +sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, + unsigned flags); + +/** + * Execute `adb tcpip ` + */ +bool +sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, + unsigned flags); + +/** + * Execute `adb connect ` + * + * `ip_port` may not be NULL. + */ +bool +sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); + +/** + * Execute `adb disconnect []` + * + * If `ip_port` is NULL, execute `adb disconnect`. + * Otherwise, execute `adb disconnect `. + */ +bool +sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); + +/** + * Execute `adb devices` and parse the result to select a device + * + * Return true if a single matching device is found, and write it to out_device. + */ +bool +sc_adb_select_device(struct sc_intr *intr, + const struct sc_adb_device_selector *selector, + unsigned flags, struct sc_adb_device *out_device); + +/** + * Execute `adb getprop ` + */ +char * +sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, + unsigned flags); + +/** + * Attempt to retrieve the device IP + * + * Return the IP as a string of the form "xxx.xxx.xxx.xxx", to be freed by the + * caller, or NULL on error. + */ +char * +sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); + +#endif diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c new file mode 100644 index 00000000..5ea8eb44 --- /dev/null +++ b/app/src/adb/adb_device.c @@ -0,0 +1,43 @@ +#include "adb_device.h" + +#include +#include + +void +sc_adb_device_destroy(struct sc_adb_device *device) { + free(device->serial); + free(device->state); + free(device->model); +} + +void +sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) { + *dst = *src; + src->serial = NULL; + src->state = NULL; + src->model = NULL; +} + +void +sc_adb_devices_destroy(struct sc_vec_adb_devices *devices) { + for (size_t i = 0; i < devices->size; ++i) { + sc_adb_device_destroy(&devices->data[i]); + } + sc_vector_destroy(devices); +} + +enum sc_adb_device_type +sc_adb_device_get_type(const char *serial) { + // Starts with "emulator-" + if (!strncmp(serial, "emulator-", sizeof("emulator-") - 1)) { + return SC_ADB_DEVICE_TYPE_EMULATOR; + } + + // If the serial contains a ':', then it is a TCP/IP device (it is + // sufficient to distinguish an ip:port from a real USB serial) + if (strchr(serial, ':')) { + return SC_ADB_DEVICE_TYPE_TCPIP; + } + + return SC_ADB_DEVICE_TYPE_USB; +} diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h new file mode 100644 index 00000000..56393bcf --- /dev/null +++ b/app/src/adb/adb_device.h @@ -0,0 +1,50 @@ +#ifndef SC_ADB_DEVICE_H +#define SC_ADB_DEVICE_H + +#include "common.h" + +#include +#include + +#include "util/vector.h" + +struct sc_adb_device { + char *serial; + char *state; + char *model; + bool selected; +}; + +enum sc_adb_device_type { + SC_ADB_DEVICE_TYPE_USB, + SC_ADB_DEVICE_TYPE_TCPIP, + SC_ADB_DEVICE_TYPE_EMULATOR, +}; + +struct sc_vec_adb_devices SC_VECTOR(struct sc_adb_device); + +void +sc_adb_device_destroy(struct sc_adb_device *device); + +/** + * Move src to dst + * + * After this call, the content of src is undefined, except that + * sc_adb_device_destroy() can be called. + * + * This is useful to take a device from a list that will be destroyed, without + * making unnecessary copies. + */ +void +sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); + +void +sc_adb_devices_destroy(struct sc_vec_adb_devices *devices); + +/** + * Deduce the device type from the serial + */ +enum sc_adb_device_type +sc_adb_device_get_type(const char *serial); + +#endif diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c new file mode 100644 index 00000000..66bb1854 --- /dev/null +++ b/app/src/adb/adb_parser.c @@ -0,0 +1,228 @@ +#include "adb_parser.h" + +#include +#include +#include + +#include "util/log.h" +#include "util/str.h" + +static bool +sc_adb_parse_device(char *line, struct sc_adb_device *device) { + // One device line looks like: + // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + // "device:MyDevice transport_id:1" + + if (line[0] == '*') { + // Garbage lines printed by adb daemon while starting start with a '*' + return false; + } + + if (!strncmp("adb server", line, sizeof("adb server") - 1)) { + // Ignore lines starting with "adb server": + // adb server version (41) doesn't match this client (39); killing... + return false; + } + + char *s = line; // cursor in the line + + // After the serial: + // - "adb devices" writes a single '\t' + // - "adb devices -l" writes multiple spaces + // For flexibility, accept both. + size_t serial_len = strcspn(s, " \t"); + if (!serial_len) { + // empty serial + return false; + } + bool eol = s[serial_len] == '\0'; + if (eol) { + // serial alone is unexpected + return false; + } + s[serial_len] = '\0'; + char *serial = s; + s += serial_len + 1; + // After the serial, there might be several spaces + s += strspn(s, " \t"); // consume all separators + + size_t state_len = strcspn(s, " "); + if (!state_len) { + // empty state + return false; + } + eol = s[state_len] == '\0'; + s[state_len] = '\0'; + char *state = s; + + char *model = NULL; + if (!eol) { + s += state_len + 1; + + // Iterate over all properties "key:value key:value ..." + for (;;) { + size_t token_len = strcspn(s, " "); + if (!token_len) { + break; + } + eol = s[token_len] == '\0'; + s[token_len] = '\0'; + char *token = s; + + if (!strncmp("model:", token, sizeof("model:") - 1)) { + model = &token[sizeof("model:") - 1]; + // We only need the model + break; + } + + if (eol) { + break; + } else { + s+= token_len + 1; + } + } + } + + device->serial = strdup(serial); + if (!device->serial) { + return false; + } + + device->state = strdup(state); + if (!device->state) { + free(device->serial); + return false; + } + + if (model) { + device->model = strdup(model); + if (!device->model) { + LOG_OOM(); + // model is optional, do not fail + } + } else { + device->model = NULL; + } + + device->selected = false; + + return true; +} + +bool +sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec) { +#define HEADER "List of devices attached" +#define HEADER_LEN (sizeof(HEADER) - 1) + bool header_found = false; + + size_t idx_line = 0; + while (str[idx_line] != '\0') { + char *line = &str[idx_line]; + size_t len = strcspn(line, "\n"); + + // The next line starts after the '\n' (replaced by `\0`) + idx_line += len; + + if (str[idx_line] != '\0') { + // The next line starts after the '\n' + ++idx_line; + } + + if (!header_found) { + if (!strncmp(line, HEADER, HEADER_LEN)) { + header_found = true; + } + // Skip everything until the header, there might be garbage lines + // related to daemon starting before + continue; + } + + // The line, but without any trailing '\r' + size_t line_len = sc_str_remove_trailing_cr(line, len); + line[line_len] = '\0'; + + struct sc_adb_device device; + bool ok = sc_adb_parse_device(line, &device); + if (!ok) { + continue; + } + + ok = sc_vector_push(out_vec, device); + if (!ok) { + LOG_OOM(); + LOGE("Could not push adb_device to vector"); + sc_adb_device_destroy(&device); + // continue anyway + continue; + } + } + + assert(header_found || out_vec->size == 0); + return header_found; +} + +static char * +sc_adb_parse_device_ip_from_line(char *line) { + // One line from "ip route" looks like: + // "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x" + + // Get the location of the device name (index of "wlan0" in the example) + ssize_t idx_dev_name = sc_str_index_of_column(line, 2, " "); + if (idx_dev_name == -1) { + return NULL; + } + + // Get the location of the ip address (column 8, but column 6 if we start + // from column 2). Must be computed before truncating individual columns. + ssize_t idx_ip = sc_str_index_of_column(&line[idx_dev_name], 6, " "); + if (idx_ip == -1) { + return NULL; + } + // idx_ip is searched from &line[idx_dev_name] + idx_ip += idx_dev_name; + + char *dev_name = &line[idx_dev_name]; + size_t dev_name_len = strcspn(dev_name, " \t"); + dev_name[dev_name_len] = '\0'; + + char *ip = &line[idx_ip]; + size_t ip_len = strcspn(ip, " \t"); + ip[ip_len] = '\0'; + + // Only consider lines where the device name starts with "wlan" + if (strncmp(dev_name, "wlan", sizeof("wlan") - 1)) { + LOGD("Device ip lookup: ignoring %s (%s)", ip, dev_name); + return NULL; + } + + return strdup(ip); +} + +char * +sc_adb_parse_device_ip(char *str) { + size_t idx_line = 0; + while (str[idx_line] != '\0') { + char *line = &str[idx_line]; + size_t len = strcspn(line, "\n"); + bool is_last_line = line[len] == '\0'; + + // The same, but without any trailing '\r' + size_t line_len = sc_str_remove_trailing_cr(line, len); + line[line_len] = '\0'; + + char *ip = sc_adb_parse_device_ip_from_line(line); + if (ip) { + // Found + return ip; + } + + if (is_last_line) { + break; + } + + // The next line starts after the '\n' + idx_line += len + 1; + } + + return NULL; +} diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h new file mode 100644 index 00000000..f20349f6 --- /dev/null +++ b/app/src/adb/adb_parser.h @@ -0,0 +1,30 @@ +#ifndef SC_ADB_PARSER_H +#define SC_ADB_PARSER_H + +#include "common.h" + +#include + +#include "adb_device.h" + +/** + * Parse the available devices from the output of `adb devices` + * + * The parameter must be a NUL-terminated string. + * + * Warning: this function modifies the buffer for optimization purposes. + */ +bool +sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec); + +/** + * Parse the ip from the output of `adb shell ip route` + * + * The parameter must be a NUL-terminated string. + * + * Warning: this function modifies the buffer for optimization purposes. + */ +char * +sc_adb_parse_device_ip(char *str); + +#endif diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c new file mode 100644 index 00000000..fa936e4b --- /dev/null +++ b/app/src/adb/adb_tunnel.c @@ -0,0 +1,172 @@ +#include "adb_tunnel.h" + +#include + +#include "adb.h" +#include "util/log.h" +#include "util/net_intr.h" +#include "util/process_intr.h" + +static bool +listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { + return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); +} + +static bool +enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, + struct sc_intr *intr, const char *serial, + const char *device_socket_name, + struct sc_port_range port_range) { + uint16_t port = port_range.first; + for (;;) { + if (!sc_adb_reverse(intr, serial, device_socket_name, port, + SC_ADB_NO_STDOUT)) { + // the command itself failed, it will fail on any port + return false; + } + + // At the application level, the device part is "the server" because it + // serves video stream and control. However, at the network level, the + // client listens and the server connects to the client. That way, the + // client can listen before starting the server app, so there is no + // need to try to connect until the server socket is listening on the + // device. + sc_socket server_socket = net_socket(); + if (server_socket != SC_SOCKET_NONE) { + bool ok = listen_on_port(intr, server_socket, port); + if (ok) { + // success + tunnel->server_socket = server_socket; + tunnel->local_port = port; + tunnel->enabled = true; + return true; + } + + net_close(server_socket); + } + + if (sc_intr_is_interrupted(intr)) { + // Stop immediately + return false; + } + + // failure, disable tunnel and try another port + if (!sc_adb_reverse_remove(intr, serial, device_socket_name, + SC_ADB_NO_STDOUT)) { + LOGW("Could not remove reverse tunnel on port %" PRIu16, port); + } + + // check before incrementing to avoid overflow on port 65535 + if (port < port_range.last) { + LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16, + port, (uint16_t) (port + 1)); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not listen on port %" PRIu16, port_range.first); + } else { + LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +static bool +enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel, + struct sc_intr *intr, const char *serial, + const char *device_socket_name, + struct sc_port_range port_range) { + tunnel->forward = true; + + uint16_t port = port_range.first; + for (;;) { + if (sc_adb_forward(intr, serial, port, device_socket_name, + SC_ADB_NO_STDOUT)) { + // success + tunnel->local_port = port; + tunnel->enabled = true; + return true; + } + + if (sc_intr_is_interrupted(intr)) { + // Stop immediately + return false; + } + + if (port < port_range.last) { + LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16, + port, (uint16_t) (port + 1)); + port++; + continue; + } + + if (port_range.first == port_range.last) { + LOGE("Could not forward port %" PRIu16, port_range.first); + } else { + LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16, + port_range.first, port_range.last); + } + return false; + } +} + +void +sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) { + tunnel->enabled = false; + tunnel->forward = false; + tunnel->server_socket = SC_SOCKET_NONE; + tunnel->local_port = 0; +} + +bool +sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial, const char *device_socket_name, + struct sc_port_range port_range, bool force_adb_forward) { + assert(!tunnel->enabled); + + if (!force_adb_forward) { + // Attempt to use "adb reverse" + if (enable_tunnel_reverse_any_port(tunnel, intr, serial, + device_socket_name, port_range)) { + return true; + } + + // if "adb reverse" does not work (e.g. over "adb connect"), it + // fallbacks to "adb forward", so the app socket is the client + + LOGW("'adb reverse' failed, fallback to 'adb forward'"); + } + + return enable_tunnel_forward_any_port(tunnel, intr, serial, + device_socket_name, port_range); +} + +bool +sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial, const char *device_socket_name) { + assert(tunnel->enabled); + + bool ret; + if (tunnel->forward) { + ret = sc_adb_forward_remove(intr, serial, tunnel->local_port, + SC_ADB_NO_STDOUT); + } else { + ret = sc_adb_reverse_remove(intr, serial, device_socket_name, + SC_ADB_NO_STDOUT); + + assert(tunnel->server_socket != SC_SOCKET_NONE); + if (!net_close(tunnel->server_socket)) { + LOGW("Could not close server socket"); + } + + // server_socket is never used anymore + } + + // Consider tunnel disabled even if the command failed + tunnel->enabled = false; + + return ret; +} diff --git a/app/src/adb/adb_tunnel.h b/app/src/adb/adb_tunnel.h new file mode 100644 index 00000000..7ed5bf54 --- /dev/null +++ b/app/src/adb/adb_tunnel.h @@ -0,0 +1,47 @@ +#ifndef SC_ADB_TUNNEL_H +#define SC_ADB_TUNNEL_H + +#include "common.h" + +#include +#include + +#include "options.h" +#include "util/intr.h" +#include "util/net.h" + +struct sc_adb_tunnel { + bool enabled; + bool forward; // use "adb forward" instead of "adb reverse" + sc_socket server_socket; // only used if !forward + uint16_t local_port; +}; + +/** + * Initialize the adb tunnel struct to default values + */ +void +sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel); + +/** + * Open a tunnel + * + * Blocking calls may be interrupted asynchronously via `intr`. + * + * If `force_adb_forward` is not set, then attempts to set up an "adb reverse" + * tunnel first. Only if it fails (typical on old Android version connected via + * TCP/IP), use "adb forward". + */ +bool +sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial, const char *device_socket_name, + struct sc_port_range port_range, bool force_adb_forward); + +/** + * Close the tunnel + */ +bool +sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, + const char *serial, const char *device_socket_name); + +#endif diff --git a/app/src/audio_player.c b/app/src/audio_player.c new file mode 100644 index 00000000..bd799c51 --- /dev/null +++ b/app/src/audio_player.c @@ -0,0 +1,487 @@ +#include "audio_player.h" + +#include +#include + +#include "util/log.h" + +#define SC_AUDIO_PLAYER_NDEBUG // comment to debug + +/** + * Real-time audio player with configurable latency + * + * As input, the player regularly receives AVFrames of decoded audio samples. + * As output, an SDL callback regularly requests audio samples to be played. + * In the middle, an audio buffer stores the samples produced but not consumed + * yet. + * + * The goal of the player is to feed the audio output with a latency as low as + * possible while avoiding buffer underrun (i.e. not being able to provide + * samples when requested). + * + * The player aims to feed the audio output with as little latency as possible + * while avoiding buffer underrun. To achieve this, it attempts to maintain the + * average buffering (the number of samples present in the buffer) around a + * target value. If this target buffering is too low, then buffer underrun will + * occur frequently. If it is too high, then latency will become unacceptable. + * This target value is configured using the scrcpy option --audio-buffer. + * + * The player cannot adjust the sample input rate (it receives samples produced + * in real-time) or the sample output rate (it must provide samples as + * requested by the audio output callback). Therefore, it may only apply + * compensation by resampling (converting _m_ input samples to _n_ output + * samples). + * + * The compensation itself is applied by libswresample (FFmpeg). It is + * configured using swr_set_compensation(). An important work for the player + * is to estimate the compensation value regularly and apply it. + * + * The estimated buffering level is the result of averaging the "natural" + * buffering (samples are produced and consumed by blocks, so it must be + * smoothed), and making instant adjustments resulting of its own actions + * (explicit compensation and silence insertion on underflow), which are not + * smoothed. + * + * Buffer underflow events can occur when packets arrive too late. In that case, + * the player inserts silence. Once the packets finally arrive (late), one + * strategy could be to drop the samples that were replaced by silence, in + * order to keep a minimal latency. However, dropping samples in case of buffer + * underflow is inadvisable, as it would temporarily increase the underflow + * even more and cause very noticeable audio glitches. + * + * Therefore, the player doesn't drop any sample on underflow. The compensation + * mechanism will absorb the delay introduced by the inserted silence. + */ + +/** Downcast frame_sink to sc_audio_player */ +#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) + +#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT +#define SC_SDL_SAMPLE_FMT AUDIO_F32 + +#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES)) +#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) + +static void SDLCALL +sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { + struct sc_audio_player *ap = userdata; + + // This callback is called with the lock used by SDL_LockAudioDevice() + + assert(len_int > 0); + size_t len = len_int; + uint32_t count = TO_SAMPLES(len); + +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); +#endif + + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); + if (!played) { + uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); + // Wait until the buffer is filled up to at least target_buffering + // before playing + if (buffered_samples < ap->target_buffering) { + LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 + " samples", count); + // Delay playback starting to reach the target buffering. Fill the + // whole buffer with silence (len is small compared to the + // arbitrary margin value). + memset(stream, 0, len); + return; + } + } + + uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); + + if (read < count) { + uint32_t silence = count - read; + // Insert silence. In theory, the inserted silent samples replace the + // missing real samples, which will arrive later, so they should be + // dropped to keep the latency minimal. However, this would cause very + // audible glitches, so let the clock compensation restore the target + // latency. + LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", + silence); + memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); + + bool received = atomic_load_explicit(&ap->received, + memory_order_relaxed); + if (received) { + // Inserting additional samples immediately increases buffering + atomic_fetch_add_explicit(&ap->underflow, silence, + memory_order_relaxed); + } + } + + atomic_store_explicit(&ap->played, true, memory_order_relaxed); +} + +static uint8_t * +sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) { + size_t min_buf_size = TO_BYTES(min_samples); + if (min_buf_size > ap->swr_buf_alloc_size) { + size_t new_size = min_buf_size + 4096; + uint8_t *buf = realloc(ap->swr_buf, new_size); + if (!buf) { + LOG_OOM(); + // Could not realloc to the requested size + return NULL; + } + ap->swr_buf = buf; + ap->swr_buf_alloc_size = new_size; + } + + return ap->swr_buf; +} + +static bool +sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, + const AVFrame *frame) { + struct sc_audio_player *ap = DOWNCAST(sink); + + SwrContext *swr_ctx = ap->swr_ctx; + + int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate); + // No need to av_rescale_rnd(), input and output sample rates are the same. + // Add more space (256) for clock compensation. + int dst_nb_samples = swr_delay + frame->nb_samples + 256; + + uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples); + if (!swr_buf) { + return false; + } + + int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, + (const uint8_t **) frame->data, frame->nb_samples); + if (ret < 0) { + LOGE("Resampling failed: %d", ret); + return false; + } + + // swr_convert() returns the number of samples which would have been + // written if the buffer was big enough. + uint32_t samples = MIN(ret, dst_nb_samples); +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); +#endif + + uint32_t cap = sc_audiobuf_capacity(&ap->buf); + if (samples > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the audio buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf to avoid memory corruption anyway. + swr_buf += TO_BYTES(samples - cap); + samples = cap; + } + + uint32_t skipped_samples = 0; + + uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples); + if (written < samples) { + uint32_t remaining = samples - written; + + // All samples that could be written without locking have been written, + // now we need to lock to drop/consume old samples + SDL_LockAudioDevice(ap->device); + + // Retry with the lock + written += sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + if (written < samples) { + remaining = samples - written; + // Still insufficient, drop old samples to make space + skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); + assert(skipped_samples == remaining); + + // Now there is enough space + uint32_t w = sc_audiobuf_write(&ap->buf, + swr_buf + TO_BYTES(written), + remaining); + assert(w == remaining); + (void) w; + } + + SDL_UnlockAudioDevice(ap->device); + } + + uint32_t underflow = 0; + uint32_t max_buffered_samples; + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); + if (played) { + underflow = atomic_exchange_explicit(&ap->underflow, 0, + memory_order_relaxed); + + max_buffered_samples = ap->target_buffering + + 12 * ap->output_buffer + + ap->target_buffering / 10; + } else { + // SDL playback not started yet, do not accumulate more than + // max_initial_buffering samples, this would cause unnecessary delay + // (and glitches to compensate) on start. + max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer; + } + + uint32_t can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + uint32_t skip_samples = 0; + + SDL_LockAudioDevice(ap->device); + can_read = sc_audiobuf_can_read(&ap->buf); + if (can_read > max_buffered_samples) { + skip_samples = can_read - max_buffered_samples; + uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples); + assert(r == skip_samples); + (void) r; + skipped_samples += skip_samples; + } + SDL_UnlockAudioDevice(ap->device); + + if (skip_samples) { + if (played) { + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); +#ifndef SC_AUDIO_PLAYER_NDEBUG + } else { + LOGD("[Audio] Playback not started, skipping %" PRIu32 + " samples", skip_samples); +#endif + } + } + } + + atomic_store_explicit(&ap->received, true, memory_order_relaxed); + if (!played) { + // Nothing more to do + return true; + } + + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = (int32_t) written - frame->nb_samples; + // Inserting silence instantly increases buffering + int32_t inserted_silence = (int32_t) underflow; + // Dropping input samples instantly decreases buffering + int32_t dropped = (int32_t) skipped_samples; + + // The compensation must apply instantly, it must not be smoothed + ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped; + if (ap->avg_buffering.avg < 0) { + // Since dropping samples instantly reduces buffering, the difference + // is applied immediately to the average value, assuming that the delay + // between the producer and the consumer will be caught up. + // + // However, when this assumption is not valid, the average buffering + // may decrease indefinitely. Prevent it to become negative to limit + // the consequences. + ap->avg_buffering.avg = 0; + } + + // However, the buffering level must be smoothed + sc_average_push(&ap->avg_buffering, can_read); + +#ifndef SC_AUDIO_PLAYER_NDEBUG + LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", + can_read, sc_average_get(&ap->avg_buffering)); +#endif + + ap->samples_since_resync += written; + if (ap->samples_since_resync >= ap->sample_rate) { + // Recompute compensation every second + ap->samples_since_resync = 0; + + float avg = sc_average_get(&ap->avg_buffering); + int diff = ap->target_buffering - avg; + + // Enable compensation when the difference exceeds +/- 4ms. + // Disable compensation when the difference is lower than +/- 1ms. + int threshold = ap->compensation != 0 + ? ap->sample_rate / 1000 /* 1ms */ + : ap->sample_rate * 4 / 1000; /* 4ms */ + + if (abs(diff) < threshold) { + // Do not compensate for small values, the error is just noise + diff = 0; + } else if (diff < 0 && can_read < ap->target_buffering) { + // Do not accelerate if the instant buffering level is below the + // target, this would increase underflow + diff = 0; + } + // Compensate the diff over 4 seconds (but will be recomputed after 1 + // second) + int distance = 4 * ap->sample_rate; + // Limit compensation rate to 2% + int abs_max_diff = distance / 50; + diff = CLAMP(diff, -abs_max_diff, abs_max_diff); + LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 + " compensation=%d", ap->target_buffering, avg, can_read, diff); + + if (diff != ap->compensation) { + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ap->compensation = diff; + } + } + } + + return true; +} + +static bool +sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, + const AVCodecContext *ctx) { + struct sc_audio_player *ap = DOWNCAST(sink); +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + assert(ctx->ch_layout.nb_channels > 0); + unsigned nb_channels = ctx->ch_layout.nb_channels; +#else + int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout); + assert(tmp > 0); + unsigned nb_channels = tmp; +#endif + + assert(ctx->sample_rate > 0); + assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT)); + int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); + assert(out_bytes_per_sample > 0); + + ap->sample_rate = ctx->sample_rate; + ap->nb_channels = nb_channels; + ap->out_bytes_per_sample = out_bytes_per_sample; + + ap->target_buffering = ap->target_buffering_delay * ap->sample_rate + / SC_TICK_FREQ; + + uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate + / SC_TICK_FREQ; + assert(aout_samples <= 0xFFFF); + ap->output_buffer = (uint16_t) aout_samples; + + SDL_AudioSpec desired = { + .freq = ctx->sample_rate, + .format = SC_SDL_SAMPLE_FMT, + .channels = nb_channels, + .samples = aout_samples, + .callback = sc_audio_player_sdl_callback, + .userdata = ap, + }; + SDL_AudioSpec obtained; + + ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); + if (!ap->device) { + LOGE("Could not open audio device: %s", SDL_GetError()); + return false; + } + + SwrContext *swr_ctx = swr_alloc(); + if (!swr_ctx) { + LOG_OOM(); + goto error_close_audio_device; + } + ap->swr_ctx = swr_ctx; + +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); + av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); +#else + av_opt_set_channel_layout(swr_ctx, "in_channel_layout", + ctx->channel_layout, 0); + av_opt_set_channel_layout(swr_ctx, "out_channel_layout", + ctx->channel_layout, 0); +#endif + + av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); + av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); + + av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); + av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); + + int ret = swr_init(swr_ctx); + if (ret) { + LOGE("Failed to initialize the resampling context"); + goto error_free_swr_ctx; + } + + // Use a ring-buffer of the target buffering size plus 1 second between the + // producer and the consumer. It's too big on purpose, to guarantee that + // the producer and the consumer will be able to access it in parallel + // without locking. + uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; + + size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; + bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); + if (!ok) { + goto error_free_swr_ctx; + } + + size_t initial_swr_buf_size = TO_BYTES(4096); + ap->swr_buf = malloc(initial_swr_buf_size); + if (!ap->swr_buf) { + LOG_OOM(); + goto error_destroy_audiobuf; + } + ap->swr_buf_alloc_size = initial_swr_buf_size; + + // Samples are produced and consumed by blocks, so the buffering must be + // smoothed to get a relatively stable value. + sc_average_init(&ap->avg_buffering, 128); + ap->samples_since_resync = 0; + + ap->received = false; + atomic_init(&ap->played, false); + atomic_init(&ap->received, false); + atomic_init(&ap->underflow, 0); + ap->compensation = 0; + + // The thread calling open() is the thread calling push(), which fills the + // audio buffer consumed by the SDL audio thread. + ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL); + if (!ok) { + ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH); + (void) ok; // We don't care if it worked, at least we tried + } + + SDL_PauseAudioDevice(ap->device, 0); + + return true; + +error_destroy_audiobuf: + sc_audiobuf_destroy(&ap->buf); +error_free_swr_ctx: + swr_free(&ap->swr_ctx); +error_close_audio_device: + SDL_CloseAudioDevice(ap->device); + + return false; +} + +static void +sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_audio_player *ap = DOWNCAST(sink); + + assert(ap->device); + SDL_PauseAudioDevice(ap->device, 1); + SDL_CloseAudioDevice(ap->device); + + free(ap->swr_buf); + sc_audiobuf_destroy(&ap->buf); + swr_free(&ap->swr_ctx); +} + +void +sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering, + sc_tick output_buffer_duration) { + ap->target_buffering_delay = target_buffering; + ap->output_buffer_duration = output_buffer_duration; + + static const struct sc_frame_sink_ops ops = { + .open = sc_audio_player_frame_sink_open, + .close = sc_audio_player_frame_sink_close, + .push = sc_audio_player_frame_sink_push, + }; + + ap->frame_sink.ops = &ops; +} diff --git a/app/src/audio_player.h b/app/src/audio_player.h new file mode 100644 index 00000000..0c677363 --- /dev/null +++ b/app/src/audio_player.h @@ -0,0 +1,84 @@ +#ifndef SC_AUDIO_PLAYER_H +#define SC_AUDIO_PLAYER_H + +#include "common.h" + +#include +#include +#include +#include +#include + +#include "trait/frame_sink.h" +#include "util/audiobuf.h" +#include "util/average.h" +#include "util/thread.h" +#include "util/tick.h" + +struct sc_audio_player { + struct sc_frame_sink frame_sink; + + SDL_AudioDeviceID device; + + // The target buffering between the producer and the consumer. This value + // is directly use for compensation. + // Since audio capture and/or encoding on the device typically produce + // blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target + // value should be higher. + sc_tick target_buffering_delay; + uint32_t target_buffering; // in samples + + // SDL audio output buffer size. + sc_tick output_buffer_duration; + uint16_t output_buffer; + + // Audio buffer to communicate between the receiver and the SDL audio + // callback + struct sc_audiobuf buf; + + // Resampler (only used from the receiver thread) + struct SwrContext *swr_ctx; + + // The sample rate is the same for input and output + unsigned sample_rate; + // The number of channels is the same for input and output + unsigned nb_channels; + // The number of bytes per sample for a single channel + size_t out_bytes_per_sample; + + // Target buffer for resampling (only used by the receiver thread) + uint8_t *swr_buf; + size_t swr_buf_alloc_size; + + // Number of buffered samples (may be negative on underflow) (only used by + // the receiver thread) + struct sc_average avg_buffering; + // Count the number of samples to trigger a compensation update regularly + // (only used by the receiver thread) + uint32_t samples_since_resync; + + // Number of silence samples inserted since the last received packet + atomic_uint_least32_t underflow; + + // Current applied compensation value (only used by the receiver thread) + int compensation; + + // Set to true the first time a sample is received + atomic_bool received; + + // Set to true the first time the SDL callback is called + atomic_bool played; + + const struct sc_audio_player_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_audio_player_callbacks { + void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata); +}; + +void +sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering, + sc_tick audio_output_buffer); + +#endif diff --git a/app/src/cli.c b/app/src/cli.c index d22096ca..daa041cf 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -4,323 +4,1342 @@ #include #include #include +#include #include -#include "scrcpy.h" +#include "options.h" #include "util/log.h" -#include "util/str_util.h" +#include "util/net.h" +#include "util/str.h" +#include "util/strbuf.h" +#include "util/term.h" #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) -void -scrcpy_print_usage(const char *arg0) { - fprintf(stderr, - "Usage: %s [options]\n" - "\n" - "Options:\n" - "\n" - " --always-on-top\n" - " Make scrcpy window always on top (above other windows).\n" - "\n" - " -b, --bit-rate value\n" - " Encode the video at the given bit-rate, expressed in bits/s.\n" - " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" - " Default is " STR(DEFAULT_BIT_RATE) ".\n" - "\n" - " --codec-options key[:type]=value[,...]\n" - " Set a list of comma-separated key:type=value options for the\n" - " device encoder.\n" - " The possible values for 'type' are 'int' (default), 'long',\n" - " 'float' and 'string'.\n" - " The list of possible codec options is available in the\n" - " Android documentation:\n" - " \n" - "\n" - " --crop width:height:x:y\n" - " Crop the device screen on the server.\n" - " The values are expressed in the device natural orientation\n" - " (typically, portrait for a phone, landscape for a tablet).\n" - " Any --max-size value is computed on the cropped size.\n" - "\n" - " --disable-screensaver\n" - " Disable screensaver while scrcpy is running.\n" - "\n" - " --display id\n" - " Specify the display id to mirror.\n" - "\n" - " The list of possible display ids can be listed by:\n" - " adb shell dumpsys display\n" - " (search \"mDisplayId=\" in the output)\n" - "\n" - " Default is 0.\n" - "\n" - " --display-buffer ms\n" - " Add a buffering delay (in milliseconds) before displaying.\n" - " This increases latency to compensate for jitter.\n" - "\n" - " Default is 0 (no buffering).\n" - "\n" - " --encoder name\n" - " Use a specific MediaCodec encoder (must be a H.264 encoder).\n" - "\n" - " --force-adb-forward\n" - " Do not attempt to use \"adb reverse\" to connect to the\n" - " the device.\n" - "\n" - " --forward-all-clicks\n" - " By default, right-click triggers BACK (or POWER on) and\n" - " middle-click triggers HOME. This option disables these\n" - " shortcuts and forward the clicks to the device instead.\n" - "\n" - " -f, --fullscreen\n" - " Start in fullscreen.\n" - "\n" - " -h, --help\n" - " Print this help.\n" - "\n" - " --legacy-paste\n" - " Inject computer clipboard text as a sequence of key events\n" - " on Ctrl+v (like MOD+Shift+v).\n" - " This is a workaround for some devices not behaving as\n" - " expected when setting the device clipboard programmatically.\n" - "\n" - " --lock-video-orientation[=value]\n" - " Lock video orientation to value.\n" - " Possible values are \"unlocked\", \"initial\" (locked to the\n" - " initial orientation), 0, 1, 2 and 3.\n" - " Natural device orientation is 0, and each increment adds a\n" - " 90 degrees rotation counterclockwise.\n" - " Default is \"unlocked\".\n" - " Passing the option without argument is equivalent to passing\n" - " \"initial\".\n" - "\n" - " --max-fps value\n" - " Limit the frame rate of screen capture (officially supported\n" - " since Android 10, but may work on earlier versions).\n" - "\n" - " -m, --max-size value\n" - " Limit both the width and height of the video to value. The\n" - " other dimension is computed so that the device aspect-ratio\n" - " is preserved.\n" - " Default is 0 (unlimited).\n" - "\n" - " -n, --no-control\n" - " Disable device control (mirror the device in read-only).\n" - "\n" - " -N, --no-display\n" - " Do not display device (only when screen recording is\n" - " enabled).\n" - "\n" - " --no-key-repeat\n" - " Do not forward repeated key events when a key is held down.\n" - "\n" - " --no-mipmaps\n" - " If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then\n" - " mipmaps are automatically generated to improve downscaling\n" - " quality. This option disables the generation of mipmaps.\n" - "\n" - " -p, --port port[:port]\n" - " Set the TCP port (range) used by the client to listen.\n" - " Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" - STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".\n" - "\n" - " --prefer-text\n" - " Inject alpha characters and space as text events instead of\n" - " key events.\n" - " This avoids issues when combining multiple keys to enter a\n" - " special character, but breaks the expected behavior of alpha\n" - " keys in games (typically WASD).\n" - "\n" - " --push-target path\n" - " Set the target directory for pushing files to the device by\n" - " drag & drop. It is passed as-is to \"adb push\".\n" - " Default is \"/sdcard/Download/\".\n" - "\n" - " -r, --record file.mp4\n" - " Record screen to file.\n" - " The format is determined by the --record-format option if\n" - " set, or by the file extension (.mp4 or .mkv).\n" - "\n" - " --record-format format\n" - " Force recording format (either mp4 or mkv).\n" - "\n" - " --render-driver name\n" - " Request SDL to use the given render driver (this is just a\n" - " hint).\n" - " Supported names are currently \"direct3d\", \"opengl\",\n" - " \"opengles2\", \"opengles\", \"metal\" and \"software\".\n" - " \n" - "\n" - " --rotation value\n" - " Set the initial display rotation.\n" - " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n" - " degrees rotation counterclockwise.\n" - "\n" - " -s, --serial serial\n" - " The device serial number. Mandatory only if several devices\n" - " are connected to adb.\n" - "\n" - " --shortcut-mod key[+...]][,...]\n" - " Specify the modifiers to use for scrcpy shortcuts.\n" - " Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\",\n" - " \"lsuper\" and \"rsuper\".\n" - "\n" - " A shortcut can consist in several keys, separated by '+'.\n" - " Several shortcuts can be specified, separated by ','.\n" - "\n" - " For example, to use either LCtrl+LAlt or LSuper for scrcpy\n" - " shortcuts, pass \"lctrl+lalt,lsuper\".\n" - "\n" - " Default is \"lalt,lsuper\" (left-Alt or left-Super).\n" - "\n" - " -S, --turn-screen-off\n" - " Turn the device screen off immediately.\n" - "\n" - " -t, --show-touches\n" - " Enable \"show touches\" on start, restore the initial value\n" - " on exit.\n" - " It only shows physical touches (not clicks from scrcpy).\n" - "\n" -#ifdef HAVE_V4L2 - " --v4l2-sink /dev/videoN\n" - " Output to v4l2loopback device.\n" - " It requires to lock the video orientation (see\n" - " --lock-video-orientation).\n" - "\n" - " --v4l2-buffer ms\n" - " Add a buffering delay (in milliseconds) before pushing\n" - " frames. This increases latency to compensate for jitter.\n" - "\n" - " This option is similar to --display-buffer, but specific to\n" - " V4L2 sink.\n" - "\n" - " Default is 0 (no buffering).\n" - "\n" -#endif - " -V, --verbosity value\n" - " Set the log level (verbose, debug, info, warn or error).\n" +enum { + OPT_BIT_RATE = 1000, + OPT_WINDOW_TITLE, + OPT_PUSH_TARGET, + OPT_ALWAYS_ON_TOP, + OPT_CROP, + OPT_RECORD_FORMAT, + OPT_PREFER_TEXT, + OPT_WINDOW_X, + OPT_WINDOW_Y, + OPT_WINDOW_WIDTH, + OPT_WINDOW_HEIGHT, + OPT_WINDOW_BORDERLESS, + OPT_MAX_FPS, + OPT_LOCK_VIDEO_ORIENTATION, + OPT_DISPLAY, + OPT_DISPLAY_ID, + OPT_ROTATION, + OPT_RENDER_DRIVER, + OPT_NO_MIPMAPS, + OPT_CODEC_OPTIONS, + OPT_VIDEO_CODEC_OPTIONS, + OPT_FORCE_ADB_FORWARD, + OPT_DISABLE_SCREENSAVER, + OPT_SHORTCUT_MOD, + OPT_NO_KEY_REPEAT, + OPT_FORWARD_ALL_CLICKS, + OPT_LEGACY_PASTE, + OPT_ENCODER, + OPT_VIDEO_ENCODER, + OPT_POWER_OFF_ON_CLOSE, + OPT_V4L2_SINK, + OPT_DISPLAY_BUFFER, + OPT_V4L2_BUFFER, + OPT_TUNNEL_HOST, + OPT_TUNNEL_PORT, + OPT_NO_CLIPBOARD_AUTOSYNC, + OPT_TCPIP, + OPT_RAW_KEY_EVENTS, + OPT_NO_DOWNSIZE_ON_ERROR, + OPT_OTG, + OPT_NO_CLEANUP, + OPT_PRINT_FPS, + OPT_NO_POWER_ON, + OPT_CODEC, + OPT_VIDEO_CODEC, + OPT_NO_AUDIO, + OPT_AUDIO_BIT_RATE, + OPT_AUDIO_CODEC, + OPT_AUDIO_CODEC_OPTIONS, + OPT_AUDIO_ENCODER, + OPT_LIST_ENCODERS, + OPT_LIST_DISPLAYS, + OPT_REQUIRE_AUDIO, + OPT_AUDIO_BUFFER, + OPT_AUDIO_OUTPUT_BUFFER, + OPT_NO_DISPLAY, + OPT_NO_VIDEO, + OPT_NO_AUDIO_PLAYBACK, + OPT_NO_VIDEO_PLAYBACK, + OPT_VIDEO_SOURCE, + OPT_AUDIO_SOURCE, + OPT_KILL_ADB_ON_CLOSE, + OPT_TIME_LIMIT, + OPT_PAUSE_ON_EXIT, + OPT_LIST_CAMERAS, + OPT_LIST_CAMERA_SIZES, + OPT_CAMERA_ID, + OPT_CAMERA_SIZE, + OPT_CAMERA_FACING, + OPT_CAMERA_AR, + OPT_CAMERA_FPS, + OPT_CAMERA_HIGH_SPEED, + OPT_DISPLAY_ORIENTATION, + OPT_RECORD_ORIENTATION, + OPT_ORIENTATION, + OPT_KEYBOARD, + OPT_MOUSE, + OPT_HID_KEYBOARD_DEPRECATED, + OPT_HID_MOUSE_DEPRECATED, +}; + +struct sc_option { + char shortopt; + int longopt_id; // either shortopt or longopt_id is non-zero + const char *longopt; + // no argument: argdesc == NULL && !optional_arg + // optional argument: argdesc != NULL && optional_arg + // required argument: argdesc != NULL && !optional_arg + const char *argdesc; + bool optional_arg; + const char *text; // if NULL, the option does not appear in the help +}; + +#define MAX_EQUIVALENT_SHORTCUTS 3 +struct sc_shortcut { + const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1]; + const char *text; +}; + +struct sc_envvar { + const char *name; + const char *text; +}; + +struct sc_exit_status { + unsigned value; + const char *text; +}; + +struct sc_getopt_adapter { + char *optstring; + struct option *longopts; +}; + +static const struct sc_option options[] = { + { + .longopt_id = OPT_ALWAYS_ON_TOP, + .longopt = "always-on-top", + .text = "Make scrcpy window always on top (above other windows).", + }, + { + .longopt_id = OPT_AUDIO_BIT_RATE, + .longopt = "audio-bit-rate", + .argdesc = "value", + .text = "Encode the audio at the given bit rate, expressed in bits/s. " + "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + "Default is 128K (128000).", + }, + { + .longopt_id = OPT_AUDIO_BUFFER, + .longopt = "audio-buffer", + .argdesc = "ms", + .text = "Configure the audio buffering delay (in milliseconds).\n" + "Lower values decrease the latency, but increase the " + "likelyhood of buffer underrun (causing audio glitches).\n" + "Default is 50.", + }, + { + .longopt_id = OPT_AUDIO_CODEC, + .longopt = "audio-codec", + .argdesc = "name", + .text = "Select an audio codec (opus, aac, flac or raw).\n" + "Default is opus.", + }, + { + .longopt_id = OPT_AUDIO_CODEC_OPTIONS, + .longopt = "audio-codec-options", + .argdesc = "key[:type]=value[,...]", + .text = "Set a list of comma-separated key:type=value options for the " + "device audio encoder.\n" + "The possible values for 'type' are 'int' (default), 'long', " + "'float' and 'string'.\n" + "The list of possible codec options is available in the " + "Android documentation: " + "", + }, + { + .longopt_id = OPT_AUDIO_ENCODER, + .longopt = "audio-encoder", + .argdesc = "name", + .text = "Use a specific MediaCodec audio encoder (depending on the " + "codec provided by --audio-codec).\n" + "The available encoders can be listed by --list-encoders.", + }, + { + .longopt_id = OPT_AUDIO_SOURCE, + .longopt = "audio-source", + .argdesc = "source", + .text = "Select the audio source (output or mic).\n" + "Default is output.", + }, + { + .longopt_id = OPT_AUDIO_OUTPUT_BUFFER, + .longopt = "audio-output-buffer", + .argdesc = "ms", + .text = "Configure the size of the SDL audio output buffer (in " + "milliseconds).\n" + "If you get \"robotic\" audio playback, you should test with " + "a higher value (10). Do not change this setting otherwise.\n" + "Default is 5.", + }, + { + .shortopt = 'b', + .longopt = "video-bit-rate", + .argdesc = "value", + .text = "Encode the video at the given bit rate, expressed in bits/s. " + "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" + "Default is 8M (8000000).", + }, + { + // deprecated + .longopt_id = OPT_BIT_RATE, + .longopt = "bit-rate", + .argdesc = "value", + }, + { + .longopt_id = OPT_CAMERA_AR, + .longopt = "camera-ar", + .argdesc = "ar", + .text = "Select the camera size by its aspect ratio (+/- 10%).\n" + "Possible values are \"sensor\" (use the camera sensor aspect " + "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " + "\"1.6\")." + }, + { + .longopt_id = OPT_CAMERA_ID, + .longopt = "camera-id", + .argdesc = "id", + .text = "Specify the device camera id to mirror.\n" + "The available camera ids can be listed by:\n" + " scrcpy --list-cameras", + }, + { + .longopt_id = OPT_CAMERA_FACING, + .longopt = "camera-facing", + .argdesc = "facing", + .text = "Select the device camera by its facing direction.\n" + "Possible values are \"front\", \"back\" and \"external\".", + }, + { + .longopt_id = OPT_CAMERA_HIGH_SPEED, + .longopt = "camera-high-speed", + .text = "Enable high-speed camera capture mode.\n" + "This mode is restricted to specific resolutions and frame " + "rates, listed by --list-camera-sizes.", + }, + { + .longopt_id = OPT_CAMERA_SIZE, + .longopt = "camera-size", + .argdesc = "x", + .text = "Specify an explicit camera capture size.", + }, + { + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", + .argdesc = "value", + .text = "Specify the camera capture frame rate.\n" + "If not specified, Android's default frame rate (30 fps) is " + "used.", + }, + { + // Not really deprecated (--codec has never been released), but without + // declaring an explicit --codec option, getopt_long() partial matching + // behavior would consider --codec to be equivalent to --codec-options, + // which would be confusing. + .longopt_id = OPT_CODEC, + .longopt = "codec", + .argdesc = "value", + }, + { + // deprecated + .longopt_id = OPT_CODEC_OPTIONS, + .longopt = "codec-options", + .argdesc = "key[:type]=value[,...]", + }, + { + .longopt_id = OPT_CROP, + .longopt = "crop", + .argdesc = "width:height:x:y", + .text = "Crop the device screen on the server.\n" + "The values are expressed in the device natural orientation " + "(typically, portrait for a phone, landscape for a tablet). " + "Any --max-size value is computed on the cropped size.", + }, + { + .shortopt = 'd', + .longopt = "select-usb", + .text = "Use USB device (if there is exactly one, like adb -d).\n" + "Also see -e (--select-tcpip).", + }, + { + .longopt_id = OPT_DISABLE_SCREENSAVER, + .longopt = "disable-screensaver", + .text = "Disable screensaver while scrcpy is running.", + }, + { + // deprecated + .longopt_id = OPT_DISPLAY, + .longopt = "display", + .argdesc = "id", + }, + { + .longopt_id = OPT_DISPLAY_BUFFER, + .longopt = "display-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before displaying. " + "This increases latency to compensate for jitter.\n" + "Default is 0 (no buffering).", + }, + { + .longopt_id = OPT_DISPLAY_ID, + .longopt = "display-id", + .argdesc = "id", + .text = "Specify the device display id to mirror.\n" + "The available display ids can be listed by:\n" + " scrcpy --list-displays\n" + "Default is 0.", + }, + { + .longopt_id = OPT_DISPLAY_ORIENTATION, + .longopt = "display-orientation", + .argdesc = "value", + .text = "Set the initial display orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270. The number represents the clockwise rotation " + "in degrees; the \"flip\" keyword applies a horizontal flip " + "before the rotation.\n" + "Default is 0.", + }, + { + .shortopt = 'e', + .longopt = "select-tcpip", + .text = "Use TCP/IP device (if there is exactly one, like adb -e).\n" + "Also see -d (--select-usb).", + }, + { + // deprecated + .longopt_id = OPT_ENCODER, + .longopt = "encoder", + .argdesc = "name", + }, + { + .shortopt = 'f', + .longopt = "fullscreen", + .text = "Start in fullscreen.", + }, + { + .longopt_id = OPT_FORCE_ADB_FORWARD, + .longopt = "force-adb-forward", + .text = "Do not attempt to use \"adb reverse\" to connect to the " + "device.", + }, + { + .longopt_id = OPT_FORWARD_ALL_CLICKS, + .longopt = "forward-all-clicks", + .text = "By default, right-click triggers BACK (or POWER on) and " + "middle-click triggers HOME. This option disables these " + "shortcuts and forwards the clicks to the device instead.", + }, + { + .shortopt = 'h', + .longopt = "help", + .text = "Print this help.", + }, + { + .shortopt = 'K', + .text = "Same as --keyboard=uhid.", + }, + { + .longopt_id = OPT_KEYBOARD, + .longopt = "keyboard", + .argdesc = "mode", + .text = "Select how to send keyboard inputs to the device.\n" + "Possible values are \"disabled\", \"sdk\", \"uhid\" and " + "\"aoa\".\n" + "\"disabled\" does not send keyboard inputs to the device.\n" + "\"sdk\" uses the Android system API to deliver keyboard " + "events to applications.\n" + "\"uhid\" simulates a physical HID keyboard using the Linux " + "UHID kernel module on the device.\n" + "\"aoa\" simulates a physical keyboard using the AOAv2 " + "protocol. It may only work over USB.\n" + "For \"uhid\" and \"aoa\", the keyboard layout must be " + "configured (once and for all) on the device, via Settings -> " + "System -> Languages and input -> Physical keyboard. This " + "settings page can be started directly using the shortcut " + "MOD+k (except in OTG mode) or by executing: `adb shell am " + "start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n" + "This option is only available when a HID keyboard is enabled " + "(or a physical keyboard is connected).\n" + "Also see --mouse.", + }, + { + .longopt_id = OPT_KILL_ADB_ON_CLOSE, + .longopt = "kill-adb-on-close", + .text = "Kill adb when scrcpy terminates.", + }, + { + // deprecated + //.shortopt = 'K', // old, reassigned + .longopt_id = OPT_HID_KEYBOARD_DEPRECATED, + .longopt = "hid-keyboard", + }, + { + .longopt_id = OPT_LEGACY_PASTE, + .longopt = "legacy-paste", + .text = "Inject computer clipboard text as a sequence of key events " + "on Ctrl+v (like MOD+Shift+v).\n" + "This is a workaround for some devices not behaving as " + "expected when setting the device clipboard programmatically.", + }, + { + .longopt_id = OPT_LIST_CAMERAS, + .longopt = "list-cameras", + .text = "List device cameras.", + }, + { + .longopt_id = OPT_LIST_CAMERA_SIZES, + .longopt = "list-camera-sizes", + .text = "List the valid camera capture sizes.", + }, + { + .longopt_id = OPT_LIST_DISPLAYS, + .longopt = "list-displays", + .text = "List device displays.", + }, + { + .longopt_id = OPT_LIST_ENCODERS, + .longopt = "list-encoders", + .text = "List video and audio encoders available on the device.", + }, + { + .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, + .longopt = "lock-video-orientation", + .argdesc = "value", + .optional_arg = true, + .text = "Lock capture video orientation to value.\n" + "Possible values are \"unlocked\", \"initial\" (locked to the " + "initial orientation), 0, 90, 180 and 270. The values " + "represent the clockwise rotation from the natural device " + "orientation, in degrees.\n" + "Default is \"unlocked\".\n" + "Passing the option without argument is equivalent to passing " + "\"initial\".", + }, + { + .shortopt = 'm', + .longopt = "max-size", + .argdesc = "value", + .text = "Limit both the width and height of the video to value. The " + "other dimension is computed so that the device aspect-ratio " + "is preserved.\n" + "Default is 0 (unlimited).", + }, + { + // deprecated + //.shortopt = 'M', // old, reassigned + .longopt_id = OPT_HID_MOUSE_DEPRECATED, + .longopt = "hid-mouse", + }, + { + .shortopt = 'M', + .text = "Same as --mouse=uhid.", + }, + { + .longopt_id = OPT_MAX_FPS, + .longopt = "max-fps", + .argdesc = "value", + .text = "Limit the frame rate of screen capture (officially supported " + "since Android 10, but may work on earlier versions).", + }, + { + .longopt_id = OPT_MOUSE, + .longopt = "mouse", + .argdesc = "mode", + .text = "Select how to send mouse inputs to the device.\n" + "Possible values are \"disabled\", \"sdk\", \"uhid\" and " + "\"aoa\".\n" + "\"disabled\" does not send mouse inputs to the device.\n" + "\"sdk\" uses the Android system API to deliver mouse events" + "to applications.\n" + "\"uhid\" simulates a physical HID mouse using the Linux UHID " + "kernel module on the device.\n" + "\"aoa\" simulates a physical mouse using the AOAv2 protocol. " + "It may only work over USB.\n" + "In \"uhid\" and \"aoa\" modes, the computer mouse is captured " + "to control the device directly (relative mouse mode).\n" + "LAlt, LSuper or RSuper toggle the capture mode, to give " + "control of the mouse back to the computer.\n" + "Also see --keyboard.", + }, + { + .shortopt = 'n', + .longopt = "no-control", + .text = "Disable device control (mirror the device in read-only).", + }, + { + .shortopt = 'N', + .longopt = "no-playback", + .text = "Disable video and audio playback on the computer (equivalent " + "to --no-video-playback --no-audio-playback).", + }, + { + .longopt_id = OPT_NO_AUDIO, + .longopt = "no-audio", + .text = "Disable audio forwarding.", + }, + { + .longopt_id = OPT_NO_AUDIO_PLAYBACK, + .longopt = "no-audio-playback", + .text = "Disable audio playback on the computer.", + }, + { + .longopt_id = OPT_NO_CLEANUP, + .longopt = "no-cleanup", + .text = "By default, scrcpy removes the server binary from the device " + "and restores the device state (show touches, stay awake and " + "power mode) on exit.\n" + "This option disables this cleanup." + }, + { + .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, + .longopt = "no-clipboard-autosync", + .text = "By default, scrcpy automatically synchronizes the computer " + "clipboard to the device clipboard before injecting Ctrl+v, " + "and the device clipboard to the computer clipboard whenever " + "it changes.\n" + "This option disables this automatic synchronization." + }, + { + .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, + .longopt = "no-downsize-on-error", + .text = "By default, on MediaCodec error, scrcpy automatically tries " + "again with a lower definition.\n" + "This option disables this behavior.", + }, + { + // deprecated + .longopt_id = OPT_NO_DISPLAY, + .longopt = "no-display", + }, + { + .longopt_id = OPT_NO_KEY_REPEAT, + .longopt = "no-key-repeat", + .text = "Do not forward repeated key events when a key is held down.", + }, + { + .longopt_id = OPT_NO_MIPMAPS, + .longopt = "no-mipmaps", + .text = "If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then " + "mipmaps are automatically generated to improve downscaling " + "quality. This option disables the generation of mipmaps.", + }, + { + .longopt_id = OPT_NO_POWER_ON, + .longopt = "no-power-on", + .text = "Do not power on the device on start.", + }, + { + .longopt_id = OPT_NO_VIDEO, + .longopt = "no-video", + .text = "Disable video forwarding.", + }, + { + .longopt_id = OPT_NO_VIDEO_PLAYBACK, + .longopt = "no-video-playback", + .text = "Disable video playback on the computer.", + }, + { + .longopt_id = OPT_ORIENTATION, + .longopt = "orientation", + .argdesc = "value", + .text = "Same as --display-orientation=value " + "--record-orientation=value.", + }, + { + .longopt_id = OPT_OTG, + .longopt = "otg", + .text = "Run in OTG mode: simulate physical keyboard and mouse, " + "as if the computer keyboard and mouse were plugged directly " + "to the device via an OTG cable.\n" + "In this mode, adb (USB debugging) is not necessary, and " + "mirroring is disabled.\n" + "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " + "control of the mouse back to the computer.\n" + "Keyboard and mouse may be disabled separately using" + "--keyboard=disabled and --mouse=disabled.\n" + "It may only work over USB.\n" + "See --keyboard and --mouse.", + }, + { + .shortopt = 'p', + .longopt = "port", + .argdesc = "port[:port]", + .text = "Set the TCP port (range) used by the client to listen.\n" + "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" + STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", + }, + { + .longopt_id = OPT_PAUSE_ON_EXIT, + .longopt = "pause-on-exit", + .argdesc = "mode", + .optional_arg = true, + .text = "Configure pause on exit. Possible values are \"true\" (always " + "pause on exit), \"false\" (never pause on exit) and " + "\"if-error\" (pause only if an error occured).\n" + "This is useful to prevent the terminal window from " + "automatically closing, so that error messages can be read.\n" + "Default is \"false\".\n" + "Passing the option without argument is equivalent to passing " + "\"true\".", + }, + { + .longopt_id = OPT_POWER_OFF_ON_CLOSE, + .longopt = "power-off-on-close", + .text = "Turn the device screen off when closing scrcpy.", + }, + { + .longopt_id = OPT_PREFER_TEXT, + .longopt = "prefer-text", + .text = "Inject alpha characters and space as text events instead of " + "key events.\n" + "This avoids issues when combining multiple keys to enter a " + "special character, but breaks the expected behavior of alpha " + "keys in games (typically WASD).", + }, + { + .longopt_id = OPT_PRINT_FPS, + .longopt = "print-fps", + .text = "Start FPS counter, to print framerate logs to the console. " + "It can be started or stopped at any time with MOD+i.", + }, + { + .longopt_id = OPT_PUSH_TARGET, + .longopt = "push-target", + .argdesc = "path", + .text = "Set the target directory for pushing files to the device by " + "drag & drop. It is passed as is to \"adb push\".\n" + "Default is \"/sdcard/Download/\".", + }, + { + .shortopt = 'r', + .longopt = "record", + .argdesc = "file.mp4", + .text = "Record screen to file.\n" + "The format is determined by the --record-format option if " + "set, or by the file extension.", + }, + { + .longopt_id = OPT_RAW_KEY_EVENTS, + .longopt = "raw-key-events", + .text = "Inject key events for all input keys, and ignore text events." + }, + { + .longopt_id = OPT_RECORD_FORMAT, + .longopt = "record-format", + .argdesc = "format", + .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " + "or wav).", + }, + { + .longopt_id = OPT_RECORD_ORIENTATION, + .longopt = "record-orientation", + .argdesc = "value", + .text = "Set the record orientation.\n" + "Possible values are 0, 90, 180 and 270. The number represents " + "the clockwise rotation in degrees.\n" + "Default is 0.", + }, + { + .longopt_id = OPT_RENDER_DRIVER, + .longopt = "render-driver", + .argdesc = "name", + .text = "Request SDL to use the given render driver (this is just a " + "hint).\n" + "Supported names are currently \"direct3d\", \"opengl\", " + "\"opengles2\", \"opengles\", \"metal\" and \"software\".\n" + "", + }, + { + .longopt_id = OPT_REQUIRE_AUDIO, + .longopt = "require-audio", + .text = "By default, scrcpy mirrors only the video when audio capture " + "fails on the device. This option makes scrcpy fail if audio " + "is enabled but does not work." + }, + { + // deprecated + .longopt_id = OPT_ROTATION, + .longopt = "rotation", + .argdesc = "value", + }, + { + .shortopt = 's', + .longopt = "serial", + .argdesc = "serial", + .text = "The device serial number. Mandatory only if several devices " + "are connected to adb.", + }, + { + .shortopt = 'S', + .longopt = "turn-screen-off", + .text = "Turn the device screen off immediately.", + }, + { + .longopt_id = OPT_SHORTCUT_MOD, + .longopt = "shortcut-mod", + .argdesc = "key[+...][,...]", + .text = "Specify the modifiers to use for scrcpy shortcuts.\n" + "Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", " + "\"lsuper\" and \"rsuper\".\n" + "A shortcut can consist in several keys, separated by '+'. " + "Several shortcuts can be specified, separated by ','.\n" + "For example, to use either LCtrl+LAlt or LSuper for scrcpy " + "shortcuts, pass \"lctrl+lalt,lsuper\".\n" + "Default is \"lalt,lsuper\" (left-Alt or left-Super).", + }, + { + .shortopt = 't', + .longopt = "show-touches", + .text = "Enable \"show touches\" on start, restore the initial value " + "on exit.\n" + "It only shows physical touches (not clicks from scrcpy).", + }, + { + .longopt_id = OPT_TCPIP, + .longopt = "tcpip", + .argdesc = "ip[:port]", + .optional_arg = true, + .text = "Configure and reconnect the device over TCP/IP.\n" + "If a destination address is provided, then scrcpy connects to " + "this address before starting. The device must listen on the " + "given TCP port (default is 5555).\n" + "If no destination address is provided, then scrcpy attempts " + "to find the IP address of the current device (typically " + "connected over USB), enables TCP/IP mode, then connects to " + "this address before starting.", + }, + { + .longopt_id = OPT_TIME_LIMIT, + .longopt = "time-limit", + .argdesc = "seconds", + .text = "Set the maximum mirroring time, in seconds.", + }, + { + .longopt_id = OPT_TUNNEL_HOST, + .longopt = "tunnel-host", + .argdesc = "ip", + .text = "Set the IP address of the adb tunnel to reach the scrcpy " + "server. This option automatically enables " + "--force-adb-forward.\n" + "Default is localhost.", + }, + { + .longopt_id = OPT_TUNNEL_PORT, + .longopt = "tunnel-port", + .argdesc = "port", + .text = "Set the TCP port of the adb tunnel to reach the scrcpy " + "server. This option automatically enables " + "--force-adb-forward.\n" + "Default is 0 (not forced): the local port used for " + "establishing the tunnel will be used.", + }, + { + .shortopt = 'v', + .longopt = "version", + .text = "Print the version of scrcpy.", + }, + { + .shortopt = 'V', + .longopt = "verbosity", + .argdesc = "value", + .text = "Set the log level (verbose, debug, info, warn or error).\n" #ifndef NDEBUG - " Default is debug.\n" + "Default is debug.", #else - " Default is info.\n" + "Default is info.", #endif - "\n" - " -v, --version\n" - " Print the version of scrcpy.\n" - "\n" - " -w, --stay-awake\n" - " Keep the device on while scrcpy is running, when the device\n" - " is plugged in.\n" - "\n" - " --window-borderless\n" - " Disable window decorations (display borderless window).\n" - "\n" - " --window-title text\n" - " Set a custom window title.\n" - "\n" - " --window-x value\n" - " Set the initial window horizontal position.\n" - " Default is \"auto\".\n" - "\n" - " --window-y value\n" - " Set the initial window vertical position.\n" - " Default is \"auto\".\n" - "\n" - " --window-width value\n" - " Set the initial window width.\n" - " Default is 0 (automatic).\n" - "\n" - " --window-height value\n" - " Set the initial window height.\n" - " Default is 0 (automatic).\n" - "\n" - "Shortcuts:\n" - "\n" - " In the following list, MOD is the shortcut modifier. By default,\n" - " it's (left) Alt or (left) Super, but it can be configured by\n" - " --shortcut-mod (see above).\n" - "\n" - " MOD+f\n" - " Switch fullscreen mode\n" - "\n" - " MOD+Left\n" - " Rotate display left\n" - "\n" - " MOD+Right\n" - " Rotate display right\n" - "\n" - " MOD+g\n" - " Resize window to 1:1 (pixel-perfect)\n" - "\n" - " MOD+w\n" - " Double-click on black borders\n" - " Resize window to remove black borders\n" - "\n" - " MOD+h\n" - " Middle-click\n" - " Click on HOME\n" - "\n" - " MOD+b\n" - " MOD+Backspace\n" - " Right-click (when screen is on)\n" - " Click on BACK\n" - "\n" - " MOD+s\n" - " Click on APP_SWITCH\n" - "\n" - " MOD+m\n" - " Click on MENU\n" - "\n" - " MOD+Up\n" - " Click on VOLUME_UP\n" - "\n" - " MOD+Down\n" - " Click on VOLUME_DOWN\n" - "\n" - " MOD+p\n" - " Click on POWER (turn screen on/off)\n" - "\n" - " Right-click (when screen is off)\n" - " Power on\n" - "\n" - " MOD+o\n" - " Turn device screen off (keep mirroring)\n" - "\n" - " MOD+Shift+o\n" - " Turn device screen on\n" - "\n" - " MOD+r\n" - " Rotate device screen\n" - "\n" - " MOD+n\n" - " Expand notification panel\n" - "\n" - " MOD+Shift+n\n" - " Collapse notification panel\n" - "\n" - " MOD+c\n" - " Copy to clipboard (inject COPY keycode, Android >= 7 only)\n" - "\n" - " MOD+x\n" - " Cut to clipboard (inject CUT keycode, Android >= 7 only)\n" - "\n" - " MOD+v\n" - " Copy computer clipboard to device, then paste (inject PASTE\n" - " keycode, Android >= 7 only)\n" - "\n" - " MOD+Shift+v\n" - " Inject computer clipboard text as a sequence of key events\n" - "\n" - " MOD+i\n" - " Enable/disable FPS counter (print frames/second in logs)\n" - "\n" - " Ctrl+click-and-move\n" - " Pinch-to-zoom from the center of the screen\n" - "\n" - " Drag & drop APK file\n" - " Install APK from computer\n" - "\n", arg0); + }, + { + .longopt_id = OPT_V4L2_SINK, + .longopt = "v4l2-sink", + .argdesc = "/dev/videoN", + .text = "Output to v4l2loopback device.\n" + "It requires to lock the video orientation (see " + "--lock-video-orientation).\n" + "This feature is only available on Linux.", + }, + { + .longopt_id = OPT_V4L2_BUFFER, + .longopt = "v4l2-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before pushing " + "frames. This increases latency to compensate for jitter.\n" + "This option is similar to --display-buffer, but specific to " + "V4L2 sink.\n" + "Default is 0 (no buffering).\n" + "This option is only available on Linux.", + }, + { + .longopt_id = OPT_VIDEO_CODEC, + .longopt = "video-codec", + .argdesc = "name", + .text = "Select a video codec (h264, h265 or av1).\n" + "Default is h264.", + }, + { + .longopt_id = OPT_VIDEO_CODEC_OPTIONS, + .longopt = "video-codec-options", + .argdesc = "key[:type]=value[,...]", + .text = "Set a list of comma-separated key:type=value options for the " + "device video encoder.\n" + "The possible values for 'type' are 'int' (default), 'long', " + "'float' and 'string'.\n" + "The list of possible codec options is available in the " + "Android documentation: " + "", + }, + { + .longopt_id = OPT_VIDEO_ENCODER, + .longopt = "video-encoder", + .argdesc = "name", + .text = "Use a specific MediaCodec video encoder (depending on the " + "codec provided by --video-codec).\n" + "The available encoders can be listed by --list-encoders.", + }, + { + .longopt_id = OPT_VIDEO_SOURCE, + .longopt = "video-source", + .argdesc = "source", + .text = "Select the video source (display or camera).\n" + "Camera mirroring requires Android 12+.\n" + "Default is display.", + }, + { + .shortopt = 'w', + .longopt = "stay-awake", + .text = "Keep the device on while scrcpy is running, when the device " + "is plugged in.", + }, + { + .longopt_id = OPT_WINDOW_BORDERLESS, + .longopt = "window-borderless", + .text = "Disable window decorations (display borderless window)." + }, + { + .longopt_id = OPT_WINDOW_TITLE, + .longopt = "window-title", + .argdesc = "text", + .text = "Set a custom window title.", + }, + { + .longopt_id = OPT_WINDOW_X, + .longopt = "window-x", + .argdesc = "value", + .text = "Set the initial window horizontal position.\n" + "Default is \"auto\".", + }, + { + .longopt_id = OPT_WINDOW_Y, + .longopt = "window-y", + .argdesc = "value", + .text = "Set the initial window vertical position.\n" + "Default is \"auto\".", + }, + { + .longopt_id = OPT_WINDOW_WIDTH, + .longopt = "window-width", + .argdesc = "value", + .text = "Set the initial window width.\n" + "Default is 0 (automatic).", + }, + { + .longopt_id = OPT_WINDOW_HEIGHT, + .longopt = "window-height", + .argdesc = "value", + .text = "Set the initial window height.\n" + "Default is 0 (automatic).", + }, +}; + +static const struct sc_shortcut shortcuts[] = { + { + .shortcuts = { "MOD+f" }, + .text = "Switch fullscreen mode", + }, + { + .shortcuts = { "MOD+Left" }, + .text = "Rotate display left", + }, + { + .shortcuts = { "MOD+Right" }, + .text = "Rotate display right", + }, + { + .shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" }, + .text = "Flip display horizontally", + }, + { + .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, + .text = "Flip display vertically", + }, + { + .shortcuts = { "MOD+g" }, + .text = "Resize window to 1:1 (pixel-perfect)", + }, + { + .shortcuts = { "MOD+w", "Double-click on black borders" }, + .text = "Resize window to remove black borders", + }, + { + .shortcuts = { "MOD+h", "Middle-click" }, + .text = "Click on HOME", + }, + { + .shortcuts = { + "MOD+b", + "MOD+Backspace", + "Right-click (when screen is on)", + }, + .text = "Click on BACK", + }, + { + .shortcuts = { "MOD+s", "4th-click" }, + .text = "Click on APP_SWITCH", + }, + { + .shortcuts = { "MOD+m" }, + .text = "Click on MENU", + }, + { + .shortcuts = { "MOD+Up" }, + .text = "Click on VOLUME_UP", + }, + { + .shortcuts = { "MOD+Down" }, + .text = "Click on VOLUME_DOWN", + }, + { + .shortcuts = { "MOD+p" }, + .text = "Click on POWER (turn screen on/off)", + }, + { + .shortcuts = { "Right-click (when screen is off)" }, + .text = "Power on", + }, + { + .shortcuts = { "MOD+o" }, + .text = "Turn device screen off (keep mirroring)", + }, + { + .shortcuts = { "MOD+Shift+o" }, + .text = "Turn device screen on", + }, + { + .shortcuts = { "MOD+r" }, + .text = "Rotate device screen", + }, + { + .shortcuts = { "MOD+n", "5th-click" }, + .text = "Expand notification panel", + }, + { + .shortcuts = { "MOD+Shift+n" }, + .text = "Collapse notification panel", + }, + { + .shortcuts = { "MOD+c" }, + .text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)", + }, + { + .shortcuts = { "MOD+x" }, + .text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)", + }, + { + .shortcuts = { "MOD+v" }, + .text = "Copy computer clipboard to device, then paste (inject PASTE " + "keycode, Android >= 7 only)", + }, + { + .shortcuts = { "MOD+Shift+v" }, + .text = "Inject computer clipboard text as a sequence of key events", + }, + { + .shortcuts = { "MOD+k" }, + .text = "Open keyboard settings on the device (for HID keyboard only)", + }, + { + .shortcuts = { "MOD+i" }, + .text = "Enable/disable FPS counter (print frames/second in logs)", + }, + { + .shortcuts = { "Ctrl+click-and-move" }, + .text = "Pinch-to-zoom and rotate from the center of the screen", + }, + { + .shortcuts = { "Shift+click-and-move" }, + .text = "Tilt (slide vertically with two fingers)", + }, + { + .shortcuts = { "Drag & drop APK file" }, + .text = "Install APK from computer", + }, + { + .shortcuts = { "Drag & drop non-APK file" }, + .text = "Push file to device (see --push-target)", + }, +}; + +static const struct sc_envvar envvars[] = { + { + .name = "ADB", + .text = "Path to adb executable", + }, + { + .name = "ANDROID_SERIAL", + .text = "Device serial to use if no selector (-s, -d, -e or " + "--tcpip=) is specified", + }, + { + .name = "SCRCPY_ICON_PATH", + .text = "Path to the program icon", + }, + { + .name = "SCRCPY_SERVER_PATH", + .text = "Path to the server binary", + }, +}; + +static const struct sc_exit_status exit_statuses[] = { + { + .value = 0, + .text = "Normal program termination", + }, + { + .value = 1, + .text = "Start failure", + }, + { + .value = 2, + .text = "Device disconnected while running", + }, +}; + +static char * +sc_getopt_adapter_create_optstring(void) { + struct sc_strbuf buf; + if (!sc_strbuf_init(&buf, 64)) { + return false; + } + + for (size_t i = 0; i < ARRAY_LEN(options); ++i) { + const struct sc_option *opt = &options[i]; + if (opt->shortopt) { + if (!sc_strbuf_append_char(&buf, opt->shortopt)) { + goto error; + } + // If there is an argument, add ':' + if (opt->argdesc) { + if (!sc_strbuf_append_char(&buf, ':')) { + goto error; + } + // If the argument is optional, add another ':' + if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) { + goto error; + } + } + } + } + + return buf.s; + +error: + free(buf.s); + return NULL; +} + +static struct option * +sc_getopt_adapter_create_longopts(void) { + struct option *longopts = + malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts)); + if (!longopts) { + LOG_OOM(); + return NULL; + } + + size_t out_idx = 0; + for (size_t i = 0; i < ARRAY_LEN(options); ++i) { + const struct sc_option *in = &options[i]; + + // If longopt_id is set, then longopt must be set + assert(!in->longopt_id || in->longopt); + + if (!in->longopt) { + // The longopts array must only contain long options + continue; + } + struct option *out = &longopts[out_idx++]; + + out->name = in->longopt; + + if (!in->argdesc) { + assert(!in->optional_arg); + out->has_arg = no_argument; + } else if (in->optional_arg) { + out->has_arg = optional_argument; + } else { + out->has_arg = required_argument; + } + + out->flag = NULL; + + // Either shortopt or longopt_id is set, but not both + assert(!!in->shortopt ^ !!in->longopt_id); + out->val = in->shortopt ? in->shortopt : in->longopt_id; + } + + // The array must be terminated by a NULL item + longopts[out_idx] = (struct option) {0}; + + return longopts; +} + +static bool +sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) { + adapter->optstring = sc_getopt_adapter_create_optstring(); + if (!adapter->optstring) { + return false; + } + + adapter->longopts = sc_getopt_adapter_create_longopts(); + if (!adapter->longopts) { + free(adapter->optstring); + return false; + } + + return true; +} + +static void +sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) { + free(adapter->optstring); + free(adapter->longopts); +} + +static void +print_option_usage_header(const struct sc_option *opt) { + struct sc_strbuf buf; + if (!sc_strbuf_init(&buf, 64)) { + goto error; + } + + bool ok = true; + (void) ok; // only used for assertions + + if (opt->shortopt) { + ok = sc_strbuf_append_char(&buf, '-'); + assert(ok); + + ok = sc_strbuf_append_char(&buf, opt->shortopt); + assert(ok); + + if (opt->longopt) { + ok = sc_strbuf_append_staticstr(&buf, ", "); + assert(ok); + } + } + + if (opt->longopt) { + ok = sc_strbuf_append_staticstr(&buf, "--"); + assert(ok); + + if (!sc_strbuf_append_str(&buf, opt->longopt)) { + goto error; + } + } + + if (opt->argdesc) { + if (opt->optional_arg && !sc_strbuf_append_char(&buf, '[')) { + goto error; + } + + if (!sc_strbuf_append_char(&buf, '=')) { + goto error; + } + + if (!sc_strbuf_append_str(&buf, opt->argdesc)) { + goto error; + } + + if (opt->optional_arg && !sc_strbuf_append_char(&buf, ']')) { + goto error; + } + } + + printf("\n %s\n", buf.s); + free(buf.s); + return; + +error: + printf("\n"); +} + +static void +print_option_usage(const struct sc_option *opt, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + + if (!opt->text) { + // Option not documented in help (for example because it is deprecated) + return; + } + + print_option_usage_header(opt); + + char *text = sc_str_wrap_lines(opt->text, cols, 8); + if (!text) { + printf("\n"); + return; + } + + printf("%s\n", text); + free(text); +} + +static void +print_shortcuts_intro(unsigned cols) { + char *intro = sc_str_wrap_lines( + "In the following list, MOD is the shortcut modifier. By default, it's " + "(left) Alt or (left) Super, but it can be configured by " + "--shortcut-mod (see above).", cols, 4); + if (!intro) { + printf("\n"); + return; + } + + printf("\n%s\n", intro); + free(intro); +} + +static void +print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(shortcut->shortcuts[0]); // At least one shortcut + assert(shortcut->text); + + printf("\n"); + + unsigned i = 0; + while (shortcut->shortcuts[i]) { + printf(" %s\n", shortcut->shortcuts[i]); + ++i; + } + + char *text = sc_str_wrap_lines(shortcut->text, cols, 8); + if (!text) { + printf("\n"); + return; + } + + printf("%s\n", text); + free(text); +} + +static void +print_envvar(const struct sc_envvar *envvar, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(envvar->name); + assert(envvar->text); + + printf("\n %s\n", envvar->name); + char *text = sc_str_wrap_lines(envvar->text, cols, 8); + if (!text) { + printf("\n"); + return; + } + + printf("%s\n", text); + free(text); +} + +static void +print_exit_status(const struct sc_exit_status *status, unsigned cols) { + assert(cols > 8); // sc_str_wrap_lines() requires indent < columns + assert(status->text); + + // The text starts at 9: 4 ident spaces, 3 chars for numeric value, 2 spaces + char *text = sc_str_wrap_lines(status->text, cols, 9); + if (!text) { + printf("\n"); + return; + } + + assert(strlen(text) >= 9); // Contains at least the initial identation + + // text + 9 to remove the initial indentation + printf(" %3d %s\n", status->value, text + 9); + free(text); +} + +void +scrcpy_print_usage(const char *arg0) { +#define SC_TERM_COLS_DEFAULT 80 + unsigned cols; + + if (!isatty(STDERR_FILENO)) { + // Not a tty + cols = SC_TERM_COLS_DEFAULT; + } else { + bool ok = sc_term_get_size(NULL, &cols); + if (!ok) { + // Could not get the terminal size + cols = SC_TERM_COLS_DEFAULT; + } + if (cols < 20) { + // Do not accept a too small value + cols = 20; + } + } + + printf("Usage: %s [options]\n\n" + "Options:\n", arg0); + for (size_t i = 0; i < ARRAY_LEN(options); ++i) { + print_option_usage(&options[i], cols); + } + + // Print shortcuts section + printf("\nShortcuts:\n"); + print_shortcuts_intro(cols); + for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { + print_shortcut(&shortcuts[i], cols); + } + + // Print environment variables section + printf("\nEnvironment variables:\n"); + for (size_t i = 0; i < ARRAY_LEN(envvars); ++i) { + print_envvar(&envvars[i], cols); + } + + printf("\nExit status:\n\n"); + for (size_t i = 0; i < ARRAY_LEN(exit_statuses); ++i) { + print_exit_status(&exit_statuses[i], cols); + } } static bool @@ -329,9 +1348,9 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, long value; bool ok; if (accept_suffix) { - ok = parse_integer_with_suffix(s, &value); + ok = sc_str_parse_integer_with_suffix(s, &value); } else { - ok = parse_integer(s, &value); + ok = sc_str_parse_integer(s, &value); } if (!ok) { LOGE("Could not parse %s: %s", name, s); @@ -349,9 +1368,9 @@ parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, } static size_t -parse_integers_arg(const char *s, size_t max_items, long *out, long min, - long max, const char *name) { - size_t count = parse_integers(s, ':', max_items, out); +parse_integers_arg(const char *s, const char sep, size_t max_items, long *out, + long min, long max, const char *name) { + size_t count = sc_str_parse_integers(s, sep, max_items, out); if (!count) { LOGE("Could not parse %s: %s", name, s); return 0; @@ -398,7 +1417,7 @@ parse_max_size(const char *s, uint16_t *max_size) { static bool parse_max_fps(const char *s, uint16_t *max_fps) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 1000, "max fps"); + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps"); if (!ok) { return false; } @@ -410,7 +1429,11 @@ parse_max_fps(const char *s, uint16_t *max_fps) { static bool parse_buffering_time(const char *s, sc_tick *tick) { long value; - bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, + // In practice, buffering time should not exceed a few seconds. + // Limit it to some arbitrary value (1 hour) to prevent 32-bit overflow + // when multiplied by the audio sample size and the number of samples per + // millisecond. + bool ok = parse_integer_arg(s, &value, false, 0, 60 * 60 * 1000, "buffering time"); if (!ok) { return false; @@ -420,6 +1443,19 @@ parse_buffering_time(const char *s, sc_tick *tick) { return true; } +static bool +parse_audio_output_buffer(const char *s, sc_tick *tick) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 1000, + "audio output buffer"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_MS(value); + return true; +} + static bool parse_lock_video_orientation(const char *s, enum sc_lock_video_orientation *lock_mode) { @@ -434,15 +1470,50 @@ parse_lock_video_orientation(const char *s, return true; } - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 3, - "lock video orientation"); - if (!ok) { - return false; + if (!strcmp(s, "0")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; + return true; } - *lock_mode = (enum sc_lock_video_orientation) value; - return true; + if (!strcmp(s, "90")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + if (!strcmp(s, "180")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "270")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "1")) { + LOGW("--lock-video-orientation=1 is deprecated, use " + "--lock-video-orientation=270 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; + return true; + } + + if (!strcmp(s, "2")) { + LOGW("--lock-video-orientation=2 is deprecated, use " + "--lock-video-orientation=180 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; + return true; + } + + if (!strcmp(s, "3")) { + LOGW("--lock-video-orientation=3 is deprecated, use " + "--lock-video-orientation=90 instead."); + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; + return true; + } + + LOGE("Unsupported --lock-video-orientation value: %s (expected initial, " + "unlocked, 0, 90, 180 or 270).", s); + return false; } static bool @@ -457,6 +1528,45 @@ parse_rotation(const char *s, uint8_t *rotation) { return true; } +static bool +parse_orientation(const char *s, enum sc_orientation *orientation) { + if (!strcmp(s, "0")) { + *orientation = SC_ORIENTATION_0; + return true; + } + if (!strcmp(s, "90")) { + *orientation = SC_ORIENTATION_90; + return true; + } + if (!strcmp(s, "180")) { + *orientation = SC_ORIENTATION_180; + return true; + } + if (!strcmp(s, "270")) { + *orientation = SC_ORIENTATION_270; + return true; + } + if (!strcmp(s, "flip0")) { + *orientation = SC_ORIENTATION_FLIP_0; + return true; + } + if (!strcmp(s, "flip90")) { + *orientation = SC_ORIENTATION_FLIP_90; + return true; + } + if (!strcmp(s, "flip180")) { + *orientation = SC_ORIENTATION_FLIP_180; + return true; + } + if (!strcmp(s, "flip270")) { + *orientation = SC_ORIENTATION_FLIP_270; + return true; + } + LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, " + "flip90, flip180 or flip270)", optarg); + return false; +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -494,7 +1604,7 @@ parse_window_dimension(const char *s, uint16_t *dimension) { static bool 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"); + size_t count = parse_integers_arg(s, ':', 2, values, 0, 0xFFFF, "port"); if (!count) { return false; } @@ -563,7 +1673,7 @@ parse_log_level(const char *s, enum sc_log_level *log_level) { } // 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) +// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error) static unsigned parse_shortcut_mods_item(const char *item, size_t len) { unsigned mod = 0; @@ -581,17 +1691,17 @@ parse_shortcut_mods_item(const char *item, size_t len) { ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) if (STREQ("lctrl", item, key_len)) { - mod |= SC_MOD_LCTRL; + mod |= SC_SHORTCUT_MOD_LCTRL; } else if (STREQ("rctrl", item, key_len)) { - mod |= SC_MOD_RCTRL; + mod |= SC_SHORTCUT_MOD_RCTRL; } else if (STREQ("lalt", item, key_len)) { - mod |= SC_MOD_LALT; + mod |= SC_SHORTCUT_MOD_LALT; } else if (STREQ("ralt", item, key_len)) { - mod |= SC_MOD_RALT; + mod |= SC_SHORTCUT_MOD_RALT; } else if (STREQ("lsuper", item, key_len)) { - mod |= SC_MOD_LSUPER; + mod |= SC_SHORTCUT_MOD_LSUPER; } else if (STREQ("rsuper", item, key_len)) { - mod |= SC_MOD_RSUPER; + mod |= SC_SHORTCUT_MOD_RSUPER; } else { LOGE("Unknown modifier key: %.*s " "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", @@ -659,164 +1769,349 @@ sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { } #endif +static enum sc_record_format +get_record_format(const char *name) { + if (!strcmp(name, "mp4")) { + return SC_RECORD_FORMAT_MP4; + } + if (!strcmp(name, "mkv")) { + return SC_RECORD_FORMAT_MKV; + } + if (!strcmp(name, "m4a")) { + return SC_RECORD_FORMAT_M4A; + } + if (!strcmp(name, "mka")) { + return SC_RECORD_FORMAT_MKA; + } + if (!strcmp(name, "opus")) { + return SC_RECORD_FORMAT_OPUS; + } + if (!strcmp(name, "aac")) { + return SC_RECORD_FORMAT_AAC; + } + if (!strcmp(name, "flac")) { + return SC_RECORD_FORMAT_FLAC; + } + if (!strcmp(name, "wav")) { + return SC_RECORD_FORMAT_WAV; + } + return 0; +} + static bool parse_record_format(const char *optarg, enum sc_record_format *format) { - if (!strcmp(optarg, "mp4")) { - *format = SC_RECORD_FORMAT_MP4; - return true; + enum sc_record_format fmt = get_record_format(optarg); + if (!fmt) { + LOGE("Unsupported record format: %s (expected mp4, mkv, m4a, mka, " + "opus, aac, flac or wav)", optarg); + return false; } - if (!strcmp(optarg, "mkv")) { - *format = SC_RECORD_FORMAT_MKV; - return true; + + *format = fmt; + return true; +} + +static bool +parse_ip(const char *optarg, uint32_t *ipv4) { + return net_parse_ipv4(optarg, ipv4); +} + +static bool +parse_port(const char *optarg, uint16_t *port) { + long value; + if (!parse_integer_arg(optarg, &value, false, 0, 0xFFFF, "port")) { + return false; } - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); - return false; + *port = (uint16_t) value; + return true; } static enum sc_record_format guess_record_format(const char *filename) { - size_t len = strlen(filename); - if (len < 4) { + const char *dot = strrchr(filename, '.'); + if (!dot) { return 0; } - const char *ext = &filename[len - 4]; - if (!strcmp(ext, ".mp4")) { - return SC_RECORD_FORMAT_MP4; + + const char *ext = dot + 1; + return get_record_format(ext); +} + +static bool +parse_video_codec(const char *optarg, enum sc_codec *codec) { + if (!strcmp(optarg, "h264")) { + *codec = SC_CODEC_H264; + return true; } - if (!strcmp(ext, ".mkv")) { - return SC_RECORD_FORMAT_MKV; + if (!strcmp(optarg, "h265")) { + *codec = SC_CODEC_H265; + return true; } - return 0; + if (!strcmp(optarg, "av1")) { + *codec = SC_CODEC_AV1; + return true; + } + LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg); + return false; } -#define OPT_RENDER_EXPIRED_FRAMES 1000 -#define OPT_WINDOW_TITLE 1001 -#define OPT_PUSH_TARGET 1002 -#define OPT_ALWAYS_ON_TOP 1003 -#define OPT_CROP 1004 -#define OPT_RECORD_FORMAT 1005 -#define OPT_PREFER_TEXT 1006 -#define OPT_WINDOW_X 1007 -#define OPT_WINDOW_Y 1008 -#define OPT_WINDOW_WIDTH 1009 -#define OPT_WINDOW_HEIGHT 1010 -#define OPT_WINDOW_BORDERLESS 1011 -#define OPT_MAX_FPS 1012 -#define OPT_LOCK_VIDEO_ORIENTATION 1013 -#define OPT_DISPLAY_ID 1014 -#define OPT_ROTATION 1015 -#define OPT_RENDER_DRIVER 1016 -#define OPT_NO_MIPMAPS 1017 -#define OPT_CODEC_OPTIONS 1018 -#define OPT_FORCE_ADB_FORWARD 1019 -#define OPT_DISABLE_SCREENSAVER 1020 -#define OPT_SHORTCUT_MOD 1021 -#define OPT_NO_KEY_REPEAT 1022 -#define OPT_FORWARD_ALL_CLICKS 1023 -#define OPT_LEGACY_PASTE 1024 -#define OPT_ENCODER_NAME 1025 -#define OPT_POWER_OFF_ON_CLOSE 1026 -#define OPT_V4L2_SINK 1027 -#define OPT_DISPLAY_BUFFER 1028 -#define OPT_V4L2_BUFFER 1029 +static bool +parse_audio_codec(const char *optarg, enum sc_codec *codec) { + if (!strcmp(optarg, "opus")) { + *codec = SC_CODEC_OPUS; + return true; + } + if (!strcmp(optarg, "aac")) { + *codec = SC_CODEC_AAC; + return true; + } + if (!strcmp(optarg, "flac")) { + *codec = SC_CODEC_FLAC; + return true; + } + if (!strcmp(optarg, "raw")) { + *codec = SC_CODEC_RAW; + return true; + } + LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", + optarg); + return false; +} -bool -scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { - static const struct option long_options[] = { - {"always-on-top", no_argument, NULL, OPT_ALWAYS_ON_TOP}, - {"bit-rate", required_argument, NULL, 'b'}, - {"codec-options", required_argument, NULL, OPT_CODEC_OPTIONS}, - {"crop", required_argument, NULL, OPT_CROP}, - {"disable-screensaver", no_argument, NULL, - OPT_DISABLE_SCREENSAVER}, - {"display", required_argument, NULL, OPT_DISPLAY_ID}, - {"display-buffer", required_argument, NULL, OPT_DISPLAY_BUFFER}, - {"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}, - {"v4l2-buffer", required_argument, NULL, OPT_V4L2_BUFFER}, -#endif - {"verbosity", required_argument, NULL, 'V'}, - {"version", no_argument, NULL, 'v'}, - {"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, - {"window-x", required_argument, NULL, OPT_WINDOW_X}, - {"window-y", required_argument, NULL, OPT_WINDOW_Y}, - {"window-width", required_argument, NULL, OPT_WINDOW_WIDTH}, - {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, - {"window-borderless", no_argument, NULL, - OPT_WINDOW_BORDERLESS}, - {"power-off-on-close", no_argument, NULL, - OPT_POWER_OFF_ON_CLOSE}, - {NULL, 0, NULL, 0 }, - }; +static bool +parse_video_source(const char *optarg, enum sc_video_source *source) { + if (!strcmp(optarg, "display")) { + *source = SC_VIDEO_SOURCE_DISPLAY; + return true; + } - struct scrcpy_options *opts = &args->opts; + if (!strcmp(optarg, "camera")) { + *source = SC_VIDEO_SOURCE_CAMERA; + return true; + } - optind = 0; // reset to start from the first argument in tests + LOGE("Unsupported video source: %s (expected display or camera)", optarg); + return false; +} - int c; - 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)) { - return false; - } - break; - case 'c': - LOGW("Deprecated option -c. Use --crop instead."); - // fall through - 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; - case 'F': - LOGW("Deprecated option -F. Use --record-format instead."); - // fall through - case OPT_RECORD_FORMAT: - if (!parse_record_format(optarg, &opts->record_format)) { - return false; - } - break; - case 'h': +static bool +parse_audio_source(const char *optarg, enum sc_audio_source *source) { + if (!strcmp(optarg, "mic")) { + *source = SC_AUDIO_SOURCE_MIC; + return true; + } + + if (!strcmp(optarg, "output")) { + *source = SC_AUDIO_SOURCE_OUTPUT; + return true; + } + + LOGE("Unsupported audio source: %s (expected output or mic)", optarg); + return false; +} + +static bool +parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) { + if (!strcmp(optarg, "front")) { + *facing = SC_CAMERA_FACING_FRONT; + return true; + } + + if (!strcmp(optarg, "back")) { + *facing = SC_CAMERA_FACING_BACK; + return true; + } + + if (!strcmp(optarg, "external")) { + *facing = SC_CAMERA_FACING_EXTERNAL; + return true; + } + + if (*optarg == '\0') { + // Empty string is a valid value (equivalent to not passing the option) + *facing = SC_CAMERA_FACING_ANY; + return true; + } + + LOGE("Unsupported camera facing: %s (expected front, back or external)", + optarg); + return false; +} + +static bool +parse_camera_fps(const char *s, uint16_t *camera_fps) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "camera fps"); + if (!ok) { + return false; + } + + *camera_fps = (uint16_t) value; + return true; +} + +static bool +parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_KEYBOARD_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "sdk")) { + *mode = SC_KEYBOARD_INPUT_MODE_SDK; + return true; + } + + if (!strcmp(optarg, "uhid")) { + *mode = SC_KEYBOARD_INPUT_MODE_UHID; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_KEYBOARD_INPUT_MODE_AOA; + return true; +#else + LOGE("--keyboard=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)", + optarg); + return false; +} + +static bool +parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) { + if (!strcmp(optarg, "disabled")) { + *mode = SC_MOUSE_INPUT_MODE_DISABLED; + return true; + } + + if (!strcmp(optarg, "sdk")) { + *mode = SC_MOUSE_INPUT_MODE_SDK; + return true; + } + + if (!strcmp(optarg, "uhid")) { + *mode = SC_MOUSE_INPUT_MODE_UHID; + return true; + } + + if (!strcmp(optarg, "aoa")) { +#ifdef HAVE_USB + *mode = SC_MOUSE_INPUT_MODE_AOA; + return true; +#else + LOGE("--mouse=aoa is disabled."); + return false; +#endif + } + + LOGE("Unsupported mouse: %s (expected disabled, sdk, uhid or aoa)", optarg); + return false; +} + +static bool +parse_time_limit(const char *s, sc_tick *tick) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_SEC(value); + return true; +} + +static bool +parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { + if (!s || !strcmp(s, "true")) { + *pause_on_exit = SC_PAUSE_ON_EXIT_TRUE; + return true; + } + + if (!strcmp(s, "false")) { + *pause_on_exit = SC_PAUSE_ON_EXIT_FALSE; + return true; + } + + if (!strcmp(s, "if-error")) { + *pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR; + return true; + } + + LOGE("Unsupported pause on exit mode: %s " + "(expected true, false or if-error)", optarg); + return false; + +} + +static bool +parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], + const char *optstring, const struct option *longopts) { + struct scrcpy_options *opts = &args->opts; + + optind = 0; // reset to start from the first argument in tests + + int c; + while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) { + switch (c) { + case OPT_BIT_RATE: + LOGE("--bit-rate has been removed, " + "use --video-bit-rate or --audio-bit-rate."); + return false; + case 'b': + if (!parse_bit_rate(optarg, &opts->video_bit_rate)) { + return false; + } + break; + case OPT_AUDIO_BIT_RATE: + if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) { + return false; + } + break; + case OPT_CROP: + opts->crop = optarg; + break; + case OPT_DISPLAY: + LOGW("--display is deprecated, use --display-id instead."); + // fall through + case OPT_DISPLAY_ID: + if (!parse_display_id(optarg, &opts->display_id)) { + return false; + } + break; + case 'd': + opts->select_usb = true; + break; + case 'e': + opts->select_tcpip = true; + break; + case 'f': + opts->fullscreen = true; + break; + case OPT_RECORD_FORMAT: + if (!parse_record_format(optarg, &opts->record_format)) { + return false; + } + break; + case 'h': args->help = true; break; + case 'K': + opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID; + break; + case OPT_KEYBOARD: + if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) { + return false; + } + break; + case OPT_HID_KEYBOARD_DEPRECATED: + LOGE("--hid-keyboard has been removed, use --keyboard=aoa or " + "--keyboard=uhid instead."); + return false; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; @@ -827,17 +2122,49 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; + case 'M': + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + break; + case OPT_MOUSE: + if (!parse_mouse(optarg, &opts->mouse_input_mode)) { + return false; + } + break; + case OPT_HID_MOUSE_DEPRECATED: + LOGE("--hid-mouse has been removed, use --mouse=aoa or " + "--mouse=uhid instead."); + return false; case OPT_LOCK_VIDEO_ORIENTATION: if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { return false; } break; + case OPT_TUNNEL_HOST: + if (!parse_ip(optarg, &opts->tunnel_host)) { + return false; + } + break; + case OPT_TUNNEL_PORT: + if (!parse_port(optarg, &opts->tunnel_port)) { + return false; + } + break; case 'n': opts->control = false; break; + case OPT_NO_DISPLAY: + LOGW("--no-display is deprecated, use --no-playback instead."); + // fall through case 'N': - opts->display = false; + opts->video_playback = false; + opts->audio_playback = false; + break; + case OPT_NO_VIDEO_PLAYBACK: + opts->video_playback = false; + break; + case OPT_NO_AUDIO_PLAYBACK: + opts->audio_playback = false; break; case 'p': if (!parse_port_range(optarg, &opts->port_range)) { @@ -856,9 +2183,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 't': opts->show_touches = true; break; - case 'T': - LOGW("Deprecated option -T. Use --always-on-top instead."); - // fall through case OPT_ALWAYS_ON_TOP: opts->always_on_top = true; break; @@ -873,10 +2197,6 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case 'w': opts->stay_awake = true; break; - case OPT_RENDER_EXPIRED_FRAMES: - LOGW("Option --render-expired-frames has been removed. This " - "flag has been ignored."); - break; case OPT_WINDOW_TITLE: opts->window_title = optarg; break; @@ -907,13 +2227,65 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->push_target = optarg; break; case OPT_PREFER_TEXT: - opts->prefer_text = true; + if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) { + LOGE("--prefer-text is incompatible with --raw-key-events"); + return false; + } + opts->key_inject_mode = SC_KEY_INJECT_MODE_TEXT; + break; + case OPT_RAW_KEY_EVENTS: + if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) { + LOGE("--prefer-text is incompatible with --raw-key-events"); + return false; + } + opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; break; case OPT_ROTATION: - if (!parse_rotation(optarg, &opts->rotation)) { + LOGW("--rotation is deprecated, use --display-orientation " + "instead."); + uint8_t rotation; + if (!parse_rotation(optarg, &rotation)) { + return false; + } + assert(rotation <= 3); + switch (rotation) { + case 0: + opts->display_orientation = SC_ORIENTATION_0; + break; + case 1: + // rotation 1 was 90° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_270; + break; + case 2: + opts->display_orientation = SC_ORIENTATION_180; + break; + case 3: + // rotation 3 was 270° counterclockwise, but orientation + // is expressed clockwise + opts->display_orientation = SC_ORIENTATION_90; + break; + } + break; + case OPT_DISPLAY_ORIENTATION: + if (!parse_orientation(optarg, &opts->display_orientation)) { + return false; + } + break; + case OPT_RECORD_ORIENTATION: + if (!parse_orientation(optarg, &opts->record_orientation)) { + return false; + } + break; + case OPT_ORIENTATION: { + enum sc_orientation orientation; + if (!parse_orientation(optarg, &orientation)) { return false; } + opts->display_orientation = orientation; + opts->record_orientation = orientation; break; + } case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; @@ -924,10 +2296,24 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { opts->forward_key_repeat = false; break; case OPT_CODEC_OPTIONS: - opts->codec_options = optarg; + LOGE("--codec-options has been removed, " + "use --video-codec-options or --audio-codec-options."); + return false; + case OPT_VIDEO_CODEC_OPTIONS: + opts->video_codec_options = optarg; + break; + case OPT_AUDIO_CODEC_OPTIONS: + opts->audio_codec_options = optarg; + break; + case OPT_ENCODER: + LOGE("--encoder has been removed, " + "use --video-encoder or --audio-encoder."); + return false; + case OPT_VIDEO_ENCODER: + opts->video_encoder = optarg; break; - case OPT_ENCODER_NAME: - opts->encoder_name = optarg; + case OPT_AUDIO_ENCODER: + opts->audio_encoder = optarg; break; case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; @@ -954,77 +2340,549 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { return false; } break; -#ifdef HAVE_V4L2 + case OPT_NO_CLIPBOARD_AUTOSYNC: + opts->clipboard_autosync = false; + break; + case OPT_TCPIP: + opts->tcpip = true; + opts->tcpip_dst = optarg; + break; + case OPT_NO_DOWNSIZE_ON_ERROR: + opts->downsize_on_error = false; + break; + case OPT_NO_VIDEO: + opts->video = false; + break; + case OPT_NO_AUDIO: + opts->audio = false; + break; + case OPT_NO_CLEANUP: + opts->cleanup = false; + break; + case OPT_NO_POWER_ON: + opts->power_on = false; + break; + case OPT_PRINT_FPS: + opts->start_fps_counter = true; + break; + case OPT_CODEC: + LOGE("--codec has been removed, " + "use --video-codec or --audio-codec."); + return false; + case OPT_VIDEO_CODEC: + if (!parse_video_codec(optarg, &opts->video_codec)) { + return false; + } + break; + case OPT_AUDIO_CODEC: + if (!parse_audio_codec(optarg, &opts->audio_codec)) { + return false; + } + break; + case OPT_OTG: +#ifdef HAVE_USB + opts->otg = true; + break; +#else + LOGE("OTG mode (--otg) is disabled."); + return false; +#endif case OPT_V4L2_SINK: +#ifdef HAVE_V4L2 opts->v4l2_device = optarg; break; +#else + LOGE("V4L2 (--v4l2-sink) is disabled (or unsupported on this " + "platform)."); + return false; +#endif case OPT_V4L2_BUFFER: +#ifdef HAVE_V4L2 if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { return false; } break; +#else + LOGE("V4L2 (--v4l2-buffer) is disabled (or unsupported on this " + "platform)."); + return false; #endif + case OPT_LIST_ENCODERS: + opts->list |= SC_OPTION_LIST_ENCODERS; + break; + case OPT_LIST_DISPLAYS: + opts->list |= SC_OPTION_LIST_DISPLAYS; + break; + case OPT_LIST_CAMERAS: + opts->list |= SC_OPTION_LIST_CAMERAS; + break; + case OPT_LIST_CAMERA_SIZES: + opts->list |= SC_OPTION_LIST_CAMERA_SIZES; + break; + case OPT_REQUIRE_AUDIO: + opts->require_audio = true; + break; + case OPT_AUDIO_BUFFER: + if (!parse_buffering_time(optarg, &opts->audio_buffer)) { + return false; + } + break; + case OPT_AUDIO_OUTPUT_BUFFER: + if (!parse_audio_output_buffer(optarg, + &opts->audio_output_buffer)) { + return false; + } + break; + case OPT_VIDEO_SOURCE: + if (!parse_video_source(optarg, &opts->video_source)) { + return false; + } + break; + case OPT_AUDIO_SOURCE: + if (!parse_audio_source(optarg, &opts->audio_source)) { + return false; + } + break; + case OPT_KILL_ADB_ON_CLOSE: + opts->kill_adb_on_close = true; + break; + case OPT_TIME_LIMIT: + if (!parse_time_limit(optarg, &opts->time_limit)) { + return false; + } + break; + case OPT_PAUSE_ON_EXIT: + if (!parse_pause_on_exit(optarg, &args->pause_on_exit)) { + return false; + } + break; + case OPT_CAMERA_AR: + opts->camera_ar = optarg; + break; + case OPT_CAMERA_ID: + opts->camera_id = optarg; + break; + case OPT_CAMERA_SIZE: + opts->camera_size = optarg; + break; + case OPT_CAMERA_FACING: + if (!parse_camera_facing(optarg, &opts->camera_facing)) { + return false; + } + break; + case OPT_CAMERA_FPS: + if (!parse_camera_fps(optarg, &opts->camera_fps)) { + return false; + } + break; + case OPT_CAMERA_HIGH_SPEED: + opts->camera_high_speed = true; + break; default: // getopt prints the error message on stderr return false; } } + int index = optind; + if (index < argc) { + LOGE("Unexpected additional argument: %s", argv[index]); + return false; + } + + // If a TCP/IP address is provided, then tcpip must be enabled + assert(opts->tcpip || !opts->tcpip_dst); + + unsigned selectors = !!opts->serial + + !!opts->tcpip_dst + + opts->select_tcpip + + opts->select_usb; + if (selectors > 1) { + LOGE("At most one device selector option may be passed, among:\n" + " --serial (-s)\n" + " --select-usb (-d)\n" + " --select-tcpip (-e)\n" + " --tcpip= (with an argument)"); + return false; + } + + bool otg = false; + bool v4l2 = false; +#ifdef HAVE_USB + otg = opts->otg; +#endif #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)"); + v4l2 = !!opts->v4l2_device; +#endif + + if (!opts->video) { + opts->video_playback = false; + // Do not power on the device on start if video capture is disabled + opts->power_on = false; + } + + if (!opts->audio) { + opts->audio_playback = false; + } + + if (opts->video && !opts->video_playback && !opts->record_filename + && !v4l2) { + LOGI("No video playback, no recording, no V4L2 sink: video disabled"); + opts->video = false; + } + + if (opts->audio && !opts->audio_playback && !opts->record_filename) { + LOGI("No audio playback, no recording: audio disabled"); + opts->audio = false; + } + + if (!opts->video && !opts->audio && !otg) { + LOGE("No video, no audio, no OTG: nothing to do"); 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; + if (!opts->video && !otg) { + // If video is disabled, then scrcpy must exit on audio failure. + opts->require_audio = true; + } + + if (opts->audio_playback && opts->audio_buffer == -1) { + if (opts->audio_codec == SC_CODEC_FLAC) { + // Use 50 ms audio buffer by default, but use a higher value for FLAC, + // which is not low latency (the default encoder produces blocks of + // 4096 samples, which represent ~85.333ms). + LOGI("FLAC audio: audio buffer increased to 120 ms (use " + "--audio-buffer to set a custom value)"); + opts->audio_buffer = SC_TICK_FROM_MS(120); + } else { + opts->audio_buffer = SC_TICK_FROM_MS(50); + } + } + +#ifdef HAVE_V4L2 + if (v4l2) { + if (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; + } + + // V4L2 could not handle size change. + // Do not log because downsizing on error is the default behavior, + // not an explicit request from the user. + opts->downsize_on_error = false; } if (opts->v4l2_buffer && !opts->v4l2_device) { LOGE("V4L2 buffer value without V4L2 sink\n"); return false; } -#else - if (!opts->display && !opts->record_filename) { - LOGE("-N/--no-display requires screen recording (-r/--record)"); - return false; - } #endif - int index = optind; - if (index < argc) { - LOGE("Unexpected additional argument: %s", argv[index]); + if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) { + opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA + : SC_KEYBOARD_INPUT_MODE_SDK; + } + if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) { + opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA + : SC_MOUSE_INPUT_MODE_SDK; + } + + if (otg) { + enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode; + if (kmode != SC_KEYBOARD_INPUT_MODE_AOA + && kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --keyboard only supports aoa or disabled."); + return false; + } + + enum sc_mouse_input_mode mmode = opts->mouse_input_mode; + if (mmode != SC_MOUSE_INPUT_MODE_AOA + && mmode != SC_MOUSE_INPUT_MODE_DISABLED) { + LOGE("In OTG mode, --mouse only supports aoa or disabled."); + return false; + } + + if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED + && mmode == SC_MOUSE_INPUT_MODE_DISABLED) { + LOGE("Could not disable both keyboard and mouse in OTG mode."); + return false; + } + } + + if (opts->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_SDK) { + if (opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT) { + LOGE("--prefer-text is specific to --keyboard=sdk"); + return false; + } + + if (opts->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + LOGE("--raw-key-events is specific to --keyboard=sdk"); + return false; + } + + if (!opts->forward_key_repeat) { + LOGE("--no-key-repeat is specific to --keyboard=sdk"); + return false; + } + } + + if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { + LOGI("Tunnel host/port is set, " + "--force-adb-forward automatically enabled."); + opts->force_adb_forward = true; + } + + if (opts->video_source == SC_VIDEO_SOURCE_CAMERA) { + if (opts->display_id) { + LOGE("--display-id is only available with --video-source=display"); + return false; + } + + if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { + LOGE("Could not specify both --camera-id and --camera-facing"); + return false; + } + + if (opts->camera_size) { + if (opts->max_size) { + LOGE("Could not specify both --camera-size and -m/--max-size"); + return false; + } + + if (opts->camera_ar) { + LOGE("Could not specify both --camera-size and --camera-ar"); + return false; + } + } + + if (opts->camera_high_speed && !opts->camera_fps) { + LOGE("--camera-high-speed requires an explicit --camera-fps value"); + return false; + } + + if (opts->control) { + LOGI("Camera video source: control disabled"); + opts->control = false; + } + } else if (opts->camera_id + || opts->camera_ar + || opts->camera_facing != SC_CAMERA_FACING_ANY + || opts->camera_fps + || opts->camera_high_speed + || opts->camera_size) { + LOGE("Camera options are only available with --video-source=camera"); return false; } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { + // Select the audio source according to the video source + if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { + opts->audio_source = SC_AUDIO_SOURCE_OUTPUT; + } else { + opts->audio_source = SC_AUDIO_SOURCE_MIC; + LOGI("Camera video source: microphone audio source selected"); + } + } + if (opts->record_format && !opts->record_filename) { LOGE("Record format specified without recording"); return false; } - if (opts->record_filename && !opts->record_format) { - opts->record_format = guess_record_format(opts->record_filename); + if (opts->record_filename) { if (!opts->record_format) { - LOGE("No format specified for \"%s\" " - "(try with --record-format=mkv)", - opts->record_filename); + opts->record_format = guess_record_format(opts->record_filename); + if (!opts->record_format) { + LOGE("No format specified for \"%s\" " + "(try with --record-format=mkv)", + opts->record_filename); + return false; + } + } + + if (opts->record_orientation != SC_ORIENTATION_0) { + if (sc_orientation_is_mirror(opts->record_orientation)) { + LOGE("Record orientation only supports rotation, not " + "flipping: %s", + sc_orientation_get_name(opts->record_orientation)); + return false; + } + } + + if (opts->video + && sc_record_format_is_audio_only(opts->record_format)) { + LOGE("Audio container does not support video stream"); + return false; + } + + if (opts->record_format == SC_RECORD_FORMAT_OPUS + && opts->audio_codec != SC_CODEC_OPUS) { + LOGE("Recording to OPUS file requires an OPUS audio stream " + "(try with --audio-codec=opus)"); + return false; + } + + if (opts->record_format == SC_RECORD_FORMAT_AAC + && opts->audio_codec != SC_CODEC_AAC) { + LOGE("Recording to AAC file requires an AAC audio stream " + "(try with --audio-codec=aac)"); + return false; + } + if (opts->record_format == SC_RECORD_FORMAT_FLAC + && opts->audio_codec != SC_CODEC_FLAC) { + LOGE("Recording to FLAC file requires a FLAC audio stream " + "(try with --audio-codec=flac)"); + return false; + } + + if (opts->record_format == SC_RECORD_FORMAT_WAV + && opts->audio_codec != SC_CODEC_RAW) { + LOGE("Recording to WAV file requires a RAW audio stream " + "(try with --audio-codec=raw)"); + return false; + } + + if ((opts->record_format == SC_RECORD_FORMAT_MP4 || + opts->record_format == SC_RECORD_FORMAT_M4A) + && opts->audio_codec == SC_CODEC_RAW) { + LOGE("Recording to MP4 container does not support RAW audio"); return false; } } - if (!opts->control && opts->turn_screen_off) { - LOGE("Could not request to turn screen off if control is disabled"); - return false; + if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { + LOGW("--audio-bit-rate is ignored for FLAC audio codec"); } - if (!opts->control && opts->stay_awake) { - LOGE("Could not request to stay awake if control is disabled"); + if (opts->audio_codec == SC_CODEC_RAW) { + if (opts->audio_bit_rate) { + LOGW("--audio-bit-rate is ignored for raw audio codec"); + } + if (opts->audio_codec_options) { + LOGW("--audio-codec-options is ignored for raw audio codec"); + } + if (opts->audio_encoder) { + LOGW("--audio-encoder is ignored for raw audio codec"); + } + } + + if (!opts->control) { + if (opts->turn_screen_off) { + LOGE("Could not request to turn screen off if control is disabled"); + return false; + } + if (opts->stay_awake) { + LOGE("Could not request to stay awake if control is disabled"); + return false; + } + if (opts->show_touches) { + LOGE("Could not request to show touches if control is disabled"); + return false; + } + if (opts->power_off_on_close) { + LOGE("Could not request power off on close if control is disabled"); + return false; + } + } + +# ifdef _WIN32 + if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA + || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA)) { + LOGE("On Windows, it is not possible to open a USB device already open " + "by another process (like adb)."); + LOGE("Therefore, --keyboard=aoa and --mouse=aoa may only work in OTG" + "mode (--otg)."); return false; } +# endif + + if (otg) { + // OTG mode is compatible with only very few options. + // Only report obvious errors. + if (opts->record_filename) { + LOGE("OTG mode: could not record"); + return false; + } + if (opts->turn_screen_off) { + LOGE("OTG mode: could not turn screen off"); + return false; + } + if (opts->stay_awake) { + LOGE("OTG mode: could not stay awake"); + return false; + } + if (opts->show_touches) { + LOGE("OTG mode: could not request to show touches"); + return false; + } + if (opts->power_off_on_close) { + LOGE("OTG mode: could not request power off on close"); + return false; + } + if (opts->display_id) { + LOGE("OTG mode: could not select display"); + return false; + } + if (v4l2) { + LOGE("OTG mode: could not sink to V4L2 device"); + return false; + } + } return true; } + +static enum sc_pause_on_exit +sc_get_pause_on_exit(int argc, char *argv[]) { + // Read arguments backwards so that the last --pause-on-exit is considered + // (same behavior as getopt()) + for (int i = argc - 1; i >= 1; --i) { + const char *arg = argv[i]; + // Starts with "--pause-on-exit" + if (!strncmp("--pause-on-exit", arg, 15)) { + if (arg[15] == '\0') { + // No argument + return SC_PAUSE_ON_EXIT_TRUE; + } + if (arg[15] != '=') { + // Invalid parameter, ignore + return SC_PAUSE_ON_EXIT_FALSE; + } + const char *value = &arg[16]; + if (!strcmp(value, "true")) { + return SC_PAUSE_ON_EXIT_TRUE; + } + if (!strcmp(value, "if-error")) { + return SC_PAUSE_ON_EXIT_IF_ERROR; + } + // Set to false, inclusing when the value is invalid + return SC_PAUSE_ON_EXIT_FALSE; + } + } + + return SC_PAUSE_ON_EXIT_FALSE; +} + +bool +scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { + struct sc_getopt_adapter adapter; + if (!sc_getopt_adapter_init(&adapter)) { + LOGW("Could not create getopt adapter"); + return false; + } + + bool ret = parse_args_with_getopt(args, argc, argv, adapter.optstring, + adapter.longopts); + + sc_getopt_adapter_destroy(&adapter); + + if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) { + // Check if "--pause-on-exit" is present in the arguments list, because + // it must be taken into account even if command line parsing failed + args->pause_on_exit = sc_get_pause_on_exit(argc, argv); + } + + return ret; +} diff --git a/app/src/cli.h b/app/src/cli.h index 419f156f..23d34fcd 100644 --- a/app/src/cli.h +++ b/app/src/cli.h @@ -5,12 +5,19 @@ #include -#include "scrcpy.h" +#include "options.h" + +enum sc_pause_on_exit { + SC_PAUSE_ON_EXIT_TRUE, + SC_PAUSE_ON_EXIT_FALSE, + SC_PAUSE_ON_EXIT_IF_ERROR, +}; struct scrcpy_cli_args { struct scrcpy_options opts; bool help; bool version; + enum sc_pause_on_exit pause_on_exit; }; void diff --git a/app/src/clock.c b/app/src/clock.c index fe072f01..92989bfe 100644 --- a/app/src/clock.c +++ b/app/src/clock.c @@ -1,111 +1,36 @@ #include "clock.h" +#include + #include "util/log.h" #define SC_CLOCK_NDEBUG // comment to debug +#define SC_CLOCK_RANGE 32 + void sc_clock_init(struct sc_clock *clock) { - clock->count = 0; - clock->head = 0; - clock->left_sum.system = 0; - clock->left_sum.stream = 0; - clock->right_sum.system = 0; - clock->right_sum.stream = 0; -} - -// Estimate the affine function f(stream) = slope * stream + offset -static void -sc_clock_estimate(struct sc_clock *clock, - double *out_slope, sc_tick *out_offset) { - assert(clock->count > 1); // two points are necessary - - struct sc_clock_point left_avg = { - .system = clock->left_sum.system / (clock->count / 2), - .stream = clock->left_sum.stream / (clock->count / 2), - }; - struct sc_clock_point right_avg = { - .system = clock->right_sum.system / ((clock->count + 1) / 2), - .stream = clock->right_sum.stream / ((clock->count + 1) / 2), - }; - - double slope = (double) (right_avg.system - left_avg.system) - / (right_avg.stream - left_avg.stream); - - if (clock->count < SC_CLOCK_RANGE) { - /* The first frames are typically received and decoded with more delay - * than the others, causing a wrong slope estimation on start. To - * compensate, assume an initial slope of 1, then progressively use the - * estimated slope. */ - slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count)) - / SC_CLOCK_RANGE; - } - - struct sc_clock_point global_avg = { - .system = (clock->left_sum.system + clock->right_sum.system) - / clock->count, - .stream = (clock->left_sum.stream + clock->right_sum.stream) - / clock->count, - }; - - sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); - - *out_slope = slope; - *out_offset = offset; + clock->range = 0; + clock->offset = 0; } void sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { - struct sc_clock_point *point = &clock->points[clock->head]; - - if (clock->count == SC_CLOCK_RANGE || clock->count & 1) { - // One point passes from the right sum to the left sum - - unsigned mid; - if (clock->count == SC_CLOCK_RANGE) { - mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE; - } else { - // Only for the first frames - mid = clock->count / 2; - } - - struct sc_clock_point *mid_point = &clock->points[mid]; - clock->left_sum.system += mid_point->system; - clock->left_sum.stream += mid_point->stream; - clock->right_sum.system -= mid_point->system; - clock->right_sum.stream -= mid_point->stream; - } - - if (clock->count == SC_CLOCK_RANGE) { - // The current point overwrites the previous value in the circular - // array, update the left sum accordingly - clock->left_sum.system -= point->system; - clock->left_sum.stream -= point->stream; - } else { - ++clock->count; + if (clock->range < SC_CLOCK_RANGE) { + ++clock->range; } - point->system = system; - point->stream = stream; - - clock->right_sum.system += system; - clock->right_sum.stream += stream; - - clock->head = (clock->head + 1) % SC_CLOCK_RANGE; - - if (clock->count > 1) { - // Update estimation - sc_clock_estimate(clock, &clock->slope, &clock->offset); + sc_tick offset = system - stream; + clock->offset = ((clock->range - 1) * clock->offset + offset) + / clock->range; #ifndef SC_CLOCK_NDEBUG - LOGD("Clock estimation: %g * pts + %" PRItick, - clock->slope, clock->offset); + LOGD("Clock estimation: pts + %" PRItick, clock->offset); #endif - } } sc_tick sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { - assert(clock->count > 1); // sc_clock_update() must have been called - return (sc_tick) (stream * clock->slope) + clock->offset; + assert(clock->range); // sc_clock_update() must have been called + return stream + clock->offset; } diff --git a/app/src/clock.h b/app/src/clock.h index eb7fa594..0d34ab99 100644 --- a/app/src/clock.h +++ b/app/src/clock.h @@ -3,13 +3,8 @@ #include "common.h" -#include - #include "util/tick.h" -#define SC_CLOCK_RANGE 32 -static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even"); - struct sc_clock_point { sc_tick system; sc_tick stream; @@ -21,40 +16,18 @@ struct sc_clock_point { * * f(stream) = slope * stream + offset * - * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps - * of a frame expressed both in stream time and system time) in a circular - * array. + * Theoretically, the slope encodes the drift between the device clock and the + * computer clock. It is expected to be very close to 1. * - * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two - * sets of SC_CLOCK_RANGE/2 points, and compute their centroid ("average - * point"). The slope of the estimated affine function is that of the line - * passing through these two points. + * Since the clock is used to estimate very close points in the future (which + * are reestimated on every clock update, see delay_buffer), the error caused + * by clock drift is totally negligible, so it is better to assume that the + * slope is 1 than to estimate it (the estimation error would be larger). * - * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE - * points. The resulting affine function passes by this centroid. - * - * With a circular array, the rolling sums (and average) are quick to compute. - * In practice, the estimation is stable and the evolution is smooth. + * Therefore, only the offset is estimated. */ struct sc_clock { - // Circular array - struct sc_clock_point points[SC_CLOCK_RANGE]; - - // Number of points in the array (count <= SC_CLOCK_RANGE) - unsigned count; - - // Index of the next point to write - unsigned head; - - // Sum of the first count/2 points - struct sc_clock_point left_sum; - - // Sum of the last (count+1)/2 points - struct sc_clock_point right_sum; - - // Estimated slope and offset - // (computed on sc_clock_update(), used by sc_clock_to_system_time()) - double slope; + unsigned range; sc_tick offset; }; diff --git a/app/src/common.h b/app/src/common.h index accbc615..0382d094 100644 --- a/app/src/common.h +++ b/app/src/common.h @@ -1,12 +1,13 @@ -#ifndef COMMON_H -#define COMMON_H +#ifndef SC_COMMON_H +#define SC_COMMON_H #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) +#define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) +#define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) +#define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) ) #define container_of(ptr, type, member) \ ((type *) (((char *) (ptr)) - offsetof(type, member))) diff --git a/app/src/compat.c b/app/src/compat.c index b3b98bf1..785f843c 100644 --- a/app/src/compat.c +++ b/app/src/compat.c @@ -2,6 +2,15 @@ #include "config.h" +#include +#ifndef HAVE_REALLOCARRAY +# include +#endif +#include +#include +#include +#include + #ifndef HAVE_STRDUP char *strdup(const char *s) { size_t size = strlen(s) + 1; @@ -12,3 +21,90 @@ char *strdup(const char *s) { return dup; } #endif + +#ifndef HAVE_ASPRINTF +int asprintf(char **strp, const char *fmt, ...) { + va_list va; + va_start(va, fmt); + int ret = vasprintf(strp, fmt, va); + va_end(va); + return ret; +} +#endif + +#ifndef HAVE_VASPRINTF +int vasprintf(char **strp, const char *fmt, va_list ap) { + va_list va; + va_copy(va, ap); + int len = vsnprintf(NULL, 0, fmt, va); + va_end(va); + + char *str = malloc(len + 1); + if (!str) { + return -1; + } + + va_copy(va, ap); + int len2 = vsnprintf(str, len + 1, fmt, va); + (void) len2; + assert(len == len2); + va_end(va); + + *strp = str; + return len; +} +#endif + +#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48) +#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits +#define SC_RAND48_A UINT64_C(0x5DEECE66D) +#define SC_RAND48_C 0xB +static inline uint64_t rand_iter48(uint64_t x) { + assert((x & ~SC_RAND48_MASK) == 0); + return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK; +} + +static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) { + uint64_t x = ((uint64_t) xsubi[0] << 32) + | ((uint64_t) xsubi[1] << 16) + | xsubi[2]; + + x = rand_iter48(x); + + xsubi[0] = (x >> 32) & 0XFFFF; + xsubi[1] = (x >> 16) & 0XFFFF; + xsubi[2] = x & 0XFFFF; + + return x; +} + +#ifndef HAVE_NRAND48 +long nrand48(unsigned short xsubi[3]) { + // range [0, 2^31) + return rand_iter48_xsubi(xsubi) >> 17; +} +#endif + +#ifndef HAVE_JRAND48 +long jrand48(unsigned short xsubi[3]) { + // range [-2^31, 2^31) + union { + uint32_t u; + int32_t i; + } v; + v.u = rand_iter48_xsubi(xsubi) >> 16; + return v.i; +} +#endif +#endif + +#ifndef HAVE_REALLOCARRAY +void *reallocarray(void *ptr, size_t nmemb, size_t size) { + size_t bytes; + if (__builtin_mul_overflow(nmemb, size, &bytes)) { + errno = ENOMEM; + return NULL; + } + return realloc(ptr, bytes); +} +#endif diff --git a/app/src/compat.h b/app/src/compat.h index 8e2d18f4..fd610c02 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -1,16 +1,21 @@ -#ifndef COMPAT_H -#define COMPAT_H +#ifndef SC_COMPAT_H +#define SC_COMPAT_H -#define _POSIX_C_SOURCE 200809L -#define _XOPEN_SOURCE 700 -#define _GNU_SOURCE -#ifdef __APPLE__ -# define _DARWIN_C_SOURCE -#endif +#include "config.h" +#include #include +#include #include +#ifndef __WIN32 +# define PRIu64_ PRIu64 +# define SC_PRIsizet "zu" +#else +# define PRIu64_ "I64u" // Windows... +# define SC_PRIsizet "Iu" +#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(), @@ -22,6 +27,12 @@ # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif +// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added +// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag +// n3.3). +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100) +# define SCRCPY_LAVC_HAS_AV1 +#endif // In ffmpeg/doc/APIchanges: // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h @@ -34,13 +45,25 @@ # define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL #endif -#if SDL_VERSION_ATLEAST(2, 0, 5) -// -# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH -// -# define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS -// -# define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP +// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API +// has been replaced by chlayout in FFmpeg commit +// f423497b455da06c1337846902c770028760e094. +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100) +# define SCRCPY_LAVU_HAS_CHLAYOUT +#endif + +// In ffmpeg/doc/APIchanges: +// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h +// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(), +// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields +// from AVFormatContext.codecpar should be used from now on. +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100) +# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA +#endif + +#if SDL_VERSION_ATLEAST(2, 0, 6) +// +# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS #endif #if SDL_VERSION_ATLEAST(2, 0, 8) @@ -48,8 +71,32 @@ # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR #endif +#if SDL_VERSION_ATLEAST(2, 0, 16) +# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL +#endif + #ifndef HAVE_STRDUP char *strdup(const char *s); #endif +#ifndef HAVE_ASPRINTF +int asprintf(char **strp, const char *fmt, ...); +#endif + +#ifndef HAVE_VASPRINTF +int vasprintf(char **strp, const char *fmt, va_list ap); +#endif + +#ifndef HAVE_NRAND48 +long nrand48(unsigned short xsubi[3]); +#endif + +#ifndef HAVE_JRAND48 +long jrand48(unsigned short xsubi[3]); +#endif + +#ifndef HAVE_REALLOCARRAY +void *reallocarray(void *ptr, size_t nmemb, size_t size); +#endif + #endif diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 1257010e..b3da5fe5 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -5,9 +5,9 @@ #include #include -#include "util/buffer_util.h" +#include "util/binary.h" #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" /** * Map an enum value to a string based on an array, without crashing on an @@ -37,11 +37,11 @@ static const char *const android_motionevent_action_labels[] = { "move", "cancel", "outside", - "ponter-down", + "pointer-down", "pointer-up", "hover-move", "scroll", - "hover-enter" + "hover-enter", "hover-exit", "btn-press", "btn-release", @@ -55,83 +55,113 @@ static const char *const screen_power_mode_labels[] = { "suspend", }; +static const char *const copy_key_labels[] = { + "none", + "copy", + "cut", +}; + +static inline const char * +get_well_known_pointer_id_name(uint64_t pointer_id) { + switch (pointer_id) { + case POINTER_ID_MOUSE: + return "mouse"; + case POINTER_ID_GENERIC_FINGER: + return "finger"; + case POINTER_ID_VIRTUAL_MOUSE: + return "vmouse"; + case POINTER_ID_VIRTUAL_FINGER: + return "vfinger"; + default: + return NULL; + } +} + static void -write_position(uint8_t *buf, const struct position *position) { - buffer_write32be(&buf[0], position->point.x); - buffer_write32be(&buf[4], position->point.y); - buffer_write16be(&buf[8], position->screen_size.width); - buffer_write16be(&buf[10], position->screen_size.height); +write_position(uint8_t *buf, const struct sc_position *position) { + sc_write32be(&buf[0], position->point.x); + sc_write32be(&buf[4], position->point.y); + sc_write16be(&buf[8], position->screen_size.width); + sc_write16be(&buf[10], position->screen_size.height); } -// write length (2 bytes) + string (non nul-terminated) +// write length (4 bytes) + string (non null-terminated) static size_t -write_string(const char *utf8, size_t max_len, unsigned char *buf) { - size_t len = utf8_truncation_index(utf8, max_len); - buffer_write32be(buf, len); +write_string(const char *utf8, size_t max_len, uint8_t *buf) { + size_t len = sc_str_utf8_truncation_index(utf8, max_len); + sc_write32be(buf, len); memcpy(&buf[4], utf8, len); return 4 + len; } -static uint16_t -to_fixed_point_16(float f) { - assert(f >= 0.0f && f <= 1.0f); - uint32_t u = f * 0x1p16f; // 2^16 - if (u >= 0xffff) { - u = 0xffff; - } - return (uint16_t) u; -} - size_t -control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { +sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { buf[0] = msg->type; switch (msg->type) { - case CONTROL_MSG_TYPE_INJECT_KEYCODE: + case SC_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.repeat); - buffer_write32be(&buf[10], msg->inject_keycode.metastate); + sc_write32be(&buf[2], msg->inject_keycode.keycode); + sc_write32be(&buf[6], msg->inject_keycode.repeat); + sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; - case CONTROL_MSG_TYPE_INJECT_TEXT: { + case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(msg->inject_text.text, - CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } - case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: + case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; - buffer_write64be(&buf[2], msg->inject_touch_event.pointer_id); + sc_write64be(&buf[2], msg->inject_touch_event.pointer_id); write_position(&buf[10], &msg->inject_touch_event.position); uint16_t pressure = - to_fixed_point_16(msg->inject_touch_event.pressure); - buffer_write16be(&buf[22], pressure); - buffer_write32be(&buf[24], msg->inject_touch_event.buttons); - return 28; - case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + sc_float_to_u16fp(msg->inject_touch_event.pressure); + sc_write16be(&buf[22], pressure); + sc_write32be(&buf[24], msg->inject_touch_event.action_button); + sc_write32be(&buf[28], msg->inject_touch_event.buttons); + return 32; + case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - buffer_write32be(&buf[13], - (uint32_t) msg->inject_scroll_event.hscroll); - buffer_write32be(&buf[17], - (uint32_t) msg->inject_scroll_event.vscroll); + int16_t hscroll = + sc_float_to_i16fp(msg->inject_scroll_event.hscroll); + int16_t vscroll = + sc_float_to_i16fp(msg->inject_scroll_event.vscroll); + sc_write16be(&buf[13], (uint16_t) hscroll); + sc_write16be(&buf[15], (uint16_t) vscroll); + sc_write32be(&buf[17], msg->inject_scroll_event.buttons); return 21; - case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; - case CONTROL_MSG_TYPE_SET_CLIPBOARD: { - buf[1] = !!msg->set_clipboard.paste; + case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD: + buf[1] = msg->get_clipboard.copy_key; + return 2; + case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: + sc_write64be(&buf[1], msg->set_clipboard.sequence); + buf[9] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, - CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, - &buf[2]); - return 2 + len; - } - case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, + &buf[10]); + return 10 + len; + case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; - case CONTROL_MSG_TYPE_EXPAND_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: + case SC_CONTROL_MSG_TYPE_UHID_CREATE: + sc_write16be(&buf[1], msg->uhid_create.id); + sc_write16be(&buf[3], msg->uhid_create.report_desc_size); + memcpy(&buf[5], msg->uhid_create.report_desc, + msg->uhid_create.report_desc_size); + return 5 + msg->uhid_create.report_desc_size; + case SC_CONTROL_MSG_TYPE_UHID_INPUT: + sc_write16be(&buf[1], msg->uhid_input.id); + sc_write16be(&buf[3], msg->uhid_input.size); + memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size); + return 5 + msg->uhid_input.size; + case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: + case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: + case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: + case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: // no additional data return 1; default: @@ -141,87 +171,109 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) { } void -control_msg_log(const struct control_msg *msg) { +sc_control_msg_log(const struct sc_control_msg *msg) { #define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) switch (msg->type) { - case CONTROL_MSG_TYPE_INJECT_KEYCODE: + case SC_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: + case SC_CONTROL_MSG_TYPE_INJECT_TEXT: LOG_CMSG("text \"%s\"", msg->inject_text.text); break; - case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { + case SC_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) { + const char *pointer_name = get_well_known_pointer_id_name(id); + if (pointer_name) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 - " pressure=%g buttons=%06lx", - id == POINTER_ID_MOUSE ? "mouse" : "vfinger", + " pressure=%f action_button=%06lx buttons=%06lx", + pointer_name, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.action_button, (long) msg->inject_touch_event.buttons); } else { // numeric pointer id -#ifndef __WIN32 -# define PRIu64_ PRIu64 -#else -# define PRIu64_ "I64u" // Windows... -#endif LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" - PRIi32 " pressure=%g buttons=%06lx", + PRIi32 " pressure=%f action_button=%06lx" + " buttons=%06lx", id, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, + (long) msg->inject_touch_event.action_button, (long) msg->inject_touch_event.buttons); } break; } - case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: - LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 - " vscroll=%" PRIi32, + case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: + LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%f" + " vscroll=%f buttons=%06lx", msg->inject_scroll_event.position.point.x, msg->inject_scroll_event.position.point.y, msg->inject_scroll_event.hscroll, - msg->inject_scroll_event.vscroll); + msg->inject_scroll_event.vscroll, + (long) msg->inject_scroll_event.buttons); break; - case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: + case SC_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", + case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD: + LOG_CMSG("get clipboard copy_key=%s", + copy_key_labels[msg->get_clipboard.copy_key]); + break; + case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: + LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", + msg->set_clipboard.sequence, + msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.text); break; - case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: + case SC_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: + case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); break; - case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: + case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: LOG_CMSG("expand settings panel"); break; - case CONTROL_MSG_TYPE_COLLAPSE_PANELS: + case SC_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: + case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; + case SC_CONTROL_MSG_TYPE_UHID_CREATE: + LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16, + msg->uhid_create.id, msg->uhid_create.report_desc_size); + break; + case SC_CONTROL_MSG_TYPE_UHID_INPUT: { + char *hex = sc_str_to_hex_string(msg->uhid_input.data, + msg->uhid_input.size); + if (hex) { + LOG_CMSG("UHID input [%" PRIu16 "] %s", + msg->uhid_input.id, hex); + free(hex); + } else { + LOG_CMSG("UHID input [%" PRIu16 "] size=%" PRIu16, + msg->uhid_input.id, msg->uhid_input.size); + } + break; + } + case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + LOG_CMSG("open hard keyboard settings"); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; @@ -229,12 +281,12 @@ control_msg_log(const struct control_msg *msg) { } void -control_msg_destroy(struct control_msg *msg) { +sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { - case CONTROL_MSG_TYPE_INJECT_TEXT: + case SC_CONTROL_MSG_TYPE_INJECT_TEXT: free(msg->inject_text.text); break; - case CONTROL_MSG_TYPE_SET_CLIPBOARD: + case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; default: diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 920a493a..cd1340ef 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -1,5 +1,5 @@ -#ifndef CONTROLMSG_H -#define CONTROLMSG_H +#ifndef SC_CONTROLMSG_H +#define SC_CONTROLMSG_H #include "common.h" @@ -10,39 +10,53 @@ #include "android/input.h" #include "android/keycodes.h" #include "coords.h" +#include "hid/hid_event.h" -#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k +#define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k -#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 SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 +// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes +#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) #define POINTER_ID_MOUSE UINT64_C(-1) -#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2) +#define POINTER_ID_GENERIC_FINGER UINT64_C(-2) -enum control_msg_type { - CONTROL_MSG_TYPE_INJECT_KEYCODE, - CONTROL_MSG_TYPE_INJECT_TEXT, - CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, - CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, - CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, - CONTROL_MSG_TYPE_EXPAND_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, - CONTROL_MSG_TYPE_ROTATE_DEVICE, +// Used for injecting an additional virtual pointer for pinch-to-zoom +#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) +#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) + +enum sc_control_msg_type { + SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, + SC_CONTROL_MSG_TYPE_INJECT_TEXT, + SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, + SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, + SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, + SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, + SC_CONTROL_MSG_TYPE_UHID_CREATE, + SC_CONTROL_MSG_TYPE_UHID_INPUT, + SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; -enum screen_power_mode { +enum sc_screen_power_mode { // see - SCREEN_POWER_MODE_OFF = 0, - SCREEN_POWER_MODE_NORMAL = 2, + SC_SCREEN_POWER_MODE_OFF = 0, + SC_SCREEN_POWER_MODE_NORMAL = 2, +}; + +enum sc_copy_key { + SC_COPY_KEY_NONE, + SC_COPY_KEY_COPY, + SC_COPY_KEY_CUT, }; -struct control_msg { - enum control_msg_type type; +struct sc_control_msg { + enum sc_control_msg_type type; union { struct { enum android_keyevent_action action; @@ -55,39 +69,55 @@ struct control_msg { } inject_text; struct { enum android_motionevent_action action; + enum android_motionevent_buttons action_button; enum android_motionevent_buttons buttons; uint64_t pointer_id; - struct position position; + struct sc_position position; float pressure; } inject_touch_event; struct { - struct position position; - int32_t hscroll; - int32_t vscroll; + struct sc_position position; + float hscroll; + float vscroll; + enum android_motionevent_buttons buttons; } inject_scroll_event; struct { enum android_keyevent_action action; // action for the BACK key // screen may only be turned on on ACTION_DOWN } back_or_screen_on; struct { + enum sc_copy_key copy_key; + } get_clipboard; + struct { + uint64_t sequence; char *text; // owned, to be freed by free() bool paste; } set_clipboard; struct { - enum screen_power_mode mode; + enum sc_screen_power_mode mode; } set_screen_power_mode; + struct { + uint16_t id; + uint16_t report_desc_size; + const uint8_t *report_desc; // pointer to static data + } uhid_create; + struct { + uint16_t id; + uint16_t size; + uint8_t data[SC_HID_MAX_SIZE]; + } uhid_input; }; }; // 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); +sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf); void -control_msg_log(const struct control_msg *msg); +sc_control_msg_log(const struct sc_control_msg *msg); void -control_msg_destroy(struct control_msg *msg); +sc_control_msg_destroy(struct sc_control_msg *msg); #endif diff --git a/app/src/controller.c b/app/src/controller.c index 17844c98..499cfd3c 100644 --- a/app/src/controller.c +++ b/app/src/controller.c @@ -4,25 +4,35 @@ #include "util/log.h" +#define SC_CONTROL_MSG_QUEUE_MAX 64 + bool -controller_init(struct controller *controller, socket_t control_socket) { - cbuf_init(&controller->queue); +sc_controller_init(struct sc_controller *controller, sc_socket control_socket) { + sc_vecdeque_init(&controller->queue); + + bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); + if (!ok) { + return false; + } - bool ok = receiver_init(&controller->receiver, control_socket); + ok = sc_receiver_init(&controller->receiver, control_socket); if (!ok) { + sc_vecdeque_destroy(&controller->queue); return false; } ok = sc_mutex_init(&controller->mutex); if (!ok) { - receiver_destroy(&controller->receiver); + sc_receiver_destroy(&controller->receiver); + sc_vecdeque_destroy(&controller->queue); return false; } ok = sc_cond_init(&controller->msg_cond); if (!ok) { - receiver_destroy(&controller->receiver); + sc_receiver_destroy(&controller->receiver); sc_mutex_destroy(&controller->mutex); + sc_vecdeque_destroy(&controller->queue); return false; } @@ -33,54 +43,72 @@ controller_init(struct controller *controller, socket_t control_socket) { } void -controller_destroy(struct controller *controller) { +sc_controller_configure(struct sc_controller *controller, + struct sc_acksync *acksync, + struct sc_uhid_devices *uhid_devices) { + controller->receiver.acksync = acksync; + controller->receiver.uhid_devices = uhid_devices; +} + +void +sc_controller_destroy(struct sc_controller *controller) { sc_cond_destroy(&controller->msg_cond); sc_mutex_destroy(&controller->mutex); - struct control_msg msg; - while (cbuf_take(&controller->queue, &msg)) { - control_msg_destroy(&msg); + while (!sc_vecdeque_is_empty(&controller->queue)) { + struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue); + assert(msg); + sc_control_msg_destroy(msg); } + sc_vecdeque_destroy(&controller->queue); - receiver_destroy(&controller->receiver); + sc_receiver_destroy(&controller->receiver); } bool -controller_push_msg(struct controller *controller, - const struct control_msg *msg) { +sc_controller_push_msg(struct sc_controller *controller, + const struct sc_control_msg *msg) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { - control_msg_log(msg); + sc_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) { - sc_cond_signal(&controller->msg_cond); + bool full = sc_vecdeque_is_full(&controller->queue); + if (!full) { + bool was_empty = sc_vecdeque_is_empty(&controller->queue); + sc_vecdeque_push_noresize(&controller->queue, *msg); + if (was_empty) { + sc_cond_signal(&controller->msg_cond); + } } + // Otherwise (if the queue is full), the msg is discarded + sc_mutex_unlock(&controller->mutex); - return res; + + return !full; } static bool -process_msg(struct controller *controller, - const struct control_msg *msg) { - static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; - size_t length = control_msg_serialize(msg, serialized_msg); +process_msg(struct sc_controller *controller, + const struct sc_control_msg *msg) { + static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; + size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { return false; } - ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); + ssize_t w = + net_send_all(controller->control_socket, serialized_msg, length); return (size_t) w == length; } static int run_controller(void *data) { - struct controller *controller = data; + struct sc_controller *controller = data; for (;;) { sc_mutex_lock(&controller->mutex); - while (!controller->stopped && cbuf_is_empty(&controller->queue)) { + while (!controller->stopped + && sc_vecdeque_is_empty(&controller->queue)) { sc_cond_wait(&controller->msg_cond, &controller->mutex); } if (controller->stopped) { @@ -88,14 +116,13 @@ run_controller(void *data) { sc_mutex_unlock(&controller->mutex); break; } - struct control_msg msg; - bool non_empty = cbuf_take(&controller->queue, &msg); - assert(non_empty); - (void) non_empty; + + assert(!sc_vecdeque_is_empty(&controller->queue)); + struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue); sc_mutex_unlock(&controller->mutex); bool ok = process_msg(controller, &msg); - control_msg_destroy(&msg); + sc_control_msg_destroy(&msg); if (!ok) { LOGD("Could not write msg to socket"); break; @@ -105,18 +132,18 @@ run_controller(void *data) { } bool -controller_start(struct controller *controller) { +sc_controller_start(struct sc_controller *controller) { LOGD("Starting controller thread"); bool ok = sc_thread_create(&controller->thread, run_controller, - "controller", controller); + "scrcpy-ctl", controller); if (!ok) { - LOGC("Could not start controller thread"); + LOGE("Could not start controller thread"); return false; } - if (!receiver_start(&controller->receiver)) { - controller_stop(controller); + if (!sc_receiver_start(&controller->receiver)) { + sc_controller_stop(controller); sc_thread_join(&controller->thread, NULL); return false; } @@ -125,7 +152,7 @@ controller_start(struct controller *controller) { } void -controller_stop(struct controller *controller) { +sc_controller_stop(struct sc_controller *controller) { sc_mutex_lock(&controller->mutex); controller->stopped = true; sc_cond_signal(&controller->msg_cond); @@ -133,7 +160,7 @@ controller_stop(struct controller *controller) { } void -controller_join(struct controller *controller) { +sc_controller_join(struct sc_controller *controller) { sc_thread_join(&controller->thread, NULL); - receiver_join(&controller->receiver); + sc_receiver_join(&controller->receiver); } diff --git a/app/src/controller.h b/app/src/controller.h index c53d0a61..1e44427e 100644 --- a/app/src/controller.h +++ b/app/src/controller.h @@ -1,5 +1,5 @@ -#ifndef CONTROLLER_H -#define CONTROLLER_H +#ifndef SC_CONTROLLER_H +#define SC_CONTROLLER_H #include "common.h" @@ -7,39 +7,45 @@ #include "control_msg.h" #include "receiver.h" -#include "util/cbuf.h" +#include "util/acksync.h" #include "util/net.h" #include "util/thread.h" +#include "util/vecdeque.h" -struct control_msg_queue CBUF(struct control_msg, 64); +struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg); -struct controller { - socket_t control_socket; +struct sc_controller { + sc_socket control_socket; sc_thread thread; sc_mutex mutex; sc_cond msg_cond; bool stopped; - struct control_msg_queue queue; - struct receiver receiver; + struct sc_control_msg_queue queue; + struct sc_receiver receiver; }; bool -controller_init(struct controller *controller, socket_t control_socket); +sc_controller_init(struct sc_controller *controller, sc_socket control_socket); void -controller_destroy(struct controller *controller); +sc_controller_configure(struct sc_controller *controller, + struct sc_acksync *acksync, + struct sc_uhid_devices *uhid_devices); + +void +sc_controller_destroy(struct sc_controller *controller); bool -controller_start(struct controller *controller); +sc_controller_start(struct sc_controller *controller); void -controller_stop(struct controller *controller); +sc_controller_stop(struct sc_controller *controller); void -controller_join(struct controller *controller); +sc_controller_join(struct sc_controller *controller); bool -controller_push_msg(struct controller *controller, - const struct control_msg *msg); +sc_controller_push_msg(struct sc_controller *controller, + const struct sc_control_msg *msg); #endif diff --git a/app/src/coords.h b/app/src/coords.h index 7be6836d..cdabb782 100644 --- a/app/src/coords.h +++ b/app/src/coords.h @@ -3,22 +3,22 @@ #include -struct size { +struct sc_size { uint16_t width; uint16_t height; }; -struct point { +struct sc_point { int32_t x; int32_t y; }; -struct position { +struct sc_position { // The video screen size may be different from the real device screen size, // so store to which size the absolute position apply, to scale it // accordingly. - struct size screen_size; - struct point point; + struct sc_size screen_size; + struct sc_point point; }; #endif diff --git a/app/src/decoder.c b/app/src/decoder.c index aa5018b3..5d42b8b0 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,160 +1,109 @@ #include "decoder.h" +#include #include +#include #include "events.h" -#include "video_buffer.h" #include "trait/frame_sink.h" #include "util/log.h" /** Downcast packet_sink to decoder */ -#define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink) - -static void -decoder_close_first_sinks(struct decoder *decoder, unsigned count) { - while (count) { - struct sc_frame_sink *sink = decoder->sinks[--count]; - sink->ops->close(sink); - } -} - -static inline void -decoder_close_sinks(struct decoder *decoder) { - decoder_close_first_sinks(decoder, decoder->sink_count); -} - -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; -} +#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink) static bool -decoder_open(struct decoder *decoder, const AVCodec *codec) { - decoder->codec_ctx = avcodec_alloc_context3(codec); - if (!decoder->codec_ctx) { - LOGC("Could not allocate decoder context"); - return false; - } - - if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { - LOGE("Could not open codec"); - avcodec_free_context(&decoder->codec_ctx); - return false; - } - +sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) { 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); + LOG_OOM(); return false; } - if (!decoder_open_sinks(decoder)) { - LOGE("Could not open decoder sinks"); + if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) { av_frame_free(&decoder->frame); - avcodec_close(decoder->codec_ctx); - avcodec_free_context(&decoder->codec_ctx); return false; } + decoder->ctx = ctx; + return true; } static void -decoder_close(struct decoder *decoder) { - decoder_close_sinks(decoder); +sc_decoder_close(struct sc_decoder *decoder) { + sc_frame_source_sinks_close(&decoder->frame_source); av_frame_free(&decoder->frame); - avcodec_close(decoder->codec_ctx); - avcodec_free_context(&decoder->codec_ctx); } 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) { +sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { 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); + int ret = avcodec_send_packet(decoder->ctx, packet); if (ret < 0 && ret != AVERROR(EAGAIN)) { - LOGE("Could not send video packet: %d", ret); + LOGE("Decoder '%s': could not send video packet: %d", + decoder->name, ret); return false; } - ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame); - if (!ret) { - // a frame was received - 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; + for (;;) { + ret = avcodec_receive_frame(decoder->ctx, decoder->frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } + + if (ret) { + LOGE("Decoder '%s', could not receive video frame: %d", + decoder->name, ret); + return false; + } + + // a frame was received + bool ok = sc_frame_source_sinks_push(&decoder->frame_source, + decoder->frame); av_frame_unref(decoder->frame); - } else if (ret != AVERROR(EAGAIN)) { - LOGE("Could not receive video frame: %d", ret); - return false; + if (!ok) { + // Error already logged + return false; + } } + return true; } static bool -decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { - struct decoder *decoder = DOWNCAST(sink); - return decoder_open(decoder, codec); +sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { + struct sc_decoder *decoder = DOWNCAST(sink); + return sc_decoder_open(decoder, ctx); } static void -decoder_packet_sink_close(struct sc_packet_sink *sink) { - struct decoder *decoder = DOWNCAST(sink); - decoder_close(decoder); +sc_decoder_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_decoder *decoder = DOWNCAST(sink); + sc_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); +sc_decoder_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_decoder *decoder = DOWNCAST(sink); + return sc_decoder_push(decoder, packet); } void -decoder_init(struct decoder *decoder) { - decoder->sink_count = 0; +sc_decoder_init(struct sc_decoder *decoder, const char *name) { + decoder->name = name; // statically allocated + sc_frame_source_init(&decoder->frame_source); static const struct sc_packet_sink_ops ops = { - .open = decoder_packet_sink_open, - .close = decoder_packet_sink_close, - .push = decoder_packet_sink_push, + .open = sc_decoder_packet_sink_open, + .close = sc_decoder_packet_sink_close, + .push = sc_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 257f751a..ba8903f4 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -1,29 +1,27 @@ -#ifndef DECODER_H -#define DECODER_H +#ifndef SC_DECODER_H +#define SC_DECODER_H #include "common.h" +#include "trait/frame_source.h" #include "trait/packet_sink.h" #include +#include #include -#define DECODER_MAX_SINKS 2 - -struct decoder { +struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait + struct sc_frame_source frame_source; // frame source trait - struct sc_frame_sink *sinks[DECODER_MAX_SINKS]; - unsigned sink_count; + const char *name; // must be statically allocated (e.g. a string literal) - AVCodecContext *codec_ctx; + AVCodecContext *ctx; AVFrame *frame; }; +// The name must be statically allocated (e.g. a string literal) void -decoder_init(struct decoder *decoder); - -void -decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink); +sc_decoder_init(struct sc_decoder *decoder, const char *name); #endif diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c new file mode 100644 index 00000000..f6141b35 --- /dev/null +++ b/app/src/delay_buffer.c @@ -0,0 +1,244 @@ +#include "delay_buffer.h" + +#include +#include + +#include +#include + +#include "util/log.h" + +#define SC_BUFFERING_NDEBUG // comment to debug + +/** Downcast frame_sink to sc_delay_buffer */ +#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink) + +static bool +sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) { + dframe->frame = av_frame_alloc(); + if (!dframe->frame) { + LOG_OOM(); + return false; + } + + if (av_frame_ref(dframe->frame, frame)) { + LOG_OOM(); + av_frame_free(&dframe->frame); + return false; + } + + return true; +} + +static void +sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) { + av_frame_unref(dframe->frame); + av_frame_free(&dframe->frame); +} + +static int +run_buffering(void *data) { + struct sc_delay_buffer *db = data; + + assert(db->delay > 0); + + for (;;) { + sc_mutex_lock(&db->mutex); + + while (!db->stopped && sc_vecdeque_is_empty(&db->queue)) { + sc_cond_wait(&db->queue_cond, &db->mutex); + } + + if (db->stopped) { + sc_mutex_unlock(&db->mutex); + goto stopped; + } + + struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue); + + sc_tick max_deadline = sc_tick_now() + db->delay; + // PTS (written by the server) are expressed in microseconds + sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts); + + bool timed_out = false; + while (!db->stopped && !timed_out) { + sc_tick deadline = sc_clock_to_system_time(&db->clock, pts) + + db->delay; + if (deadline > max_deadline) { + deadline = max_deadline; + } + + timed_out = + !sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline); + } + + bool stopped = db->stopped; + sc_mutex_unlock(&db->mutex); + + if (stopped) { + sc_delayed_frame_destroy(&dframe); + goto stopped; + } + +#ifndef SC_BUFFERING_NDEBUG + LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, + pts, dframe.push_date, sc_tick_now()); +#endif + + bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame); + sc_delayed_frame_destroy(&dframe); + if (!ok) { + LOGE("Delayed frame could not be pushed, stopping"); + sc_mutex_lock(&db->mutex); + // Prevent to push any new frame + db->stopped = true; + sc_mutex_unlock(&db->mutex); + goto stopped; + } + } + +stopped: + assert(db->stopped); + + // Flush queue + while (!sc_vecdeque_is_empty(&db->queue)) { + struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue); + sc_delayed_frame_destroy(dframe); + } + + LOGD("Buffering thread ended"); + + return 0; +} + +static bool +sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink, + const AVCodecContext *ctx) { + struct sc_delay_buffer *db = DOWNCAST(sink); + (void) ctx; + + bool ok = sc_mutex_init(&db->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&db->queue_cond); + if (!ok) { + goto error_destroy_mutex; + } + + ok = sc_cond_init(&db->wait_cond); + if (!ok) { + goto error_destroy_queue_cond; + } + + sc_clock_init(&db->clock); + sc_vecdeque_init(&db->queue); + + if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) { + goto error_destroy_wait_cond; + } + + ok = sc_thread_create(&db->thread, run_buffering, "scrcpy-dbuf", db); + if (!ok) { + LOGE("Could not start buffering thread"); + goto error_close_sinks; + } + + return true; + +error_close_sinks: + sc_frame_source_sinks_close(&db->frame_source); +error_destroy_wait_cond: + sc_cond_destroy(&db->wait_cond); +error_destroy_queue_cond: + sc_cond_destroy(&db->queue_cond); +error_destroy_mutex: + sc_mutex_destroy(&db->mutex); + + return false; +} + +static void +sc_delay_buffer_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_delay_buffer *db = DOWNCAST(sink); + + sc_mutex_lock(&db->mutex); + db->stopped = true; + sc_cond_signal(&db->queue_cond); + sc_cond_signal(&db->wait_cond); + sc_mutex_unlock(&db->mutex); + + sc_thread_join(&db->thread, NULL); + + sc_frame_source_sinks_close(&db->frame_source); + + sc_cond_destroy(&db->wait_cond); + sc_cond_destroy(&db->queue_cond); + sc_mutex_destroy(&db->mutex); +} + +static bool +sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink, + const AVFrame *frame) { + struct sc_delay_buffer *db = DOWNCAST(sink); + + sc_mutex_lock(&db->mutex); + + if (db->stopped) { + sc_mutex_unlock(&db->mutex); + return false; + } + + sc_tick pts = SC_TICK_FROM_US(frame->pts); + sc_clock_update(&db->clock, sc_tick_now(), pts); + sc_cond_signal(&db->wait_cond); + + if (db->first_frame_asap && db->clock.range == 1) { + sc_mutex_unlock(&db->mutex); + return sc_frame_source_sinks_push(&db->frame_source, frame); + } + + struct sc_delayed_frame dframe; + bool ok = sc_delayed_frame_init(&dframe, frame); + if (!ok) { + sc_mutex_unlock(&db->mutex); + return false; + } + +#ifndef SC_BUFFERING_NDEBUG + dframe.push_date = sc_tick_now(); +#endif + + ok = sc_vecdeque_push(&db->queue, dframe); + if (!ok) { + sc_mutex_unlock(&db->mutex); + LOG_OOM(); + return false; + } + + sc_cond_signal(&db->queue_cond); + + sc_mutex_unlock(&db->mutex); + + return true; +} + +void +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, + bool first_frame_asap) { + assert(delay > 0); + + db->delay = delay; + db->first_frame_asap = first_frame_asap; + + sc_frame_source_init(&db->frame_source); + + static const struct sc_frame_sink_ops ops = { + .open = sc_delay_buffer_frame_sink_open, + .close = sc_delay_buffer_frame_sink_close, + .push = sc_delay_buffer_frame_sink_push, + }; + + db->frame_sink.ops = &ops; +} diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h new file mode 100644 index 00000000..53592372 --- /dev/null +++ b/app/src/delay_buffer.h @@ -0,0 +1,60 @@ +#ifndef SC_DELAY_BUFFER_H +#define SC_DELAY_BUFFER_H + +#include "common.h" + +#include + +#include "clock.h" +#include "trait/frame_source.h" +#include "trait/frame_sink.h" +#include "util/thread.h" +#include "util/tick.h" +#include "util/vecdeque.h" + +// forward declarations +typedef struct AVFrame AVFrame; + +struct sc_delayed_frame { + AVFrame *frame; +#ifndef NDEBUG + sc_tick push_date; +#endif +}; + +struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame); + +struct sc_delay_buffer { + struct sc_frame_source frame_source; // frame source trait + struct sc_frame_sink frame_sink; // frame sink trait + + sc_tick delay; + bool first_frame_asap; + + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; + sc_cond wait_cond; + + struct sc_clock clock; + struct sc_delayed_frame_queue queue; + bool stopped; +}; + +struct sc_delay_buffer_callbacks { + bool (*on_new_frame)(struct sc_delay_buffer *db, const AVFrame *frame, + void *userdata); +}; + +/** + * Initialize a delay buffer. + * + * \param delay a (strictly) positive delay + * \param first_frame_asap if true, do not delay the first frame (useful for + a video stream). + */ +void +sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay, + bool first_frame_asap); + +#endif diff --git a/app/src/demuxer.c b/app/src/demuxer.c new file mode 100644 index 00000000..c27ea292 --- /dev/null +++ b/app/src/demuxer.c @@ -0,0 +1,320 @@ +#include "demuxer.h" + +#include +#include +#include +#include + +#include "decoder.h" +#include "events.h" +#include "packet_merger.h" +#include "recorder.h" +#include "util/binary.h" +#include "util/log.h" + +#define SC_PACKET_HEADER_SIZE 12 + +#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) +#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62) + +#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) + +static enum AVCodecID +sc_demuxer_to_avcodec_id(uint32_t codec_id) { +#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII +#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII +#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII +#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII +#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII +#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII +#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII + switch (codec_id) { + case SC_CODEC_ID_H264: + return AV_CODEC_ID_H264; + case SC_CODEC_ID_H265: + return AV_CODEC_ID_HEVC; + case SC_CODEC_ID_AV1: +#ifdef SCRCPY_LAVC_HAS_AV1 + return AV_CODEC_ID_AV1; +#else + LOGE("AV1 not supported by this FFmpeg version"); + return AV_CODEC_ID_NONE; +#endif + case SC_CODEC_ID_OPUS: + return AV_CODEC_ID_OPUS; + case SC_CODEC_ID_AAC: + return AV_CODEC_ID_AAC; + case SC_CODEC_ID_FLAC: + return AV_CODEC_ID_FLAC; + case SC_CODEC_ID_RAW: + return AV_CODEC_ID_PCM_S16LE; + default: + LOGE("Unknown codec id 0x%08" PRIx32, codec_id); + return AV_CODEC_ID_NONE; + } +} + +static bool +sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) { + uint8_t data[4]; + ssize_t r = net_recv_all(demuxer->socket, data, 4); + if (r < 4) { + return false; + } + + *codec_id = sc_read32be(data); + return true; +} + +static bool +sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width, + uint32_t *height) { + uint8_t data[8]; + ssize_t r = net_recv_all(demuxer->socket, data, 8); + if (r < 8) { + return false; + } + + *width = sc_read32be(data); + *height = sc_read32be(data + 4); + return true; +} + +static bool +sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { + // The video and audio streams contain a sequence of raw packets (as + // provided by MediaCodec), each prefixed with a "meta" header. + // + // The "meta" header length is 12 bytes: + // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + // <-------------> <-----> <-----------------------------... + // PTS packet raw packet + // size + // + // It is followed by bytes containing the packet/frame. + // + // The most significant bits of the PTS are used for packet flags: + // + // byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 + // CK...... ........ ........ ........ ........ ........ ........ ........ + // ^^<-------------------------------------------------------------------> + // || PTS + // | `- key frame + // `-- config packet + + uint8_t header[SC_PACKET_HEADER_SIZE]; + ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE); + if (r < SC_PACKET_HEADER_SIZE) { + return false; + } + + uint64_t pts_flags = sc_read64be(header); + uint32_t len = sc_read32be(&header[8]); + assert(len); + + if (av_new_packet(packet, len)) { + LOG_OOM(); + return false; + } + + r = net_recv_all(demuxer->socket, packet->data, len); + if (r < 0 || ((uint32_t) r) < len) { + av_packet_unref(packet); + return false; + } + + if (pts_flags & SC_PACKET_FLAG_CONFIG) { + packet->pts = AV_NOPTS_VALUE; + } else { + packet->pts = pts_flags & SC_PACKET_PTS_MASK; + } + + if (pts_flags & SC_PACKET_FLAG_KEY_FRAME) { + packet->flags |= AV_PKT_FLAG_KEY; + } + + packet->dts = packet->pts; + return true; +} + +static int +run_demuxer(void *data) { + struct sc_demuxer *demuxer = data; + + // Flag to report end-of-stream (i.e. device disconnected) + enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR; + + uint32_t raw_codec_id; + bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id); + if (!ok) { + LOGE("Demuxer '%s': stream disabled due to connection error", + demuxer->name); + goto end; + } + + if (raw_codec_id == 0) { + LOGW("Demuxer '%s': stream explicitly disabled by the device", + demuxer->name); + sc_packet_source_sinks_disable(&demuxer->packet_source); + status = SC_DEMUXER_STATUS_DISABLED; + goto end; + } + + if (raw_codec_id == 1) { + LOGE("Demuxer '%s': stream configuration error on the device", + demuxer->name); + goto end; + } + + enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id); + if (codec_id == AV_CODEC_ID_NONE) { + LOGE("Demuxer '%s': stream disabled due to unsupported codec", + demuxer->name); + sc_packet_source_sinks_disable(&demuxer->packet_source); + goto end; + } + + const AVCodec *codec = avcodec_find_decoder(codec_id); + if (!codec) { + LOGE("Demuxer '%s': stream disabled due to missing decoder", + demuxer->name); + sc_packet_source_sinks_disable(&demuxer->packet_source); + goto end; + } + + AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + LOG_OOM(); + goto end; + } + + codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; + + if (codec->type == AVMEDIA_TYPE_VIDEO) { + uint32_t width; + uint32_t height; + ok = sc_demuxer_recv_video_size(demuxer, &width, &height); + if (!ok) { + goto finally_free_context; + } + + codec_ctx->width = width; + codec_ctx->height = height; + codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; + } else { + // Hardcoded audio properties +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO; +#else + codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO; + codec_ctx->channels = 2; +#endif + codec_ctx->sample_rate = 48000; + + if (raw_codec_id == SC_CODEC_ID_FLAC) { + // The sample_fmt is not set by the FLAC decoder + codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16; + } + } + + if (avcodec_open2(codec_ctx, codec, NULL) < 0) { + LOGE("Demuxer '%s': could not open codec", demuxer->name); + goto finally_free_context; + } + + if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) { + goto finally_free_context; + } + + // Config packets must be merged with the next non-config packet only for + // H.26x + bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264 + || raw_codec_id == SC_CODEC_ID_H265; + + struct sc_packet_merger merger; + + if (must_merge_config_packet) { + sc_packet_merger_init(&merger); + } + + AVPacket *packet = av_packet_alloc(); + if (!packet) { + LOG_OOM(); + goto finally_close_sinks; + } + + for (;;) { + bool ok = sc_demuxer_recv_packet(demuxer, packet); + if (!ok) { + // end of stream + status = SC_DEMUXER_STATUS_EOS; + break; + } + + if (must_merge_config_packet) { + // Prepend any config packet to the next media packet + ok = sc_packet_merger_merge(&merger, packet); + if (!ok) { + av_packet_unref(packet); + break; + } + } + + ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet); + av_packet_unref(packet); + if (!ok) { + // The sink already logged its concrete error + break; + } + } + + LOGD("Demuxer '%s': end of frames", demuxer->name); + + if (must_merge_config_packet) { + sc_packet_merger_destroy(&merger); + } + + av_packet_free(&packet); +finally_close_sinks: + sc_packet_source_sinks_close(&demuxer->packet_source); +finally_free_context: + // This also calls avcodec_close() internally + avcodec_free_context(&codec_ctx); +end: + demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata); + + return 0; +} + +void +sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, + const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { + assert(socket != SC_SOCKET_NONE); + + demuxer->name = name; // statically allocated + demuxer->socket = socket; + sc_packet_source_init(&demuxer->packet_source); + + assert(cbs && cbs->on_ended); + + demuxer->cbs = cbs; + demuxer->cbs_userdata = cbs_userdata; +} + +bool +sc_demuxer_start(struct sc_demuxer *demuxer) { + LOGD("Demuxer '%s': starting thread", demuxer->name); + + bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", + demuxer); + if (!ok) { + LOGE("Demuxer '%s': could not start thread", demuxer->name); + return false; + } + return true; +} + +void +sc_demuxer_join(struct sc_demuxer *demuxer) { + sc_thread_join(&demuxer->thread, NULL); +} diff --git a/app/src/demuxer.h b/app/src/demuxer.h new file mode 100644 index 00000000..5587d12d --- /dev/null +++ b/app/src/demuxer.h @@ -0,0 +1,50 @@ +#ifndef SC_DEMUXER_H +#define SC_DEMUXER_H + +#include "common.h" + +#include +#include +#include +#include + +#include "trait/packet_source.h" +#include "trait/packet_sink.h" +#include "util/net.h" +#include "util/thread.h" + +struct sc_demuxer { + struct sc_packet_source packet_source; // packet source trait + + const char *name; // must be statically allocated (e.g. a string literal) + + sc_socket socket; + sc_thread thread; + + const struct sc_demuxer_callbacks *cbs; + void *cbs_userdata; +}; + +enum sc_demuxer_status { + SC_DEMUXER_STATUS_EOS, + SC_DEMUXER_STATUS_DISABLED, + SC_DEMUXER_STATUS_ERROR, +}; + +struct sc_demuxer_callbacks { + void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status, + void *userdata); +}; + +// The name must be statically allocated (e.g. a string literal) +void +sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket, + const struct sc_demuxer_callbacks *cbs, void *cbs_userdata); + +bool +sc_demuxer_start(struct sc_demuxer *demuxer); + +void +sc_demuxer_join(struct sc_demuxer *demuxer); + +#endif diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 827f4213..7621c040 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -1,29 +1,33 @@ #include "device_msg.h" +#include #include #include -#include "util/buffer_util.h" +#include "util/binary.h" #include "util/log.h" ssize_t -device_msg_deserialize(const unsigned char *buf, size_t len, - struct device_msg *msg) { - if (len < 5) { - // at least type + empty string length - return 0; // not available +sc_device_msg_deserialize(const uint8_t *buf, size_t len, + struct sc_device_msg *msg) { + if (!len) { + return 0; // no message } msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { - size_t clipboard_len = buffer_read32be(&buf[1]); + if (len < 5) { + // at least type + empty string length + return 0; // no complete message + } + size_t clipboard_len = sc_read32be(&buf[1]); if (clipboard_len > len - 5) { - return 0; // not available + return 0; // no complete message } char *text = malloc(clipboard_len + 1); if (!text) { - LOGW("Could not allocate text for clipboard"); + LOG_OOM(); return -1; } if (clipboard_len) { @@ -34,6 +38,39 @@ device_msg_deserialize(const unsigned char *buf, size_t len, msg->clipboard.text = text; return 5 + clipboard_len; } + case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { + if (len < 9) { + return 0; // no complete message + } + uint64_t sequence = sc_read64be(&buf[1]); + msg->ack_clipboard.sequence = sequence; + return 9; + } + case DEVICE_MSG_TYPE_UHID_OUTPUT: { + if (len < 5) { + // at least id + size + return 0; // not available + } + uint16_t id = sc_read16be(&buf[1]); + size_t size = sc_read16be(&buf[3]); + if (size < len - 5) { + return 0; // not available + } + uint8_t *data = malloc(size); + if (!data) { + LOG_OOM(); + return -1; + } + if (size) { + memcpy(data, &buf[5], size); + } + + msg->uhid_output.id = id; + msg->uhid_output.size = size; + msg->uhid_output.data = data; + + return 5 + size; + } default: LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover @@ -41,8 +78,16 @@ 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) { - free(msg->clipboard.text); +sc_device_msg_destroy(struct sc_device_msg *msg) { + switch (msg->type) { + case DEVICE_MSG_TYPE_CLIPBOARD: + free(msg->clipboard.text); + break; + case DEVICE_MSG_TYPE_UHID_OUTPUT: + free(msg->uhid_output.data); + break; + default: + // nothing to do + break; } } diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 888d9216..86b2ccb7 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -1,5 +1,5 @@ -#ifndef DEVICEMSG_H -#define DEVICEMSG_H +#ifndef SC_DEVICEMSG_H +#define SC_DEVICEMSG_H #include "common.h" @@ -11,25 +11,35 @@ // type: 1 byte; length: 4 bytes #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) -enum device_msg_type { +enum sc_device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, + DEVICE_MSG_TYPE_ACK_CLIPBOARD, + DEVICE_MSG_TYPE_UHID_OUTPUT, }; -struct device_msg { - enum device_msg_type type; +struct sc_device_msg { + enum sc_device_msg_type type; union { struct { char *text; // owned, to be freed by free() } clipboard; + struct { + uint64_t sequence; + } ack_clipboard; + struct { + uint16_t id; + uint16_t size; + uint8_t *data; // owned, to be freed by free() + } uhid_output; }; }; // return the number of bytes consumed (0 for no msg available, -1 on error) ssize_t -device_msg_deserialize(const unsigned char *buf, size_t len, - struct device_msg *msg); +sc_device_msg_deserialize(const uint8_t *buf, size_t len, + struct sc_device_msg *msg); void -device_msg_destroy(struct device_msg *msg); +sc_device_msg_destroy(struct sc_device_msg *msg); #endif diff --git a/app/src/display.c b/app/src/display.c new file mode 100644 index 00000000..c8df615d --- /dev/null +++ b/app/src/display.c @@ -0,0 +1,286 @@ +#include "display.h" + +#include + +#include "util/log.h" + +bool +sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) { + display->renderer = + SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (!display->renderer) { + LOGE("Could not create renderer: %s", SDL_GetError()); + return false; + } + + SDL_RendererInfo renderer_info; + int r = SDL_GetRendererInfo(display->renderer, &renderer_info); + const char *renderer_name = r ? NULL : renderer_info.name; + LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); + + display->mipmaps = false; + + // starts with "opengl" + bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); + if (use_opengl) { + +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + // Persuade macOS to give us something better than OpenGL 2.1. + // If we create a Core Profile context, we get the best OpenGL version. + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, + SDL_GL_CONTEXT_PROFILE_CORE); + + LOGD("Creating OpenGL Core Profile context"); + display->gl_context = SDL_GL_CreateContext(window); + if (!display->gl_context) { + LOGE("Could not create OpenGL context: %s", SDL_GetError()); + SDL_DestroyRenderer(display->renderer); + return false; + } +#endif + + struct sc_opengl *gl = &display->gl; + sc_opengl_init(gl); + + LOGI("OpenGL version: %s", gl->version); + + if (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"); + display->mipmaps = true; + } else { + LOGW("Trilinear filtering disabled " + "(OpenGL 3.0+ or ES 2.0+ required)"); + } + } else { + LOGI("Trilinear filtering disabled"); + } + } else if (mipmaps) { + LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); + } + + display->texture = NULL; + display->pending.flags = 0; + display->pending.frame = NULL; + + return true; +} + +void +sc_display_destroy(struct sc_display *display) { + if (display->pending.frame) { + av_frame_free(&display->pending.frame); + } +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + SDL_GL_DeleteContext(display->gl_context); +#endif + if (display->texture) { + SDL_DestroyTexture(display->texture); + } + SDL_DestroyRenderer(display->renderer); +} + +static SDL_Texture * +sc_display_create_texture(struct sc_display *display, + struct sc_size size) { + SDL_Renderer *renderer = display->renderer; + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, + SDL_TEXTUREACCESS_STREAMING, + size.width, size.height); + if (!texture) { + LOGD("Could not create texture: %s", SDL_GetError()); + return NULL; + } + + if (display->mipmaps) { + struct sc_opengl *gl = &display->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; +} + +static inline void +sc_display_set_pending_size(struct sc_display *display, struct sc_size size) { + assert(!display->texture); + display->pending.size = size; + display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE; +} + +static bool +sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) { + if (!display->pending.frame) { + display->pending.frame = av_frame_alloc(); + if (!display->pending.frame) { + LOG_OOM(); + return false; + } + } + + int r = av_frame_ref(display->pending.frame, frame); + if (r) { + LOGE("Could not ref frame: %d", r); + return false; + } + + display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME; + + return true; +} + +static bool +sc_display_apply_pending(struct sc_display *display) { + if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) { + assert(!display->texture); + display->texture = + sc_display_create_texture(display, display->pending.size); + if (!display->texture) { + return false; + } + + display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE; + } + + if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) { + assert(display->pending.frame); + bool ok = sc_display_update_texture(display, display->pending.frame); + if (!ok) { + return false; + } + + av_frame_unref(display->pending.frame); + display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME; + } + + return true; +} + +static bool +sc_display_set_texture_size_internal(struct sc_display *display, + struct sc_size size) { + assert(size.width && size.height); + + if (display->texture) { + SDL_DestroyTexture(display->texture); + } + + display->texture = sc_display_create_texture(display, size); + if (!display->texture) { + return false; + } + + LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height); + return true; +} + +enum sc_display_result +sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { + bool ok = sc_display_set_texture_size_internal(display, size); + if (!ok) { + sc_display_set_pending_size(display, size); + return SC_DISPLAY_RESULT_PENDING; + + } + + return SC_DISPLAY_RESULT_OK; +} + +static bool +sc_display_update_texture_internal(struct sc_display *display, + const AVFrame *frame) { + int ret = SDL_UpdateYUVTexture(display->texture, NULL, + frame->data[0], frame->linesize[0], + frame->data[1], frame->linesize[1], + frame->data[2], frame->linesize[2]); + if (ret) { + LOGD("Could not update texture: %s", SDL_GetError()); + return false; + } + + if (display->mipmaps) { + SDL_GL_BindTexture(display->texture, NULL, NULL); + display->gl.GenerateMipmap(GL_TEXTURE_2D); + SDL_GL_UnbindTexture(display->texture); + } + + return true; +} + +enum sc_display_result +sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { + bool ok = sc_display_update_texture_internal(display, frame); + if (!ok) { + ok = sc_display_set_pending_frame(display, frame); + if (!ok) { + LOGE("Could not set pending frame"); + return SC_DISPLAY_RESULT_ERROR; + } + + return SC_DISPLAY_RESULT_PENDING; + } + + return SC_DISPLAY_RESULT_OK; +} + +enum sc_display_result +sc_display_render(struct sc_display *display, const SDL_Rect *geometry, + enum sc_orientation orientation) { + SDL_RenderClear(display->renderer); + + if (display->pending.flags) { + bool ok = sc_display_apply_pending(display); + if (!ok) { + return SC_DISPLAY_RESULT_PENDING; + } + } + + SDL_Renderer *renderer = display->renderer; + SDL_Texture *texture = display->texture; + + if (orientation == SC_ORIENTATION_0) { + int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); + if (ret) { + LOGE("Could not render texture: %s", SDL_GetError()); + return SC_DISPLAY_RESULT_ERROR; + } + } else { + unsigned cw_rotation = sc_orientation_get_rotation(orientation); + double angle = 90 * cw_rotation; + + const SDL_Rect *dstrect = NULL; + SDL_Rect rect; + if (sc_orientation_is_swap(orientation)) { + rect.x = geometry->x + (geometry->w - geometry->h) / 2; + rect.y = geometry->y + (geometry->h - geometry->w) / 2; + rect.w = geometry->h; + rect.h = geometry->w; + dstrect = ▭ + } else { + dstrect = geometry; + } + + SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) + ? SDL_FLIP_HORIZONTAL : 0; + + int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, + NULL, flip); + if (ret) { + LOGE("Could not render texture: %s", SDL_GetError()); + return SC_DISPLAY_RESULT_ERROR; + } + } + + SDL_RenderPresent(display->renderer); + return SC_DISPLAY_RESULT_OK; +} diff --git a/app/src/display.h b/app/src/display.h new file mode 100644 index 00000000..643ce73c --- /dev/null +++ b/app/src/display.h @@ -0,0 +1,60 @@ +#ifndef SC_DISPLAY_H +#define SC_DISPLAY_H + +#include "common.h" + +#include +#include +#include + +#include "coords.h" +#include "opengl.h" +#include "options.h" + +#ifdef __APPLE__ +# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE +#endif + +struct sc_display { + SDL_Renderer *renderer; + SDL_Texture *texture; + + struct sc_opengl gl; +#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE + SDL_GLContext *gl_context; +#endif + + bool mipmaps; + + struct { +#define SC_DISPLAY_PENDING_FLAG_SIZE 1 +#define SC_DISPLAY_PENDING_FLAG_FRAME 2 + int8_t flags; + struct sc_size size; + AVFrame *frame; + } pending; +}; + +enum sc_display_result { + SC_DISPLAY_RESULT_OK, + SC_DISPLAY_RESULT_PENDING, + SC_DISPLAY_RESULT_ERROR, +}; + +bool +sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps); + +void +sc_display_destroy(struct sc_display *display); + +enum sc_display_result +sc_display_set_texture_size(struct sc_display *display, struct sc_size size); + +enum sc_display_result +sc_display_update_texture(struct sc_display *display, const AVFrame *frame); + +enum sc_display_result +sc_display_render(struct sc_display *display, const SDL_Rect *geometry, + enum sc_orientation orientation); + +#endif diff --git a/app/src/event_converter.c b/app/src/event_converter.c deleted file mode 100644 index a3c2da89..00000000 --- a/app/src/event_converter.c +++ /dev/null @@ -1,195 +0,0 @@ -#include "event_converter.h" - -#define MAP(FROM, TO) case FROM: *to = TO; return true -#define FAIL default: return false - -bool -convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { - switch (from) { - MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN); - MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP); - FAIL; - } -} - -static enum android_metastate -autocomplete_metastate(enum android_metastate metastate) { - // fill dependent flags - if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { - metastate |= AMETA_SHIFT_ON; - } - if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { - metastate |= AMETA_CTRL_ON; - } - if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { - metastate |= AMETA_ALT_ON; - } - if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { - metastate |= AMETA_META_ON; - } - - return metastate; -} - -enum android_metastate -convert_meta_state(SDL_Keymod mod) { - enum android_metastate metastate = 0; - if (mod & KMOD_LSHIFT) { - metastate |= AMETA_SHIFT_LEFT_ON; - } - if (mod & KMOD_RSHIFT) { - metastate |= AMETA_SHIFT_RIGHT_ON; - } - if (mod & KMOD_LCTRL) { - metastate |= AMETA_CTRL_LEFT_ON; - } - if (mod & KMOD_RCTRL) { - metastate |= AMETA_CTRL_RIGHT_ON; - } - if (mod & KMOD_LALT) { - metastate |= AMETA_ALT_LEFT_ON; - } - if (mod & KMOD_RALT) { - metastate |= AMETA_ALT_RIGHT_ON; - } - if (mod & KMOD_LGUI) { // Windows key - metastate |= AMETA_META_LEFT_ON; - } - if (mod & KMOD_RGUI) { // Windows key - metastate |= AMETA_META_RIGHT_ON; - } - if (mod & KMOD_NUM) { - metastate |= AMETA_NUM_LOCK_ON; - } - if (mod & KMOD_CAPS) { - metastate |= AMETA_CAPS_LOCK_ON; - } - if (mod & KMOD_MODE) { // Alt Gr - // no mapping? - } - - // fill the dependent fields - return autocomplete_metastate(metastate); -} - -bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text) { - switch (from) { - MAP(SDLK_RETURN, AKEYCODE_ENTER); - MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER); - MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE); - MAP(SDLK_BACKSPACE, AKEYCODE_DEL); - MAP(SDLK_TAB, AKEYCODE_TAB); - MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP); - MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL); - MAP(SDLK_HOME, AKEYCODE_MOVE_HOME); - MAP(SDLK_END, AKEYCODE_MOVE_END); - MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN); - MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT); - 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 (!(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; - } - - if (mod & (KMOD_LALT | KMOD_RALT | KMOD_LGUI | KMOD_RGUI)) { - return false; - } - // if ALT and META are not pressed, also handle letters and space - switch (from) { - MAP(SDLK_a, AKEYCODE_A); - MAP(SDLK_b, AKEYCODE_B); - MAP(SDLK_c, AKEYCODE_C); - MAP(SDLK_d, AKEYCODE_D); - MAP(SDLK_e, AKEYCODE_E); - MAP(SDLK_f, AKEYCODE_F); - MAP(SDLK_g, AKEYCODE_G); - MAP(SDLK_h, AKEYCODE_H); - MAP(SDLK_i, AKEYCODE_I); - MAP(SDLK_j, AKEYCODE_J); - MAP(SDLK_k, AKEYCODE_K); - MAP(SDLK_l, AKEYCODE_L); - MAP(SDLK_m, AKEYCODE_M); - MAP(SDLK_n, AKEYCODE_N); - MAP(SDLK_o, AKEYCODE_O); - MAP(SDLK_p, AKEYCODE_P); - MAP(SDLK_q, AKEYCODE_Q); - MAP(SDLK_r, AKEYCODE_R); - MAP(SDLK_s, AKEYCODE_S); - MAP(SDLK_t, AKEYCODE_T); - MAP(SDLK_u, AKEYCODE_U); - MAP(SDLK_v, AKEYCODE_V); - MAP(SDLK_w, AKEYCODE_W); - MAP(SDLK_x, AKEYCODE_X); - MAP(SDLK_y, AKEYCODE_Y); - MAP(SDLK_z, AKEYCODE_Z); - MAP(SDLK_SPACE, AKEYCODE_SPACE); - FAIL; - } -} - -enum android_motionevent_buttons -convert_mouse_buttons(uint32_t state) { - enum android_motionevent_buttons buttons = 0; - if (state & SDL_BUTTON_LMASK) { - buttons |= AMOTION_EVENT_BUTTON_PRIMARY; - } - if (state & SDL_BUTTON_RMASK) { - buttons |= AMOTION_EVENT_BUTTON_SECONDARY; - } - if (state & SDL_BUTTON_MMASK) { - buttons |= AMOTION_EVENT_BUTTON_TERTIARY; - } - if (state & SDL_BUTTON_X1MASK) { - buttons |= AMOTION_EVENT_BUTTON_BACK; - } - if (state & SDL_BUTTON_X2MASK) { - buttons |= AMOTION_EVENT_BUTTON_FORWARD; - } - return buttons; -} - -bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} - -bool -convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { - switch (from) { - MAP(SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE); - MAP(SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN); - MAP(SDL_FINGERUP, AMOTION_EVENT_ACTION_UP); - FAIL; - } -} diff --git a/app/src/event_converter.h b/app/src/event_converter.h deleted file mode 100644 index d28e9fdc..00000000 --- a/app/src/event_converter.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef CONVERT_H -#define CONVERT_H - -#include "common.h" - -#include -#include - -#include "control_msg.h" - -bool -convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to); - -enum android_metastate -convert_meta_state(SDL_Keymod mod); - -bool -convert_keycode(SDL_Keycode from, enum android_keycode *to, uint16_t mod, - bool prefer_text); - -enum android_motionevent_buttons -convert_mouse_buttons(uint32_t state); - -bool -convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to); - -bool -convert_touch_action(SDL_EventType from, enum android_motionevent_action *to); - -#endif diff --git a/app/src/events.h b/app/src/events.h index a4d6f3df..8bfa2582 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -1,2 +1,9 @@ -#define EVENT_NEW_FRAME SDL_USEREVENT -#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) +#define SC_EVENT_NEW_FRAME SDL_USEREVENT +#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1) +#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) +#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) +#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) +#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5) +#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6) +#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7) +#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8) diff --git a/app/src/file_handler.c b/app/src/file_handler.c deleted file mode 100644 index 27fe6fa3..00000000 --- a/app/src/file_handler.c +++ /dev/null @@ -1,197 +0,0 @@ -#include "file_handler.h" - -#include -#include - -#include "adb.h" -#include "util/log.h" - -#define DEFAULT_PUSH_TARGET "/sdcard/Download/" - -static void -file_handler_request_destroy(struct file_handler_request *req) { - free(req->file); -} - -bool -file_handler_init(struct file_handler *file_handler, const char *serial, - const char *push_target) { - - cbuf_init(&file_handler->queue); - - bool ok = sc_mutex_init(&file_handler->mutex); - if (!ok) { - return false; - } - - ok = sc_cond_init(&file_handler->event_cond); - if (!ok) { - sc_mutex_destroy(&file_handler->mutex); - return false; - } - - if (serial) { - file_handler->serial = strdup(serial); - if (!file_handler->serial) { - LOGW("Could not strdup serial"); - sc_cond_destroy(&file_handler->event_cond); - sc_mutex_destroy(&file_handler->mutex); - return false; - } - } else { - file_handler->serial = NULL; - } - - // lazy initialization - file_handler->initialized = false; - - file_handler->stopped = false; - file_handler->current_process = PROCESS_NONE; - - file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; - - return true; -} - -void -file_handler_destroy(struct file_handler *file_handler) { - 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)) { - file_handler_request_destroy(&req); - } -} - -static process_t -install_apk(const char *serial, const char *file) { - return adb_install(serial, file); -} - -static process_t -push_file(const char *serial, const char *file, const char *push_target) { - return adb_push(serial, file, push_target); -} - -bool -file_handler_request(struct file_handler *file_handler, - file_handler_action_t action, char *file) { - // start file_handler if it's used for the first time - if (!file_handler->initialized) { - if (!file_handler_start(file_handler)) { - return false; - } - file_handler->initialized = true; - } - - LOGI("Request to %s %s", action == ACTION_INSTALL_APK ? "install" : "push", - file); - struct file_handler_request req = { - .action = action, - .file = file, - }; - - 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) { - sc_cond_signal(&file_handler->event_cond); - } - sc_mutex_unlock(&file_handler->mutex); - return res; -} - -static int -run_file_handler(void *data) { - struct file_handler *file_handler = data; - - for (;;) { - sc_mutex_lock(&file_handler->mutex); - file_handler->current_process = PROCESS_NONE; - while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { - sc_cond_wait(&file_handler->event_cond, &file_handler->mutex); - } - if (file_handler->stopped) { - // stop immediately, do not process further events - sc_mutex_unlock(&file_handler->mutex); - break; - } - struct file_handler_request req; - bool non_empty = cbuf_take(&file_handler->queue, &req); - assert(non_empty); - (void) non_empty; - - process_t process; - if (req.action == ACTION_INSTALL_APK) { - LOGI("Installing %s...", req.file); - process = install_apk(file_handler->serial, req.file); - } else { - LOGI("Pushing %s...", req.file); - process = push_file(file_handler->serial, req.file, - file_handler->push_target); - } - file_handler->current_process = process; - sc_mutex_unlock(&file_handler->mutex); - - if (req.action == ACTION_INSTALL_APK) { - 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", false)) { - LOGI("%s successfully pushed to %s", req.file, - file_handler->push_target); - } else { - LOGE("Failed to push %s to %s", req.file, - file_handler->push_target); - } - } - - 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; -} - -bool -file_handler_start(struct file_handler *file_handler) { - LOGD("Starting 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; - } - - return true; -} - -void -file_handler_stop(struct file_handler *file_handler) { - sc_mutex_lock(&file_handler->mutex); - file_handler->stopped = true; - sc_cond_signal(&file_handler->event_cond); - if (file_handler->current_process != PROCESS_NONE) { - if (!process_terminate(file_handler->current_process)) { - LOGW("Could not terminate push/install process"); - } - } - sc_mutex_unlock(&file_handler->mutex); -} - -void -file_handler_join(struct file_handler *file_handler) { - sc_thread_join(&file_handler->thread, NULL); -} diff --git a/app/src/file_handler.h b/app/src/file_handler.h deleted file mode 100644 index fe1d1804..00000000 --- a/app/src/file_handler.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef FILE_HANDLER_H -#define FILE_HANDLER_H - -#include "common.h" - -#include - -#include "adb.h" -#include "util/cbuf.h" -#include "util/thread.h" - -typedef enum { - ACTION_INSTALL_APK, - ACTION_PUSH_FILE, -} file_handler_action_t; - -struct file_handler_request { - file_handler_action_t action; - char *file; -}; - -struct file_handler_request_queue CBUF(struct file_handler_request, 16); - -struct file_handler { - char *serial; - const char *push_target; - sc_thread thread; - sc_mutex mutex; - sc_cond event_cond; - bool stopped; - bool initialized; - process_t current_process; - struct file_handler_request_queue queue; -}; - -bool -file_handler_init(struct file_handler *file_handler, const char *serial, - const char *push_target); - -void -file_handler_destroy(struct file_handler *file_handler); - -bool -file_handler_start(struct file_handler *file_handler); - -void -file_handler_stop(struct file_handler *file_handler); - -void -file_handler_join(struct file_handler *file_handler); - -// take ownership of file, and will free() it -bool -file_handler_request(struct file_handler *file_handler, - file_handler_action_t action, - char *file); - -#endif diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c new file mode 100644 index 00000000..06911052 --- /dev/null +++ b/app/src/file_pusher.c @@ -0,0 +1,189 @@ +#include "file_pusher.h" + +#include +#include + +#include "adb/adb.h" +#include "util/log.h" +#include "util/process_intr.h" + +#define DEFAULT_PUSH_TARGET "/sdcard/Download/" + +static void +sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { + free(req->file); +} + +bool +sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, + const char *push_target) { + assert(serial); + + sc_vecdeque_init(&fp->queue); + + bool ok = sc_mutex_init(&fp->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&fp->event_cond); + if (!ok) { + sc_mutex_destroy(&fp->mutex); + return false; + } + + ok = sc_intr_init(&fp->intr); + if (!ok) { + sc_cond_destroy(&fp->event_cond); + sc_mutex_destroy(&fp->mutex); + return false; + } + + fp->serial = strdup(serial); + if (!fp->serial) { + LOG_OOM(); + sc_intr_destroy(&fp->intr); + sc_cond_destroy(&fp->event_cond); + sc_mutex_destroy(&fp->mutex); + return false; + } + + // lazy initialization + fp->initialized = false; + + fp->stopped = false; + + fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; + + return true; +} + +void +sc_file_pusher_destroy(struct sc_file_pusher *fp) { + sc_cond_destroy(&fp->event_cond); + sc_mutex_destroy(&fp->mutex); + sc_intr_destroy(&fp->intr); + free(fp->serial); + + while (!sc_vecdeque_is_empty(&fp->queue)) { + struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue); + assert(req); + sc_file_pusher_request_destroy(req); + } +} + +bool +sc_file_pusher_request(struct sc_file_pusher *fp, + enum sc_file_pusher_action action, char *file) { + // start file_pusher if it's used for the first time + if (!fp->initialized) { + if (!sc_file_pusher_start(fp)) { + return false; + } + fp->initialized = true; + } + + LOGI("Request to %s %s", action == SC_FILE_PUSHER_ACTION_INSTALL_APK + ? "install" : "push", + file); + struct sc_file_pusher_request req = { + .action = action, + .file = file, + }; + + sc_mutex_lock(&fp->mutex); + bool was_empty = sc_vecdeque_is_empty(&fp->queue); + bool res = sc_vecdeque_push(&fp->queue, req); + if (!res) { + LOG_OOM(); + sc_mutex_unlock(&fp->mutex); + return false; + } + + if (was_empty) { + sc_cond_signal(&fp->event_cond); + } + sc_mutex_unlock(&fp->mutex); + + return true; +} + +static int +run_file_pusher(void *data) { + struct sc_file_pusher *fp = data; + struct sc_intr *intr = &fp->intr; + + const char *serial = fp->serial; + assert(serial); + + const char *push_target = fp->push_target; + assert(push_target); + + for (;;) { + sc_mutex_lock(&fp->mutex); + while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) { + sc_cond_wait(&fp->event_cond, &fp->mutex); + } + if (fp->stopped) { + // stop immediately, do not process further events + sc_mutex_unlock(&fp->mutex); + break; + } + + assert(!sc_vecdeque_is_empty(&fp->queue)); + struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue); + sc_mutex_unlock(&fp->mutex); + + if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) { + LOGI("Installing %s...", req.file); + bool ok = sc_adb_install(intr, serial, req.file, 0); + if (ok) { + LOGI("%s successfully installed", req.file); + } else { + LOGE("Failed to install %s", req.file); + } + } else { + LOGI("Pushing %s...", req.file); + bool ok = sc_adb_push(intr, serial, req.file, push_target, 0); + if (ok) { + LOGI("%s successfully pushed to %s", req.file, push_target); + } else { + LOGE("Failed to push %s to %s", req.file, push_target); + } + } + + sc_file_pusher_request_destroy(&req); + } + return 0; +} + +bool +sc_file_pusher_start(struct sc_file_pusher *fp) { + LOGD("Starting file_pusher thread"); + + bool ok = sc_thread_create(&fp->thread, run_file_pusher, "scrcpy-file", fp); + if (!ok) { + LOGE("Could not start file_pusher thread"); + return false; + } + + return true; +} + +void +sc_file_pusher_stop(struct sc_file_pusher *fp) { + if (fp->initialized) { + sc_mutex_lock(&fp->mutex); + fp->stopped = true; + sc_cond_signal(&fp->event_cond); + sc_intr_interrupt(&fp->intr); + sc_mutex_unlock(&fp->mutex); + } +} + +void +sc_file_pusher_join(struct sc_file_pusher *fp) { + if (fp->initialized) { + sc_thread_join(&fp->thread, NULL); + } +} diff --git a/app/src/file_pusher.h b/app/src/file_pusher.h new file mode 100644 index 00000000..0ffb3721 --- /dev/null +++ b/app/src/file_pusher.h @@ -0,0 +1,58 @@ +#ifndef SC_FILE_PUSHER_H +#define SC_FILE_PUSHER_H + +#include "common.h" + +#include + +#include "util/intr.h" +#include "util/thread.h" +#include "util/vecdeque.h" + +enum sc_file_pusher_action { + SC_FILE_PUSHER_ACTION_INSTALL_APK, + SC_FILE_PUSHER_ACTION_PUSH_FILE, +}; + +struct sc_file_pusher_request { + enum sc_file_pusher_action action; + char *file; +}; + +struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request); + +struct sc_file_pusher { + char *serial; + const char *push_target; + sc_thread thread; + sc_mutex mutex; + sc_cond event_cond; + bool stopped; + bool initialized; + struct sc_file_pusher_request_queue queue; + + struct sc_intr intr; +}; + +bool +sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, + const char *push_target); + +void +sc_file_pusher_destroy(struct sc_file_pusher *fp); + +bool +sc_file_pusher_start(struct sc_file_pusher *fp); + +void +sc_file_pusher_stop(struct sc_file_pusher *fp); + +void +sc_file_pusher_join(struct sc_file_pusher *fp); + +// take ownership of file, and will free() it +bool +sc_file_pusher_request(struct sc_file_pusher *fp, + enum sc_file_pusher_action action, char *file); + +#endif diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index c92d4140..dd4ae1da 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -4,10 +4,10 @@ #include "util/log.h" -#define FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) +#define SC_FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool -fps_counter_init(struct fps_counter *counter) { +sc_fps_counter_init(struct sc_fps_counter *counter) { bool ok = sc_mutex_init(&counter->mutex); if (!ok) { return false; @@ -27,26 +27,26 @@ fps_counter_init(struct fps_counter *counter) { } void -fps_counter_destroy(struct fps_counter *counter) { +sc_fps_counter_destroy(struct sc_fps_counter *counter) { sc_cond_destroy(&counter->state_cond); sc_mutex_destroy(&counter->mutex); } static inline bool -is_started(struct fps_counter *counter) { +is_started(struct sc_fps_counter *counter) { return atomic_load_explicit(&counter->started, memory_order_acquire); } static inline void -set_started(struct fps_counter *counter, bool started) { +set_started(struct sc_fps_counter *counter, bool started) { atomic_store_explicit(&counter->started, started, memory_order_release); } // must be called with mutex locked static void -display_fps(struct fps_counter *counter) { +display_fps(struct sc_fps_counter *counter) { unsigned rendered_per_second = - counter->nr_rendered * SC_TICK_FREQ / FPS_COUNTER_INTERVAL; + counter->nr_rendered * SC_TICK_FREQ / SC_FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); @@ -57,7 +57,7 @@ display_fps(struct fps_counter *counter) { // must be called with mutex locked static void -check_interval_expired(struct fps_counter *counter, uint32_t now) { +check_interval_expired(struct sc_fps_counter *counter, sc_tick now) { if (now < counter->next_timestamp) { return; } @@ -67,13 +67,13 @@ check_interval_expired(struct fps_counter *counter, uint32_t now) { counter->nr_skipped = 0; // add a multiple of the interval uint32_t elapsed_slices = - (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL + 1; - counter->next_timestamp += FPS_COUNTER_INTERVAL * elapsed_slices; + (now - counter->next_timestamp) / SC_FPS_COUNTER_INTERVAL + 1; + counter->next_timestamp += SC_FPS_COUNTER_INTERVAL * elapsed_slices; } static int run_fps_counter(void *data) { - struct fps_counter *counter = data; + struct sc_fps_counter *counter = data; sc_mutex_lock(&counter->mutex); while (!counter->interrupted) { @@ -94,9 +94,10 @@ run_fps_counter(void *data) { } bool -fps_counter_start(struct fps_counter *counter) { +sc_fps_counter_start(struct sc_fps_counter *counter) { sc_mutex_lock(&counter->mutex); - counter->next_timestamp = sc_tick_now() + FPS_COUNTER_INTERVAL; + counter->interrupted = false; + counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; sc_mutex_unlock(&counter->mutex); @@ -108,7 +109,7 @@ fps_counter_start(struct fps_counter *counter) { // same thread, no need to lock if (!counter->thread_started) { bool ok = sc_thread_create(&counter->thread, run_fps_counter, - "fps counter", counter); + "scrcpy-fps", counter); if (!ok) { LOGE("Could not start FPS counter thread"); return false; @@ -117,22 +118,24 @@ fps_counter_start(struct fps_counter *counter) { counter->thread_started = true; } + LOGI("FPS counter started"); return true; } void -fps_counter_stop(struct fps_counter *counter) { +sc_fps_counter_stop(struct sc_fps_counter *counter) { set_started(counter, false); sc_cond_signal(&counter->state_cond); + LOGI("FPS counter stopped"); } bool -fps_counter_is_started(struct fps_counter *counter) { +sc_fps_counter_is_started(struct sc_fps_counter *counter) { return is_started(counter); } void -fps_counter_interrupt(struct fps_counter *counter) { +sc_fps_counter_interrupt(struct sc_fps_counter *counter) { if (!counter->thread_started) { return; } @@ -145,7 +148,7 @@ fps_counter_interrupt(struct fps_counter *counter) { } void -fps_counter_join(struct fps_counter *counter) { +sc_fps_counter_join(struct sc_fps_counter *counter) { if (counter->thread_started) { // interrupted must be set by the thread calling join(), so no need to // lock for the assertion @@ -156,7 +159,7 @@ fps_counter_join(struct fps_counter *counter) { } void -fps_counter_add_rendered_frame(struct fps_counter *counter) { +sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter) { if (!is_started(counter)) { return; } @@ -169,7 +172,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { } void -fps_counter_add_skipped_frame(struct fps_counter *counter) { +sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter) { if (!is_started(counter)) { return; } diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index 9609c814..e7619271 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -1,5 +1,5 @@ -#ifndef FPSCOUNTER_H -#define FPSCOUNTER_H +#ifndef SC_FPSCOUNTER_H +#define SC_FPSCOUNTER_H #include "common.h" @@ -9,7 +9,7 @@ #include "util/thread.h" -struct fps_counter { +struct sc_fps_counter { sc_thread thread; sc_mutex mutex; sc_cond state_cond; @@ -28,32 +28,32 @@ struct fps_counter { }; bool -fps_counter_init(struct fps_counter *counter); +sc_fps_counter_init(struct sc_fps_counter *counter); void -fps_counter_destroy(struct fps_counter *counter); +sc_fps_counter_destroy(struct sc_fps_counter *counter); bool -fps_counter_start(struct fps_counter *counter); +sc_fps_counter_start(struct sc_fps_counter *counter); void -fps_counter_stop(struct fps_counter *counter); +sc_fps_counter_stop(struct sc_fps_counter *counter); bool -fps_counter_is_started(struct fps_counter *counter); +sc_fps_counter_is_started(struct sc_fps_counter *counter); // request to stop the thread (on quit) -// must be called before fps_counter_join() +// must be called before sc_fps_counter_join() void -fps_counter_interrupt(struct fps_counter *counter); +sc_fps_counter_interrupt(struct sc_fps_counter *counter); void -fps_counter_join(struct fps_counter *counter); +sc_fps_counter_join(struct sc_fps_counter *counter); void -fps_counter_add_rendered_frame(struct fps_counter *counter); +sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter); void -fps_counter_add_skipped_frame(struct fps_counter *counter); +sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter); #endif diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index 33ca6227..5699b58f 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -10,11 +10,13 @@ bool sc_frame_buffer_init(struct sc_frame_buffer *fb) { fb->pending_frame = av_frame_alloc(); if (!fb->pending_frame) { + LOG_OOM(); return false; } fb->tmp_frame = av_frame_alloc(); if (!fb->tmp_frame) { + LOG_OOM(); av_frame_free(&fb->pending_frame); return false; } @@ -48,9 +50,7 @@ swap_frames(AVFrame **lhs, AVFrame **rhs) { bool sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, - bool *previous_frame_skipped) { - sc_mutex_lock(&fb->mutex); - + bool *previous_frame_skipped) { // Use a temporary frame to preserve pending_frame in case of error. // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. int r = av_frame_ref(fb->tmp_frame, frame); @@ -59,6 +59,8 @@ sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, return false; } + sc_mutex_lock(&fb->mutex); + // Now that av_frame_ref() succeeded, we can replace the previous // pending_frame swap_frames(&fb->pending_frame, &fb->tmp_frame); diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h new file mode 100644 index 00000000..e17f8569 --- /dev/null +++ b/app/src/hid/hid_event.h @@ -0,0 +1,15 @@ +#ifndef SC_HID_EVENT_H +#define SC_HID_EVENT_H + +#include "common.h" + +#include + +#define SC_HID_MAX_SIZE 8 + +struct sc_hid_event { + uint8_t data[SC_HID_MAX_SIZE]; + uint8_t size; +}; + +#endif diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c new file mode 100644 index 00000000..f3001df4 --- /dev/null +++ b/app/src/hid/hid_keyboard.c @@ -0,0 +1,333 @@ +#include "hid_keyboard.h" + +#include + +#include "util/log.h" + +#define SC_HID_MOD_NONE 0x00 +#define SC_HID_MOD_LEFT_CONTROL (1 << 0) +#define SC_HID_MOD_LEFT_SHIFT (1 << 1) +#define SC_HID_MOD_LEFT_ALT (1 << 2) +#define SC_HID_MOD_LEFT_GUI (1 << 3) +#define SC_HID_MOD_RIGHT_CONTROL (1 << 4) +#define SC_HID_MOD_RIGHT_SHIFT (1 << 5) +#define SC_HID_MOD_RIGHT_ALT (1 << 6) +#define SC_HID_MOD_RIGHT_GUI (1 << 7) + +#define SC_HID_KEYBOARD_INDEX_MODS 0 +#define SC_HID_KEYBOARD_INDEX_KEYS 2 + +// USB HID protocol says 6 keys in an event is the requirement for BIOS +// keyboard support, though OS could support more keys via modifying the report +// desc. 6 should be enough for scrcpy. +#define SC_HID_KEYBOARD_MAX_KEYS 6 +#define SC_HID_KEYBOARD_EVENT_SIZE \ + (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS) + +#define SC_HID_RESERVED 0x00 +#define SC_HID_ERROR_ROLL_OVER 0x01 + +/** + * For HID, only report descriptor is needed. + * + * The specification is available here: + * + * + * In particular, read: + * - 6.2.2 Report Descriptor + * - Appendix B.1 Protocol 1 (Keyboard) + * - Appendix C: Keyboard Implementation + * + * Normally a basic HID keyboard uses 8 bytes: + * Modifier Reserved Key Key Key Key Key Key + * + * You can dump your device's report descriptor with: + * + * sudo usbhid-dump -m vid:pid -e descriptor + * + * (change vid:pid' to your device's vendor ID and product ID). + */ +const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Keyboard) + 0x09, 0x06, + + // Collection (Application) + 0xA1, 0x01, + + // Usage Page (Key Codes) + 0x05, 0x07, + // Usage Minimum (224) + 0x19, 0xE0, + // Usage Maximum (231) + 0x29, 0xE7, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Size (1) + 0x75, 0x01, + // Report Count (8) + 0x95, 0x08, + // Input (Data, Variable, Absolute): Modifier byte + 0x81, 0x02, + + // Report Size (8) + 0x75, 0x08, + // Report Count (1) + 0x95, 0x01, + // Input (Constant): Reserved byte + 0x81, 0x01, + + // Usage Page (LEDs) + 0x05, 0x08, + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (5) + 0x29, 0x05, + // Report Size (1) + 0x75, 0x01, + // Report Count (5) + 0x95, 0x05, + // Output (Data, Variable, Absolute): LED report + 0x91, 0x02, + + // Report Size (3) + 0x75, 0x03, + // Report Count (1) + 0x95, 0x01, + // Output (Constant): LED report padding + 0x91, 0x01, + + // Usage Page (Key Codes) + 0x05, 0x07, + // Usage Minimum (0) + 0x19, 0x00, + // Usage Maximum (101) + 0x29, SC_HID_KEYBOARD_KEYS - 1, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum(101) + 0x25, SC_HID_KEYBOARD_KEYS - 1, + // Report Size (8) + 0x75, 0x08, + // Report Count (6) + 0x95, SC_HID_KEYBOARD_MAX_KEYS, + // Input (Data, Array): Keys + 0x81, 0x00, + + // End Collection + 0xC0 +}; + +const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN = + sizeof(SC_HID_KEYBOARD_REPORT_DESC); + +/** + * A keyboard HID event is 8 bytes long: + * + * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) + * - byte 1: reserved (always 0) + * - bytes 2 to 7: pressed keys (6 at most) + * + * 7 6 5 4 3 2 1 0 + * +---------------+ + * byte 0: |. . . . . . . .| modifiers + * +---------------+ + * ^ ^ ^ ^ ^ ^ ^ ^ + * | | | | | | | `- left Ctrl + * | | | | | | `--- left Shift + * | | | | | `----- left Alt + * | | | | `------- left Gui + * | | | `--------- right Ctrl + * | | `----------- right Shift + * | `------------- right Alt + * `--------------- right Gui + * + * +---------------+ + * byte 1: |0 0 0 0 0 0 0 0| reserved + * +---------------+ + * + * +---------------+ + * bytes 2 to 7: |. . . . . . . .| scancode of 1st key pressed + * +---------------+ + * |. . . . . . . .| scancode of 2nd key pressed + * +---------------+ + * |. . . . . . . .| scancode of 3rd key pressed + * +---------------+ + * |. . . . . . . .| scancode of 4th key pressed + * +---------------+ + * |. . . . . . . .| scancode of 5th key pressed + * +---------------+ + * |. . . . . . . .| scancode of 6th key pressed + * +---------------+ + * + * If there are less than 6 keys pressed, the last items are set to 0. + * For example, if A and W are pressed: + * + * +---------------+ + * bytes 2 to 7: |0 0 0 0 0 1 0 0| A is pressed (scancode = 4) + * +---------------+ + * |0 0 0 1 1 0 1 0| W is pressed (scancode = 26) + * +---------------+ + * |0 0 0 0 0 0 0 0| ^ + * +---------------+ | only 2 keys are pressed, the + * |0 0 0 0 0 0 0 0| | remaining items are set to 0 + * +---------------+ | + * |0 0 0 0 0 0 0 0| | + * +---------------+ | + * |0 0 0 0 0 0 0 0| v + * +---------------+ + * + * Pressing more than 6 keys is not supported. If this happens (typically, + * never in practice), report a "phantom state": + * + * +---------------+ + * bytes 2 to 7: |0 0 0 0 0 0 0 1| ^ + * +---------------+ | + * |0 0 0 0 0 0 0 1| | more than 6 keys pressed: + * +---------------+ | the list is filled with a special + * |0 0 0 0 0 0 0 1| | rollover error code (0x01) + * +---------------+ | + * |0 0 0 0 0 0 0 1| | + * +---------------+ | + * |0 0 0 0 0 0 0 1| | + * +---------------+ | + * |0 0 0 0 0 0 0 1| v + * +---------------+ + */ + +static void +sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { + hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE; + + uint8_t *data = hid_event->data; + + data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE; + data[1] = SC_HID_RESERVED; + memset(&data[SC_HID_KEYBOARD_INDEX_KEYS], 0, SC_HID_KEYBOARD_MAX_KEYS); +} + +static uint16_t +sc_hid_mod_from_sdl_keymod(uint16_t mod) { + uint16_t mods = SC_HID_MOD_NONE; + if (mod & SC_MOD_LCTRL) { + mods |= SC_HID_MOD_LEFT_CONTROL; + } + if (mod & SC_MOD_LSHIFT) { + mods |= SC_HID_MOD_LEFT_SHIFT; + } + if (mod & SC_MOD_LALT) { + mods |= SC_HID_MOD_LEFT_ALT; + } + if (mod & SC_MOD_LGUI) { + mods |= SC_HID_MOD_LEFT_GUI; + } + if (mod & SC_MOD_RCTRL) { + mods |= SC_HID_MOD_RIGHT_CONTROL; + } + if (mod & SC_MOD_RSHIFT) { + mods |= SC_HID_MOD_RIGHT_SHIFT; + } + if (mod & SC_MOD_RALT) { + mods |= SC_HID_MOD_RIGHT_ALT; + } + if (mod & SC_MOD_RGUI) { + mods |= SC_HID_MOD_RIGHT_GUI; + } + return mods; +} + +void +sc_hid_keyboard_init(struct sc_hid_keyboard *hid) { + memset(hid->keys, false, SC_HID_KEYBOARD_KEYS); +} + +static inline bool +scancode_is_modifier(enum sc_scancode scancode) { + return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI; +} + +bool +sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_event *hid_event, + const struct sc_key_event *event) { + enum sc_scancode scancode = event->scancode; + assert(scancode >= 0); + + // SDL also generates events when only modifiers are pressed, we cannot + // ignore them totally, for example press 'a' first then press 'Control', + // if we ignore 'Control' event, only 'a' is sent. + if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) { + // Scancode to ignore + return false; + } + + sc_hid_keyboard_event_init(hid_event); + + uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state); + + if (scancode < SC_HID_KEYBOARD_KEYS) { + // Pressed is true and released is false + hid->keys[scancode] = (event->action == SC_ACTION_DOWN); + LOGV("keys[%02x] = %s", scancode, + hid->keys[scancode] ? "true" : "false"); + } + + hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods; + + uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS]; + // Re-calculate pressed keys every time + int keys_pressed_count = 0; + for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { + if (hid->keys[i]) { + // USB HID protocol says that if keys exceeds report count, a + // phantom state should be reported + if (keys_pressed_count >= SC_HID_KEYBOARD_MAX_KEYS) { + // Phantom state: + // - Modifiers + // - Reserved + // - ErrorRollOver * HID_MAX_KEYS + memset(keys_data, SC_HID_ERROR_ROLL_OVER, + SC_HID_KEYBOARD_MAX_KEYS); + goto end; + } + + keys_data[keys_pressed_count] = i; + ++keys_pressed_count; + } + } + +end: + LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", + event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode, + event->scancode, mods); + + return true; +} + +bool +sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, + uint16_t mods_state) { + bool capslock = mods_state & SC_MOD_CAPS; + bool numlock = mods_state & SC_MOD_NUM; + if (!capslock && !numlock) { + // Nothing to do + return false; + } + + sc_hid_keyboard_event_init(event); + + unsigned i = 0; + if (capslock) { + event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; + ++i; + } + if (numlock) { + event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; + ++i; + } + + return true; +} diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h new file mode 100644 index 00000000..ddd2cc91 --- /dev/null +++ b/app/src/hid/hid_keyboard.h @@ -0,0 +1,48 @@ +#ifndef SC_HID_KEYBOARD_H +#define SC_HID_KEYBOARD_H + +#include "common.h" + +#include + +#include "hid/hid_event.h" +#include "input_events.h" + +// See "SDL2/SDL_scancode.h". +// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB +// HID protocol. +// 0x65 is Application, typically AT-101 Keyboard ends here. +#define SC_HID_KEYBOARD_KEYS 0x66 + +extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[]; +extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN; + +/** + * HID keyboard events are sequence-based, every time keyboard state changes + * it sends an array of currently pressed keys, the host is responsible for + * compare events and determine which key becomes pressed and which key becomes + * released. In order to convert SDL_KeyboardEvent to HID events, we first use + * an array of keys to save each keys' state. And when a SDL_KeyboardEvent was + * emitted, we updated our state, and then we use a loop to generate HID + * events. The sequence of array elements is unimportant and when too much keys + * pressed at the same time (more than report count), we should generate + * phantom state. Don't forget that modifiers should be updated too, even for + * phantom state. + */ +struct sc_hid_keyboard { + bool keys[SC_HID_KEYBOARD_KEYS]; +}; + +void +sc_hid_keyboard_init(struct sc_hid_keyboard *hid); + +bool +sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid, + struct sc_hid_event *hid_event, + const struct sc_key_event *event); + +bool +sc_hid_keyboard_event_from_mods(struct sc_hid_event *event, + uint16_t mods_state); + +#endif diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c new file mode 100644 index 00000000..9d814448 --- /dev/null +++ b/app/src/hid/hid_mouse.c @@ -0,0 +1,192 @@ +#include "hid_mouse.h" + +// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, +// 1 byte for wheel motion +#define HID_MOUSE_EVENT_SIZE 4 + +/** + * Mouse descriptor from the specification: + * + * + * Appendix E (p71): §E.10 Report Descriptor (Mouse) + * + * The usage tags (like Wheel) are listed in "HID Usage Tables": + * + * §4 Generic Desktop Page (0x01) (p26) + */ +const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Mouse) + 0x09, 0x02, + + // Collection (Application) + 0xA1, 0x01, + + // Usage (Pointer) + 0x09, 0x01, + + // Collection (Physical) + 0xA1, 0x00, + + // Usage Page (Buttons) + 0x05, 0x09, + + // Usage Minimum (1) + 0x19, 0x01, + // Usage Maximum (5) + 0x29, 0x05, + // Logical Minimum (0) + 0x15, 0x00, + // Logical Maximum (1) + 0x25, 0x01, + // Report Count (5) + 0x95, 0x05, + // Report Size (1) + 0x75, 0x01, + // Input (Data, Variable, Absolute): 5 buttons bits + 0x81, 0x02, + + // Report Count (1) + 0x95, 0x01, + // Report Size (3) + 0x75, 0x03, + // Input (Constant): 3 bits padding + 0x81, 0x01, + + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (X) + 0x09, 0x30, + // Usage (Y) + 0x09, 0x31, + // Usage (Wheel) + 0x09, 0x38, + // Local Minimum (-127) + 0x15, 0x81, + // Local Maximum (127) + 0x25, 0x7F, + // Report Size (8) + 0x75, 0x08, + // Report Count (3) + 0x95, 0x03, + // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) + 0x81, 0x06, + + // End Collection + 0xC0, + + // End Collection + 0xC0, +}; + +const size_t SC_HID_MOUSE_REPORT_DESC_LEN = + sizeof(SC_HID_MOUSE_REPORT_DESC); + +/** + * A mouse HID event is 4 bytes long: + * + * - byte 0: buttons state + * - byte 1: relative x motion (signed byte from -127 to 127) + * - byte 2: relative y motion (signed byte from -127 to 127) + * - byte 3: wheel motion (-1, 0 or 1) + * + * 7 6 5 4 3 2 1 0 + * +---------------+ + * byte 0: |0 0 0 . . . . .| buttons state + * +---------------+ + * ^ ^ ^ ^ ^ + * | | | | `- left button + * | | | `--- right button + * | | `----- middle button + * | `------- button 4 + * `--------- button 5 + * + * +---------------+ + * byte 1: |. . . . . . . .| relative x motion + * +---------------+ + * byte 2: |. . . . . . . .| relative y motion + * +---------------+ + * byte 3: |. . . . . . . .| wheel motion + * +---------------+ + * + * As an example, here is the report for a motion of (x=5, y=-4) with left + * button pressed: + * + * +---------------+ + * |0 0 0 0 0 0 0 1| left button pressed + * +---------------+ + * |0 0 0 0 0 1 0 1| horizontal motion (x = 5) + * +---------------+ + * |1 1 1 1 1 1 0 0| relative y motion (y = -4) + * +---------------+ + * |0 0 0 0 0 0 0 0| wheel motion + * +---------------+ + */ + +static void +sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { + hid_event->size = HID_MOUSE_EVENT_SIZE; + // Leave hid_event->data uninitialized, it will be fully initialized by + // callers +} + +static uint8_t +sc_hid_buttons_from_buttons_state(uint8_t buttons_state) { + uint8_t c = 0; + if (buttons_state & SC_MOUSE_BUTTON_LEFT) { + c |= 1 << 0; + } + if (buttons_state & SC_MOUSE_BUTTON_RIGHT) { + c |= 1 << 1; + } + if (buttons_state & SC_MOUSE_BUTTON_MIDDLE) { + c |= 1 << 2; + } + if (buttons_state & SC_MOUSE_BUTTON_X1) { + c |= 1 << 3; + } + if (buttons_state & SC_MOUSE_BUTTON_X2) { + c |= 1 << 4; + } + return c; +} + +void +sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, + const struct sc_mouse_motion_event *event) { + sc_hid_mouse_event_init(hid_event); + + uint8_t *data = hid_event->data; + data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); + data[1] = CLAMP(event->xrel, -127, 127); + data[2] = CLAMP(event->yrel, -127, 127); + data[3] = 0; // wheel coordinates only used for scrolling +} + +void +sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, + const struct sc_mouse_click_event *event) { + sc_hid_mouse_event_init(hid_event); + + uint8_t *data = hid_event->data; + data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); + data[1] = 0; // no x motion + data[2] = 0; // no y motion + data[3] = 0; // wheel coordinates only used for scrolling +} + +void +sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, + const struct sc_mouse_scroll_event *event) { + sc_hid_mouse_event_init(hid_event); + + uint8_t *data = hid_event->data; + data[0] = 0; // buttons state irrelevant (and unknown) + data[1] = 0; // no x motion + data[2] = 0; // no y motion + // In practice, vscroll is always -1, 0 or 1, but in theory other values + // are possible + data[3] = CLAMP(event->vscroll, -127, 127); + // Horizontal scrolling ignored +} diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h new file mode 100644 index 00000000..e514d7d9 --- /dev/null +++ b/app/src/hid/hid_mouse.h @@ -0,0 +1,26 @@ +#ifndef SC_HID_MOUSE_H +#define SC_HID_MOUSE_H + +#endif + +#include "common.h" + +#include + +#include "hid/hid_event.h" +#include "input_events.h" + +extern const uint8_t SC_HID_MOUSE_REPORT_DESC[]; +extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN; + +void +sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event, + const struct sc_mouse_motion_event *event); + +void +sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event, + const struct sc_mouse_click_event *event); + +void +sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event, + const struct sc_mouse_scroll_event *event); diff --git a/app/src/icon.c b/app/src/icon.c new file mode 100644 index 00000000..a9aad875 --- /dev/null +++ b/app/src/icon.c @@ -0,0 +1,291 @@ +#include "icon.h" + +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "compat.h" +#include "util/file.h" +#include "util/log.h" +#include "util/str.h" + +#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" +#define SCRCPY_DEFAULT_ICON_PATH \ + PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png" + +static char * +get_icon_path(void) { +#ifdef __WINDOWS__ + const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); +#else + const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); +#endif + if (icon_path_env) { + // if the envvar is set, use it +#ifdef __WINDOWS__ + char *icon_path = sc_str_from_wchars(icon_path_env); +#else + char *icon_path = strdup(icon_path_env); +#endif + if (!icon_path) { + LOG_OOM(); + return NULL; + } + LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); + return icon_path; + } + +#ifndef PORTABLE + LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); + char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); + if (!icon_path) { + LOG_OOM(); + return NULL; + } +#else + char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); + if (!icon_path) { + LOGE("Could not get icon path"); + return NULL; + } + LOGD("Using icon (portable): %s", icon_path); +#endif + + return icon_path; +} + +static AVFrame * +decode_image(const char *path) { + AVFrame *result = NULL; + + AVFormatContext *ctx = avformat_alloc_context(); + if (!ctx) { + LOG_OOM(); + return NULL; + } + + if (avformat_open_input(&ctx, path, NULL, NULL) < 0) { + LOGE("Could not open icon image: %s", path); + goto free_ctx; + } + + if (avformat_find_stream_info(ctx, NULL) < 0) { + LOGE("Could not find image stream info"); + goto close_input; + } + + int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + if (stream < 0 ) { + LOGE("Could not find best image stream"); + goto close_input; + } + + AVCodecParameters *params = ctx->streams[stream]->codecpar; + + const AVCodec *codec = avcodec_find_decoder(params->codec_id); + if (!codec) { + LOGE("Could not find image decoder"); + goto close_input; + } + + AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + LOG_OOM(); + goto close_input; + } + + if (avcodec_parameters_to_context(codec_ctx, params) < 0) { + LOGE("Could not fill codec context"); + goto free_codec_ctx; + } + + if (avcodec_open2(codec_ctx, codec, NULL) < 0) { + LOGE("Could not open image codec"); + goto free_codec_ctx; + } + + AVFrame *frame = av_frame_alloc(); + if (!frame) { + LOG_OOM(); + goto close_codec; + } + + AVPacket *packet = av_packet_alloc(); + if (!packet) { + LOG_OOM(); + av_frame_free(&frame); + goto close_codec; + } + + if (av_read_frame(ctx, packet) < 0) { + LOGE("Could not read frame"); + av_packet_free(&packet); + av_frame_free(&frame); + goto close_codec; + } + + int ret; + if ((ret = avcodec_send_packet(codec_ctx, packet)) < 0) { + LOGE("Could not send icon packet: %d", ret); + av_packet_free(&packet); + av_frame_free(&frame); + goto close_codec; + } + + if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) { + LOGE("Could not receive icon frame: %d", ret); + av_packet_free(&packet); + av_frame_free(&frame); + goto close_codec; + } + + av_packet_free(&packet); + + result = frame; + +close_codec: + avcodec_close(codec_ctx); +free_codec_ctx: + avcodec_free_context(&codec_ctx); +close_input: + avformat_close_input(&ctx); +free_ctx: + avformat_free_context(ctx); + + return result; +} + +#if !SDL_VERSION_ATLEAST(2, 0, 10) +// SDL_PixelFormatEnum has been introduced in SDL 2.0.10. Use int for older SDL +// versions. +typedef int SDL_PixelFormatEnum; +#endif + +static SDL_PixelFormatEnum +to_sdl_pixel_format(enum AVPixelFormat fmt) { + switch (fmt) { + case AV_PIX_FMT_RGB24: return SDL_PIXELFORMAT_RGB24; + case AV_PIX_FMT_BGR24: return SDL_PIXELFORMAT_BGR24; + case AV_PIX_FMT_ARGB: return SDL_PIXELFORMAT_ARGB32; + case AV_PIX_FMT_RGBA: return SDL_PIXELFORMAT_RGBA32; + case AV_PIX_FMT_ABGR: return SDL_PIXELFORMAT_ABGR32; + case AV_PIX_FMT_BGRA: return SDL_PIXELFORMAT_BGRA32; + case AV_PIX_FMT_RGB565BE: return SDL_PIXELFORMAT_RGB565; + case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_RGB555; + case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565; + case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555; + case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444; +#if SDL_VERSION_ATLEAST(2, 0, 12) + case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444; +#endif + case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8; + default: return SDL_PIXELFORMAT_UNKNOWN; + } +} + +static SDL_Surface * +load_from_path(const char *path) { + AVFrame *frame = decode_image(path); + if (!frame) { + return NULL; + } + + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format); + if (!desc) { + LOGE("Could not get icon format descriptor"); + goto error; + } + + bool is_packed = !(desc->flags & AV_PIX_FMT_FLAG_PLANAR); + if (!is_packed) { + LOGE("Could not load non-packed icon"); + goto error; + } + + SDL_PixelFormatEnum format = to_sdl_pixel_format(frame->format); + if (format == SDL_PIXELFORMAT_UNKNOWN) { + LOGE("Unsupported icon pixel format: %s (%d)", desc->name, + frame->format); + goto error; + } + + int bits_per_pixel = av_get_bits_per_pixel(desc); + SDL_Surface *surface = + SDL_CreateRGBSurfaceWithFormatFrom(frame->data[0], + frame->width, frame->height, + bits_per_pixel, + frame->linesize[0], + format); + + if (!surface) { + LOGE("Could not create icon surface"); + goto error; + } + + if (frame->format == AV_PIX_FMT_PAL8) { + // Initialize the SDL palette + uint8_t *data = frame->data[1]; + SDL_Color colors[256]; + for (int i = 0; i < 256; ++i) { + SDL_Color *color = &colors[i]; + + // The palette is transported in AVFrame.data[1], is 1024 bytes + // long (256 4-byte entries) and is formatted the same as in + // AV_PIX_FMT_RGB32 described above (i.e., it is also + // endian-specific). + // +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + color->a = data[i * 4]; + color->r = data[i * 4 + 1]; + color->g = data[i * 4 + 2]; + color->b = data[i * 4 + 3]; +#else + color->a = data[i * 4 + 3]; + color->r = data[i * 4 + 2]; + color->g = data[i * 4 + 1]; + color->b = data[i * 4]; +#endif + } + + SDL_Palette *palette = surface->format->palette; + assert(palette); + int ret = SDL_SetPaletteColors(palette, colors, 0, 256); + if (ret) { + LOGE("Could not set palette colors"); + SDL_FreeSurface(surface); + goto error; + } + } + + surface->userdata = frame; // frame owns the data + + return surface; + +error: + av_frame_free(&frame); + return NULL; +} + +SDL_Surface * +scrcpy_icon_load(void) { + char *icon_path = get_icon_path(); + if (!icon_path) { + return NULL; + } + + SDL_Surface *icon = load_from_path(icon_path); + free(icon_path); + return icon; +} + +void +scrcpy_icon_destroy(SDL_Surface *icon) { + AVFrame *frame = icon->userdata; + assert(frame); + av_frame_free(&frame); + SDL_FreeSurface(icon); +} diff --git a/app/src/icon.h b/app/src/icon.h new file mode 100644 index 00000000..3251e48f --- /dev/null +++ b/app/src/icon.h @@ -0,0 +1,16 @@ +#ifndef SC_ICON_H +#define SC_ICON_H + +#include "common.h" + +#include +#include +#include + +SDL_Surface * +scrcpy_icon_load(void); + +void +scrcpy_icon_destroy(SDL_Surface *icon); + +#endif diff --git a/app/src/icon.xpm b/app/src/icon.xpm deleted file mode 100644 index 73b29da9..00000000 --- a/app/src/icon.xpm +++ /dev/null @@ -1,53 +0,0 @@ -/* XPM */ -static char * icon_xpm[] = { -"48 48 2 1", -" c None", -". c #96C13E", -" .. .. ", -" ... ... ", -" ... ...... ... ", -" ................ ", -" .............. ", -" ................ ", -" .................. ", -" .................... ", -" ..... ........ ..... ", -" ..... ........ ..... ", -" ...................... ", -" ........................ ", -" ........................ ", -" ........................ ", -" ", -" ", -" .... ........................ .... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" ...... ........................ ...... ", -" .... ........................ .... ", -" ........................ ", -" ...................... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" ...... ...... ", -" .... .... "}; diff --git a/app/src/input_events.h b/app/src/input_events.h new file mode 100644 index 00000000..5831ba0f --- /dev/null +++ b/app/src/input_events.h @@ -0,0 +1,454 @@ +#ifndef SC_INPUT_EVENTS_H +#define SC_INPUT_EVENTS_H + +#include "common.h" + +#include +#include +#include +#include + +#include "coords.h" + +/* The representation of input events in scrcpy is very close to the SDL API, + * for simplicity. + * + * This scrcpy input events API is designed to be consumed by input event + * processors (sc_key_processor and sc_mouse_processor, see app/src/trait/). + * + * One major semantic difference between SDL input events and scrcpy input + * events is their frame of reference (for mouse and touch events): SDL events + * coordinates are expressed in SDL window coordinates (the visible UI), while + * scrcpy events are expressed in device frame coordinates. + * + * In particular, the window may be visually scaled or rotated (with --rotation + * or MOD+Left/Right), but this does not impact scrcpy input events (contrary + * to SDL input events). This allows to abstract these display details from the + * input event processors (and to make them independent from the "screen"). + * + * For many enums below, the values are purposely the same as the SDL + * constants (though not all SDL values are represented), so that the + * implementation to convert from the SDL version to the scrcpy version is + * straightforward. + * + * In practice, there are 3 levels of input events: + * 1. SDL input events (as received from SDL) + * 2. scrcpy input events (this API) + * 3. the key/mouse processors input events (Android API or HID events) + * + * An input event is first received (1), then (if accepted) converted to an + * scrcpy input event (2), then submitted to the relevant key/mouse processor, + * which (if accepted) is converted to an Android event (to be sent to the + * server) or to an HID event (to be sent over USB/AOA directly). + */ + +enum sc_mod { + SC_MOD_LSHIFT = KMOD_LSHIFT, + SC_MOD_RSHIFT = KMOD_RSHIFT, + SC_MOD_LCTRL = KMOD_LCTRL, + SC_MOD_RCTRL = KMOD_RCTRL, + SC_MOD_LALT = KMOD_LALT, + SC_MOD_RALT = KMOD_RALT, + SC_MOD_LGUI = KMOD_LGUI, + SC_MOD_RGUI = KMOD_RGUI, + + SC_MOD_NUM = KMOD_NUM, + SC_MOD_CAPS = KMOD_CAPS, +}; + +enum sc_action { + SC_ACTION_DOWN, // key or button pressed + SC_ACTION_UP, // key or button released +}; + +enum sc_keycode { + SC_KEYCODE_UNKNOWN = SDLK_UNKNOWN, + + SC_KEYCODE_RETURN = SDLK_RETURN, + SC_KEYCODE_ESCAPE = SDLK_ESCAPE, + SC_KEYCODE_BACKSPACE = SDLK_BACKSPACE, + SC_KEYCODE_TAB = SDLK_TAB, + SC_KEYCODE_SPACE = SDLK_SPACE, + SC_KEYCODE_EXCLAIM = SDLK_EXCLAIM, + SC_KEYCODE_QUOTEDBL = SDLK_QUOTEDBL, + SC_KEYCODE_HASH = SDLK_HASH, + SC_KEYCODE_PERCENT = SDLK_PERCENT, + SC_KEYCODE_DOLLAR = SDLK_DOLLAR, + SC_KEYCODE_AMPERSAND = SDLK_AMPERSAND, + SC_KEYCODE_QUOTE = SDLK_QUOTE, + SC_KEYCODE_LEFTPAREN = SDLK_LEFTPAREN, + SC_KEYCODE_RIGHTPAREN = SDLK_RIGHTPAREN, + SC_KEYCODE_ASTERISK = SDLK_ASTERISK, + SC_KEYCODE_PLUS = SDLK_PLUS, + SC_KEYCODE_COMMA = SDLK_COMMA, + SC_KEYCODE_MINUS = SDLK_MINUS, + SC_KEYCODE_PERIOD = SDLK_PERIOD, + SC_KEYCODE_SLASH = SDLK_SLASH, + SC_KEYCODE_0 = SDLK_0, + SC_KEYCODE_1 = SDLK_1, + SC_KEYCODE_2 = SDLK_2, + SC_KEYCODE_3 = SDLK_3, + SC_KEYCODE_4 = SDLK_4, + SC_KEYCODE_5 = SDLK_5, + SC_KEYCODE_6 = SDLK_6, + SC_KEYCODE_7 = SDLK_7, + SC_KEYCODE_8 = SDLK_8, + SC_KEYCODE_9 = SDLK_9, + SC_KEYCODE_COLON = SDLK_COLON, + SC_KEYCODE_SEMICOLON = SDLK_SEMICOLON, + SC_KEYCODE_LESS = SDLK_LESS, + SC_KEYCODE_EQUALS = SDLK_EQUALS, + SC_KEYCODE_GREATER = SDLK_GREATER, + SC_KEYCODE_QUESTION = SDLK_QUESTION, + SC_KEYCODE_AT = SDLK_AT, + + SC_KEYCODE_LEFTBRACKET = SDLK_LEFTBRACKET, + SC_KEYCODE_BACKSLASH = SDLK_BACKSLASH, + SC_KEYCODE_RIGHTBRACKET = SDLK_RIGHTBRACKET, + SC_KEYCODE_CARET = SDLK_CARET, + SC_KEYCODE_UNDERSCORE = SDLK_UNDERSCORE, + SC_KEYCODE_BACKQUOTE = SDLK_BACKQUOTE, + SC_KEYCODE_a = SDLK_a, + SC_KEYCODE_b = SDLK_b, + SC_KEYCODE_c = SDLK_c, + SC_KEYCODE_d = SDLK_d, + SC_KEYCODE_e = SDLK_e, + SC_KEYCODE_f = SDLK_f, + SC_KEYCODE_g = SDLK_g, + SC_KEYCODE_h = SDLK_h, + SC_KEYCODE_i = SDLK_i, + SC_KEYCODE_j = SDLK_j, + SC_KEYCODE_k = SDLK_k, + SC_KEYCODE_l = SDLK_l, + SC_KEYCODE_m = SDLK_m, + SC_KEYCODE_n = SDLK_n, + SC_KEYCODE_o = SDLK_o, + SC_KEYCODE_p = SDLK_p, + SC_KEYCODE_q = SDLK_q, + SC_KEYCODE_r = SDLK_r, + SC_KEYCODE_s = SDLK_s, + SC_KEYCODE_t = SDLK_t, + SC_KEYCODE_u = SDLK_u, + SC_KEYCODE_v = SDLK_v, + SC_KEYCODE_w = SDLK_w, + SC_KEYCODE_x = SDLK_x, + SC_KEYCODE_y = SDLK_y, + SC_KEYCODE_z = SDLK_z, + + SC_KEYCODE_CAPSLOCK = SDLK_CAPSLOCK, + + SC_KEYCODE_F1 = SDLK_F1, + SC_KEYCODE_F2 = SDLK_F2, + SC_KEYCODE_F3 = SDLK_F3, + SC_KEYCODE_F4 = SDLK_F4, + SC_KEYCODE_F5 = SDLK_F5, + SC_KEYCODE_F6 = SDLK_F6, + SC_KEYCODE_F7 = SDLK_F7, + SC_KEYCODE_F8 = SDLK_F8, + SC_KEYCODE_F9 = SDLK_F9, + SC_KEYCODE_F10 = SDLK_F10, + SC_KEYCODE_F11 = SDLK_F11, + SC_KEYCODE_F12 = SDLK_F12, + + SC_KEYCODE_PRINTSCREEN = SDLK_PRINTSCREEN, + SC_KEYCODE_SCROLLLOCK = SDLK_SCROLLLOCK, + SC_KEYCODE_PAUSE = SDLK_PAUSE, + SC_KEYCODE_INSERT = SDLK_INSERT, + SC_KEYCODE_HOME = SDLK_HOME, + SC_KEYCODE_PAGEUP = SDLK_PAGEUP, + SC_KEYCODE_DELETE = SDLK_DELETE, + SC_KEYCODE_END = SDLK_END, + SC_KEYCODE_PAGEDOWN = SDLK_PAGEDOWN, + SC_KEYCODE_RIGHT = SDLK_RIGHT, + SC_KEYCODE_LEFT = SDLK_LEFT, + SC_KEYCODE_DOWN = SDLK_DOWN, + SC_KEYCODE_UP = SDLK_UP, + + SC_KEYCODE_KP_DIVIDE = SDLK_KP_DIVIDE, + SC_KEYCODE_KP_MULTIPLY = SDLK_KP_MULTIPLY, + SC_KEYCODE_KP_MINUS = SDLK_KP_MINUS, + SC_KEYCODE_KP_PLUS = SDLK_KP_PLUS, + SC_KEYCODE_KP_ENTER = SDLK_KP_ENTER, + SC_KEYCODE_KP_1 = SDLK_KP_1, + SC_KEYCODE_KP_2 = SDLK_KP_2, + SC_KEYCODE_KP_3 = SDLK_KP_3, + SC_KEYCODE_KP_4 = SDLK_KP_4, + SC_KEYCODE_KP_5 = SDLK_KP_5, + SC_KEYCODE_KP_6 = SDLK_KP_6, + SC_KEYCODE_KP_7 = SDLK_KP_7, + SC_KEYCODE_KP_8 = SDLK_KP_8, + SC_KEYCODE_KP_9 = SDLK_KP_9, + SC_KEYCODE_KP_0 = SDLK_KP_0, + SC_KEYCODE_KP_PERIOD = SDLK_KP_PERIOD, + SC_KEYCODE_KP_EQUALS = SDLK_KP_EQUALS, + SC_KEYCODE_KP_LEFTPAREN = SDLK_KP_LEFTPAREN, + SC_KEYCODE_KP_RIGHTPAREN = SDLK_KP_RIGHTPAREN, + + SC_KEYCODE_LCTRL = SDLK_LCTRL, + SC_KEYCODE_LSHIFT = SDLK_LSHIFT, + SC_KEYCODE_LALT = SDLK_LALT, + SC_KEYCODE_LGUI = SDLK_LGUI, + SC_KEYCODE_RCTRL = SDLK_RCTRL, + SC_KEYCODE_RSHIFT = SDLK_RSHIFT, + SC_KEYCODE_RALT = SDLK_RALT, + SC_KEYCODE_RGUI = SDLK_RGUI, +}; + +enum sc_scancode { + SC_SCANCODE_UNKNOWN = SDL_SCANCODE_UNKNOWN, + + SC_SCANCODE_A = SDL_SCANCODE_A, + SC_SCANCODE_B = SDL_SCANCODE_B, + SC_SCANCODE_C = SDL_SCANCODE_C, + SC_SCANCODE_D = SDL_SCANCODE_D, + SC_SCANCODE_E = SDL_SCANCODE_E, + SC_SCANCODE_F = SDL_SCANCODE_F, + SC_SCANCODE_G = SDL_SCANCODE_G, + SC_SCANCODE_H = SDL_SCANCODE_H, + SC_SCANCODE_I = SDL_SCANCODE_I, + SC_SCANCODE_J = SDL_SCANCODE_J, + SC_SCANCODE_K = SDL_SCANCODE_K, + SC_SCANCODE_L = SDL_SCANCODE_L, + SC_SCANCODE_M = SDL_SCANCODE_M, + SC_SCANCODE_N = SDL_SCANCODE_N, + SC_SCANCODE_O = SDL_SCANCODE_O, + SC_SCANCODE_P = SDL_SCANCODE_P, + SC_SCANCODE_Q = SDL_SCANCODE_Q, + SC_SCANCODE_R = SDL_SCANCODE_R, + SC_SCANCODE_S = SDL_SCANCODE_S, + SC_SCANCODE_T = SDL_SCANCODE_T, + SC_SCANCODE_U = SDL_SCANCODE_U, + SC_SCANCODE_V = SDL_SCANCODE_V, + SC_SCANCODE_W = SDL_SCANCODE_W, + SC_SCANCODE_X = SDL_SCANCODE_X, + SC_SCANCODE_Y = SDL_SCANCODE_Y, + SC_SCANCODE_Z = SDL_SCANCODE_Z, + + SC_SCANCODE_1 = SDL_SCANCODE_1, + SC_SCANCODE_2 = SDL_SCANCODE_2, + SC_SCANCODE_3 = SDL_SCANCODE_3, + SC_SCANCODE_4 = SDL_SCANCODE_4, + SC_SCANCODE_5 = SDL_SCANCODE_5, + SC_SCANCODE_6 = SDL_SCANCODE_6, + SC_SCANCODE_7 = SDL_SCANCODE_7, + SC_SCANCODE_8 = SDL_SCANCODE_8, + SC_SCANCODE_9 = SDL_SCANCODE_9, + SC_SCANCODE_0 = SDL_SCANCODE_0, + + SC_SCANCODE_RETURN = SDL_SCANCODE_RETURN, + SC_SCANCODE_ESCAPE = SDL_SCANCODE_ESCAPE, + SC_SCANCODE_BACKSPACE = SDL_SCANCODE_BACKSPACE, + SC_SCANCODE_TAB = SDL_SCANCODE_TAB, + SC_SCANCODE_SPACE = SDL_SCANCODE_SPACE, + + SC_SCANCODE_MINUS = SDL_SCANCODE_MINUS, + SC_SCANCODE_EQUALS = SDL_SCANCODE_EQUALS, + SC_SCANCODE_LEFTBRACKET = SDL_SCANCODE_LEFTBRACKET, + SC_SCANCODE_RIGHTBRACKET = SDL_SCANCODE_RIGHTBRACKET, + SC_SCANCODE_BACKSLASH = SDL_SCANCODE_BACKSLASH, + SC_SCANCODE_NONUSHASH = SDL_SCANCODE_NONUSHASH, + SC_SCANCODE_SEMICOLON = SDL_SCANCODE_SEMICOLON, + SC_SCANCODE_APOSTROPHE = SDL_SCANCODE_APOSTROPHE, + SC_SCANCODE_GRAVE = SDL_SCANCODE_GRAVE, + SC_SCANCODE_COMMA = SDL_SCANCODE_COMMA, + SC_SCANCODE_PERIOD = SDL_SCANCODE_PERIOD, + SC_SCANCODE_SLASH = SDL_SCANCODE_SLASH, + + SC_SCANCODE_CAPSLOCK = SDL_SCANCODE_CAPSLOCK, + + SC_SCANCODE_F1 = SDL_SCANCODE_F1, + SC_SCANCODE_F2 = SDL_SCANCODE_F2, + SC_SCANCODE_F3 = SDL_SCANCODE_F3, + SC_SCANCODE_F4 = SDL_SCANCODE_F4, + SC_SCANCODE_F5 = SDL_SCANCODE_F5, + SC_SCANCODE_F6 = SDL_SCANCODE_F6, + SC_SCANCODE_F7 = SDL_SCANCODE_F7, + SC_SCANCODE_F8 = SDL_SCANCODE_F8, + SC_SCANCODE_F9 = SDL_SCANCODE_F9, + SC_SCANCODE_F10 = SDL_SCANCODE_F10, + SC_SCANCODE_F11 = SDL_SCANCODE_F11, + SC_SCANCODE_F12 = SDL_SCANCODE_F12, + + SC_SCANCODE_PRINTSCREEN = SDL_SCANCODE_PRINTSCREEN, + SC_SCANCODE_SCROLLLOCK = SDL_SCANCODE_SCROLLLOCK, + SC_SCANCODE_PAUSE = SDL_SCANCODE_PAUSE, + SC_SCANCODE_INSERT = SDL_SCANCODE_INSERT, + SC_SCANCODE_HOME = SDL_SCANCODE_HOME, + SC_SCANCODE_PAGEUP = SDL_SCANCODE_PAGEUP, + SC_SCANCODE_DELETE = SDL_SCANCODE_DELETE, + SC_SCANCODE_END = SDL_SCANCODE_END, + SC_SCANCODE_PAGEDOWN = SDL_SCANCODE_PAGEDOWN, + SC_SCANCODE_RIGHT = SDL_SCANCODE_RIGHT, + SC_SCANCODE_LEFT = SDL_SCANCODE_LEFT, + SC_SCANCODE_DOWN = SDL_SCANCODE_DOWN, + SC_SCANCODE_UP = SDL_SCANCODE_UP, + + SC_SCANCODE_NUMLOCK = SDL_SCANCODE_NUMLOCKCLEAR, + SC_SCANCODE_KP_DIVIDE = SDL_SCANCODE_KP_DIVIDE, + SC_SCANCODE_KP_MULTIPLY = SDL_SCANCODE_KP_MULTIPLY, + SC_SCANCODE_KP_MINUS = SDL_SCANCODE_KP_MINUS, + SC_SCANCODE_KP_PLUS = SDL_SCANCODE_KP_PLUS, + SC_SCANCODE_KP_ENTER = SDL_SCANCODE_KP_ENTER, + SC_SCANCODE_KP_1 = SDL_SCANCODE_KP_1, + SC_SCANCODE_KP_2 = SDL_SCANCODE_KP_2, + SC_SCANCODE_KP_3 = SDL_SCANCODE_KP_3, + SC_SCANCODE_KP_4 = SDL_SCANCODE_KP_4, + SC_SCANCODE_KP_5 = SDL_SCANCODE_KP_5, + SC_SCANCODE_KP_6 = SDL_SCANCODE_KP_6, + SC_SCANCODE_KP_7 = SDL_SCANCODE_KP_7, + SC_SCANCODE_KP_8 = SDL_SCANCODE_KP_8, + SC_SCANCODE_KP_9 = SDL_SCANCODE_KP_9, + SC_SCANCODE_KP_0 = SDL_SCANCODE_KP_0, + SC_SCANCODE_KP_PERIOD = SDL_SCANCODE_KP_PERIOD, + + SC_SCANCODE_LCTRL = SDL_SCANCODE_LCTRL, + SC_SCANCODE_LSHIFT = SDL_SCANCODE_LSHIFT, + SC_SCANCODE_LALT = SDL_SCANCODE_LALT, + SC_SCANCODE_LGUI = SDL_SCANCODE_LGUI, + SC_SCANCODE_RCTRL = SDL_SCANCODE_RCTRL, + SC_SCANCODE_RSHIFT = SDL_SCANCODE_RSHIFT, + SC_SCANCODE_RALT = SDL_SCANCODE_RALT, + SC_SCANCODE_RGUI = SDL_SCANCODE_RGUI, +}; + +// On purpose, only use the "mask" values (1, 2, 4, 8, 16) for a single button, +// to avoid unnecessary conversions (and confusion). +enum sc_mouse_button { + SC_MOUSE_BUTTON_UNKNOWN = 0, + SC_MOUSE_BUTTON_LEFT = SDL_BUTTON(SDL_BUTTON_LEFT), + SC_MOUSE_BUTTON_RIGHT = SDL_BUTTON(SDL_BUTTON_RIGHT), + SC_MOUSE_BUTTON_MIDDLE = SDL_BUTTON(SDL_BUTTON_MIDDLE), + SC_MOUSE_BUTTON_X1 = SDL_BUTTON(SDL_BUTTON_X1), + SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2), +}; + +static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod), + "SDL_Keymod must be convertible to sc_mod"); + +static_assert(sizeof(enum sc_keycode) >= sizeof(SDL_Keycode), + "SDL_Keycode must be convertible to sc_keycode"); + +static_assert(sizeof(enum sc_scancode) >= sizeof(SDL_Scancode), + "SDL_Scancode must be convertible to sc_scancode"); + +enum sc_touch_action { + SC_TOUCH_ACTION_MOVE, + SC_TOUCH_ACTION_DOWN, + SC_TOUCH_ACTION_UP, +}; + +struct sc_key_event { + enum sc_action action; + enum sc_keycode keycode; + enum sc_scancode scancode; + uint16_t mods_state; // bitwise-OR of sc_mod values + bool repeat; +}; + +struct sc_text_event { + const char *text; // not owned +}; + +struct sc_mouse_click_event { + struct sc_position position; + enum sc_action action; + enum sc_mouse_button button; + uint64_t pointer_id; + uint8_t buttons_state; // bitwise-OR of sc_mouse_button values +}; + +struct sc_mouse_scroll_event { + struct sc_position position; + float hscroll; + float vscroll; + uint8_t buttons_state; // bitwise-OR of sc_mouse_button values +}; + +struct sc_mouse_motion_event { + struct sc_position position; + uint64_t pointer_id; + int32_t xrel; + int32_t yrel; + uint8_t buttons_state; // bitwise-OR of sc_mouse_button values +}; + +struct sc_touch_event { + struct sc_position position; + enum sc_touch_action action; + uint64_t pointer_id; + float pressure; +}; + +static inline uint16_t +sc_mods_state_from_sdl(uint16_t mods_state) { + return mods_state; +} + +static inline enum sc_keycode +sc_keycode_from_sdl(SDL_Keycode keycode) { + return (enum sc_keycode) keycode; +} + +static inline enum sc_scancode +sc_scancode_from_sdl(SDL_Scancode scancode) { + return (enum sc_scancode) scancode; +} + +static inline enum sc_action +sc_action_from_sdl_keyboard_type(uint32_t type) { + assert(type == SDL_KEYDOWN || type == SDL_KEYUP); + if (type == SDL_KEYDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + +static inline enum sc_action +sc_action_from_sdl_mousebutton_type(uint32_t type) { + assert(type == SDL_MOUSEBUTTONDOWN || type == SDL_MOUSEBUTTONUP); + if (type == SDL_MOUSEBUTTONDOWN) { + return SC_ACTION_DOWN; + } + return SC_ACTION_UP; +} + +static inline enum sc_touch_action +sc_touch_action_from_sdl(uint32_t type) { + assert(type == SDL_FINGERMOTION || type == SDL_FINGERDOWN || + type == SDL_FINGERUP); + if (type == SDL_FINGERMOTION) { + return SC_TOUCH_ACTION_MOVE; + } + if (type == SDL_FINGERDOWN) { + return SC_TOUCH_ACTION_DOWN; + } + return SC_TOUCH_ACTION_UP; +} + +static inline enum sc_mouse_button +sc_mouse_button_from_sdl(uint8_t button) { + if (button >= SDL_BUTTON_LEFT && button <= SDL_BUTTON_X2) { + // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) + return SDL_BUTTON(button); + } + + return SC_MOUSE_BUTTON_UNKNOWN; +} + +static inline uint8_t +sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, + bool forward_all_clicks) { + assert(buttons_state < 0x100); // fits in uint8_t + + uint8_t mask = SC_MOUSE_BUTTON_LEFT; + if (forward_all_clicks) { + mask |= SC_MOUSE_BUTTON_RIGHT + | SC_MOUSE_BUTTON_MIDDLE + | SC_MOUSE_BUTTON_X1 + | SC_MOUSE_BUTTON_X2; + } + + return buttons_state & mask; +} + +#endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index a5d0ad07..f26c4164 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -3,40 +3,38 @@ #include #include -#include "event_converter.h" +#include "input_events.h" +#include "screen.h" #include "util/log.h" -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) { +to_sdl_mod(unsigned shortcut_mod) { uint16_t sdl_mod = 0; - if (mod & SC_MOD_LCTRL) { + if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { sdl_mod |= KMOD_LCTRL; } - if (mod & SC_MOD_RCTRL) { + if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) { sdl_mod |= KMOD_RCTRL; } - if (mod & SC_MOD_LALT) { + if (shortcut_mod & SC_SHORTCUT_MOD_LALT) { sdl_mod |= KMOD_LALT; } - if (mod & SC_MOD_RALT) { + if (shortcut_mod & SC_SHORTCUT_MOD_RALT) { sdl_mod |= KMOD_RALT; } - if (mod & SC_MOD_LSUPER) { + if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) { sdl_mod |= KMOD_LGUI; } - if (mod & SC_MOD_RSUPER) { + if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) { sdl_mod |= KMOD_RGUI; } return sdl_mod; } static bool -is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { +is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { // keep only the relevant modifier keys sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; @@ -52,20 +50,25 @@ is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { } void -input_manager_init(struct input_manager *im, struct controller *controller, - struct screen *screen, - 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; +sc_input_manager_init(struct sc_input_manager *im, + const struct sc_input_manager_params *params) { + // A key/mouse processor may not be present if there is no controller + assert((!params->kp && !params->mp) || params->controller); + // A processor must have ops initialized + assert(!params->kp || params->kp->ops); + assert(!params->mp || params->mp->ops); + + im->controller = params->controller; + im->fp = params->fp; + im->screen = params->screen; + im->kp = params->kp; + im->mp = params->mp; + + im->forward_all_clicks = params->forward_all_clicks; + im->legacy_paste = params->legacy_paste; + im->clipboard_autosync = params->clipboard_autosync; + + const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods; assert(shortcut_mods->count); assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); for (unsigned i = 0; i < shortcut_mods->count; ++i) { @@ -76,197 +79,206 @@ input_manager_init(struct input_manager *im, struct controller *controller, im->sdl_shortcut_mods.count = shortcut_mods->count; im->vfinger_down = false; + im->vfinger_invert_x = false; + im->vfinger_invert_y = false; im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; im->key_repeat = 0; + + im->next_sequence = 1; // 0 is reserved for SC_SEQUENCE_INVALID } static void -send_keycode(struct controller *controller, enum android_keycode keycode, - int actions, const char *name) { +send_keycode(struct sc_input_manager *im, enum android_keycode keycode, + enum sc_action action, const char *name) { + assert(im->controller && im->kp); + // send DOWN event - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; + msg.inject_keycode.action = action == SC_ACTION_DOWN + ? AKEY_EVENT_ACTION_DOWN + : AKEY_EVENT_ACTION_UP; 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; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'inject %s (DOWN)'", name); - return; - } - } - - if (actions & ACTION_UP) { - msg.inject_keycode.action = AKEY_EVENT_ACTION_UP; - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'inject %s (UP)'", name); - } + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'inject %s'", name); } } static inline void -action_home(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_HOME, actions, "HOME"); -} - -static inline void -action_back(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_BACK, actions, "BACK"); -} - -static inline void -action_app_switch(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_APP_SWITCH, actions, "APP_SWITCH"); +action_home(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_HOME, action, "HOME"); } static inline void -action_power(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_POWER, actions, "POWER"); +action_back(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_BACK, action, "BACK"); } static inline void -action_volume_up(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP"); +action_app_switch(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); } static inline void -action_volume_down(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN"); +action_power(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_POWER, action, "POWER"); } static inline void -action_menu(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_MENU, actions, "MENU"); +action_volume_up(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); } static inline void -action_copy(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_COPY, actions, "COPY"); +action_volume_down(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); } static inline void -action_cut(struct controller *controller, int actions) { - send_keycode(controller, AKEYCODE_CUT, actions, "CUT"); +action_menu(struct sc_input_manager *im, enum sc_action action) { + send_keycode(im, AKEYCODE_MENU, action, "MENU"); } // 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, int actions) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_BACK_OR_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; - } - } +press_back_or_turn_screen_on(struct sc_input_manager *im, + enum sc_action action) { + assert(im->controller && im->kp); - 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'"); - } + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; + msg.back_or_screen_on.action = action == SC_ACTION_DOWN + ? AKEY_EVENT_ACTION_DOWN + : AKEY_EVENT_ACTION_UP; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'press back or turn screen on'"); } } static void -expand_notification_panel(struct controller *controller) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; +expand_notification_panel(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'expand notification panel'"); } } static void -expand_settings_panel(struct controller *controller) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; +expand_settings_panel(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->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; +collapse_panels(struct sc_input_manager *im) { + assert(im->controller); - if (!controller_push_msg(controller, &msg)) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; + + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); } } -static void -set_device_clipboard(struct controller *controller, bool paste) { +static bool +get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) { + assert(im->controller && im->kp); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; + msg.get_clipboard.copy_key = copy_key; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request 'get device clipboard'"); + return false; + } + + return true; +} + +static bool +set_device_clipboard(struct sc_input_manager *im, bool paste, + uint64_t sequence) { + assert(im->controller && im->kp); + char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); - return; - } - if (!*text) { - // empty text - SDL_free(text); - return; + return false; } char *text_dup = strdup(text); SDL_free(text); if (!text_dup) { LOGW("Could not strdup input text"); - return; + return false; } - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD; + msg.set_clipboard.sequence = sequence; msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { free(text_dup); LOGW("Could not request 'set device clipboard'"); + return false; } + + return true; } static void -set_screen_power_mode(struct controller *controller, - enum screen_power_mode mode) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; +set_screen_power_mode(struct sc_input_manager *im, + enum sc_screen_power_mode mode) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = mode; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } static void -switch_fps_counter_state(struct fps_counter *fps_counter) { +switch_fps_counter_state(struct sc_input_manager *im) { + struct sc_fps_counter *fps_counter = &im->screen->fps_counter; + // the started state can only be written from the current thread, so there // is no ToCToU issue - if (fps_counter_is_started(fps_counter)) { - fps_counter_stop(fps_counter); - LOGI("FPS counter stopped"); + if (sc_fps_counter_is_started(fps_counter)) { + sc_fps_counter_stop(fps_counter); } else { - if (fps_counter_start(fps_counter)) { - LOGI("FPS counter started"); - } else { - LOGE("FPS counter starting failed"); - } + sc_fps_counter_start(fps_counter); + // Any error is already logged } } static void -clipboard_paste(struct controller *controller) { +clipboard_paste(struct sc_input_manager *im) { + assert(im->controller && im->kp); + char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); @@ -285,82 +297,87 @@ clipboard_paste(struct controller *controller) { return; } - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; - if (!controller_push_msg(controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { free(text_dup); LOGW("Could not request 'paste clipboard'"); } } static void -rotate_device(struct controller *controller) { - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_ROTATE_DEVICE; +rotate_device(struct sc_input_manager *im) { + assert(im->controller); - if (!controller_push_msg(controller, &msg)) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; + + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request device rotation"); } } static void -rotate_client_left(struct screen *screen) { - unsigned new_rotation = (screen->rotation + 1) % 4; - screen_set_rotation(screen, new_rotation); +open_hard_keyboard_settings(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request opening hard keyboard settings"); + } } static void -rotate_client_right(struct screen *screen) { - unsigned new_rotation = (screen->rotation + 3) % 4; - screen_set_rotation(screen, new_rotation); +apply_orientation_transform(struct sc_input_manager *im, + enum sc_orientation transform) { + struct sc_screen *screen = im->screen; + enum sc_orientation new_orientation = + sc_orientation_apply(screen->orientation, transform); + sc_screen_set_orientation(screen, new_orientation); } 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 +sc_input_manager_process_text_input(struct sc_input_manager *im, + const SDL_TextInputEvent *event) { + if (!im->kp->ops->process_text) { + // The key processor does not support text input return; } - if (!im->prefer_text) { - char c = event->text[0]; - if (isalpha(c) || c == ' ') { - assert(event->text[1] == '\0'); - // letters and space are handled as raw key event - return; - } - } - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; - msg.inject_text.text = strdup(event->text); - if (!msg.inject_text.text) { - LOGW("Could not strdup input text"); + if (is_shortcut_mod(im, SDL_GetModState())) { + // A shortcut must never generate text events return; } - if (!controller_push_msg(im->controller, &msg)) { - free(msg.inject_text.text); - LOGW("Could not request 'inject text'"); - } + + struct sc_text_event evt = { + .text = event->text, + }; + + im->kp->ops->process_text(im->kp, &evt); } static bool -simulate_virtual_finger(struct input_manager *im, +simulate_virtual_finger(struct sc_input_manager *im, enum android_motionevent_action action, - struct point point) { + struct sc_point point) { bool up = action == AMOTION_EVENT_ACTION_UP; - struct control_msg msg; - msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; + struct sc_control_msg msg; + msg.type = SC_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.pointer_id = + im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE + : POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; + msg.inject_touch_event.action_button = 0; msg.inject_touch_event.buttons = 0; - if (!controller_push_msg(im->controller, &msg)) { + if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject virtual finger event'"); return false; } @@ -368,41 +385,23 @@ simulate_virtual_finger(struct input_manager *im, 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, uint32_t repeat) { - to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; - - if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { - return false; +static struct sc_point +inverse_point(struct sc_point point, struct sc_size size, + bool invert_x, bool invert_y) { + if (invert_x) { + point.x = size.width - point.x; } - - uint16_t mod = from->keysym.mod; - if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, - prefer_text)) { - return false; + if (invert_y) { + point.y = size.height - point.y; } - - to->inject_keycode.repeat = repeat; - to->inject_keycode.metastate = convert_meta_state(mod); - - return true; + return point; } static void -input_manager_process_key(struct input_manager *im, - const SDL_KeyboardEvent *event) { - // control: indicates the state of the command-line option --no-control - bool control = im->control; - - struct controller *controller = im->controller; +sc_input_manager_process_key(struct sc_input_manager *im, + const SDL_KeyboardEvent *event) { + // controller is NULL if --no-control is requested + bool control = im->controller; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; @@ -425,119 +424,149 @@ input_manager_process_key(struct input_manager *im, // The shortcut modifier is pressed if (smod) { - int action = down ? ACTION_DOWN : ACTION_UP; + enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: - if (control && !shift && !repeat) { - action_home(controller, action); + if (im->kp && !shift && !repeat) { + action_home(im, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: - if (control && !shift && !repeat) { - action_back(controller, action); + if (im->kp && !shift && !repeat) { + action_back(im, action); } return; case SDLK_s: - if (control && !shift && !repeat) { - action_app_switch(controller, action); + if (im->kp && !shift && !repeat) { + action_app_switch(im, action); } return; case SDLK_m: - if (control && !shift && !repeat) { - action_menu(controller, action); + if (im->kp && !shift && !repeat) { + action_menu(im, action); } return; case SDLK_p: - if (control && !shift && !repeat) { - action_power(controller, action); + if (im->kp && !shift && !repeat) { + action_power(im, action); } return; case SDLK_o: if (control && !repeat && down) { - enum screen_power_mode mode = shift - ? SCREEN_POWER_MODE_NORMAL - : SCREEN_POWER_MODE_OFF; - set_screen_power_mode(controller, mode); + enum sc_screen_power_mode mode = shift + ? SC_SCREEN_POWER_MODE_NORMAL + : SC_SCREEN_POWER_MODE_OFF; + set_screen_power_mode(im, mode); } return; case SDLK_DOWN: - if (control && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_180); + } + } else if (im->kp) { // forward repeated events - action_volume_down(controller, action); + action_volume_down(im, action); } return; case SDLK_UP: - if (control && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_180); + } + } else if (im->kp) { // forward repeated events - action_volume_up(controller, action); + action_volume_up(im, action); } return; case SDLK_LEFT: - if (!shift && !repeat && down) { - rotate_client_left(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im, + SC_ORIENTATION_270); + } } return; case SDLK_RIGHT: - if (!shift && !repeat && down) { - rotate_client_right(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im, + SC_ORIENTATION_90); + } } return; case SDLK_c: - if (control && !shift && !repeat) { - action_copy(controller, action); + if (im->kp && !shift && !repeat && down) { + get_device_clipboard(im, SC_COPY_KEY_COPY); } return; case SDLK_x: - if (control && !shift && !repeat) { - action_cut(controller, action); + if (im->kp && !shift && !repeat && down) { + get_device_clipboard(im, SC_COPY_KEY_CUT); } return; case SDLK_v: - if (control && !repeat && down) { + if (im->kp && !repeat && down) { if (shift || im->legacy_paste) { // inject the text as input events - clipboard_paste(controller); + clipboard_paste(im); } else { - // store the text in the device clipboard and paste - set_device_clipboard(controller, true); + // store the text in the device clipboard and paste, + // without requesting an acknowledgment + set_device_clipboard(im, true, SC_SEQUENCE_INVALID); } } return; case SDLK_f: if (!shift && !repeat && down) { - screen_switch_fullscreen(im->screen); + sc_screen_switch_fullscreen(im->screen); } return; case SDLK_w: if (!shift && !repeat && down) { - screen_resize_to_fit(im->screen); + sc_screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && !repeat && down) { - screen_resize_to_pixel_perfect(im->screen); + sc_screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: if (!shift && !repeat && down) { - switch_fps_counter_state(&im->screen->fps_counter); + switch_fps_counter_state(im); } return; case SDLK_n: if (control && !repeat && down) { if (shift) { - collapse_panels(controller); + collapse_panels(im); } else if (im->key_repeat == 0) { - expand_notification_panel(controller); + expand_notification_panel(im); } else { - expand_settings_panel(controller); + expand_settings_panel(im); } } return; case SDLK_r: if (control && !shift && !repeat && down) { - rotate_device(controller); + rotate_device(im); + } + return; + case SDLK_k: + if (control && !shift && !repeat && down + && im->kp && im->kp->hid) { + // Only if the current keyboard is hid + open_hard_keyboard_settings(im); } return; } @@ -545,147 +574,128 @@ input_manager_process_key(struct input_manager *im, return; } - if (!control) { + if (!im->kp) { return; } - if (event->repeat) { - if (!im->forward_key_repeat) { - return; - } - ++im->repeat; - } else { - im->repeat = 0; - } - - if (ctrl && !shift && keycode == SDLK_v && down && !repeat) { + uint64_t ack_to_wait = SC_SEQUENCE_INVALID; + bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat; + if (im->clipboard_autosync && is_ctrl_v) { if (im->legacy_paste) { // inject the text as input events - clipboard_paste(controller); + clipboard_paste(im); return; } + + // Request an acknowledgement only if necessary + uint64_t sequence = im->kp->async_paste ? im->next_sequence + : SC_SEQUENCE_INVALID; + // Synchronize the computer clipboard to the device clipboard before // sending Ctrl+v, to allow seamless copy-paste. - set_device_clipboard(controller, false); - } + bool ok = set_device_clipboard(im, false, sequence); + if (!ok) { + LOGW("Clipboard could not be synchronized, Ctrl+v not injected"); + return; + } - struct control_msg msg; - if (convert_input_key(event, &msg, im->prefer_text, im->repeat)) { - if (!controller_push_msg(controller, &msg)) { - LOGW("Could not request 'inject keycode'"); + if (im->kp->async_paste) { + // The key processor must wait for this ack before injecting Ctrl+v + ack_to_wait = sequence; + // Increment only when the request succeeded + ++im->next_sequence; } } -} -static bool -convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point = - screen_convert_window_to_frame_coords(screen, from->x, from->y); - to->inject_touch_event.pressure = 1.f; - to->inject_touch_event.buttons = convert_mouse_buttons(from->state); + struct sc_key_event evt = { + .action = sc_action_from_sdl_keyboard_type(event->type), + .keycode = sc_keycode_from_sdl(event->keysym.sym), + .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .repeat = event->repeat, + .mods_state = sc_mods_state_from_sdl(event->keysym.mod), + }; - return true; + assert(im->kp->ops->process_key); + im->kp->ops->process_key(im->kp, &evt, ack_to_wait); } static void -input_manager_process_mouse_motion(struct input_manager *im, - const SDL_MouseMotionEvent *event) { - 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; - } +sc_input_manager_process_mouse_motion(struct sc_input_manager *im, + const SDL_MouseMotionEvent *event) { + if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } - struct control_msg msg; - if (!convert_mouse_motion(event, im->screen, &msg)) { - return; - } - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse motion event'"); - } + struct sc_mouse_motion_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = sc_screen_convert_window_to_frame_coords(im->screen, + event->x, + event->y), + }, + .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, + .xrel = event->xrel, + .yrel = event->yrel, + .buttons_state = + sc_mouse_buttons_state_from_sdl(event->state, + im->forward_all_clicks), + }; + + assert(im->mp->ops->process_mouse_motion); + im->mp->ops->process_mouse_motion(im->mp, &evt); + + // vfinger must never be used in relative mode + assert(!im->mp->relative_mode || !im->vfinger_down); if (im->vfinger_down) { - struct point mouse = msg.inject_touch_event.position.point; - struct point vfinger = inverse_point(mouse, im->screen->frame_size); + assert(!im->mp->relative_mode); // assert one more time + struct sc_point mouse = + sc_screen_convert_window_to_frame_coords(im->screen, event->x, + event->y); + struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, + im->vfinger_invert_x, + im->vfinger_invert_y); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } -static bool -convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { - return false; +static void +sc_input_manager_process_touch(struct sc_input_manager *im, + const SDL_TouchFingerEvent *event) { + if (!im->mp->ops->process_touch) { + // The mouse processor does not support touch events + return; } - to->inject_touch_event.pointer_id = from->fingerId; - to->inject_touch_event.position.screen_size = screen->frame_size; - int dw; int dh; - SDL_GL_GetDrawableSize(screen->window, &dw, &dh); + SDL_GL_GetDrawableSize(im->screen->window, &dw, &dh); // SDL touch event coordinates are normalized in the range [0; 1] - int32_t x = from->x * dw; - int32_t y = from->y * dh; - to->inject_touch_event.position.point = - screen_convert_drawable_to_frame_coords(screen, x, y); - - to->inject_touch_event.pressure = from->pressure; - to->inject_touch_event.buttons = 0; - return true; -} - -static void -input_manager_process_touch(struct input_manager *im, - const SDL_TouchFingerEvent *event) { - struct control_msg msg; - if (convert_touch(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject touch event'"); - } - } -} - -static bool -convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, - struct control_msg *to) { - to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; - - if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { - return false; - } - - to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; - to->inject_touch_event.position.screen_size = screen->frame_size; - to->inject_touch_event.position.point = - screen_convert_window_to_frame_coords(screen, from->x, from->y); - to->inject_touch_event.pressure = - from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f; - to->inject_touch_event.buttons = - convert_mouse_buttons(SDL_BUTTON(from->button)); + int32_t x = event->x * dw; + int32_t y = event->y * dh; + + struct sc_touch_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = + sc_screen_convert_drawable_to_frame_coords(im->screen, x, y), + }, + .action = sc_touch_action_from_sdl(event->type), + .pointer_id = event->fingerId, + .pressure = event->pressure, + }; - return true; + im->mp->ops->process_touch(im->mp, &evt); } static void -input_manager_process_mouse_button(struct input_manager *im, - const SDL_MouseButtonEvent *event) { - bool control = im->control; +sc_input_manager_process_mouse_button(struct sc_input_manager *im, + const SDL_MouseButtonEvent *event) { + bool control = im->controller; if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate @@ -694,40 +704,42 @@ input_manager_process_mouse_button(struct input_manager *im, bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { - int action = down ? ACTION_DOWN : ACTION_UP; + if (control) { + enum sc_action action = down ? SC_ACTION_DOWN : SC_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); + if (im->kp && event->button == SDL_BUTTON_X1) { + action_app_switch(im, action); + return; + } + if (event->button == SDL_BUTTON_X2 && down) { + if (event->clicks < 2) { + expand_notification_panel(im); + } else { + expand_settings_panel(im); + } + return; + } + if (im->kp && event->button == SDL_BUTTON_RIGHT) { + press_back_or_turn_screen_on(im, action); + return; + } + if (im->kp && event->button == SDL_BUTTON_MIDDLE) { + action_home(im, action); + return; } - return; - } - if (control && event->button == SDL_BUTTON_RIGHT) { - press_back_or_turn_screen_on(im->controller, action); - return; - } - if (control && event->button == SDL_BUTTON_MIDDLE) { - 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) { int32_t x = event->x; int32_t y = event->y; - screen_hidpi_scale_coords(im->screen, &x, &y); + sc_screen_hidpi_scale_coords(im->screen, &x, &y); SDL_Rect *r = &im->screen->rect; bool outside = x < r->x || x >= r->x + r->w || y < r->y || y >= r->y + r->h; if (outside) { if (down) { - screen_resize_to_fit(im->screen); + sc_screen_resize_to_fit(im->screen); } return; } @@ -735,21 +747,38 @@ input_manager_process_mouse_button(struct input_manager *im, // otherwise, send the click event to the device } - if (!control) { + if (!im->mp) { return; } - struct control_msg msg; - if (!convert_mouse_button(event, im->screen, &msg)) { - return; - } + uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + + struct sc_mouse_click_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = sc_screen_convert_window_to_frame_coords(im->screen, + event->x, + event->y), + }, + .action = sc_action_from_sdl_mousebutton_type(event->type), + .button = sc_mouse_button_from_sdl(event->button), + .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE + : POINTER_ID_GENERIC_FINGER, + .buttons_state = + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, + im->forward_all_clicks), + }; + + assert(im->mp->ops->process_mouse_click); + im->mp->ops->process_mouse_click(im->mp, &evt); - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse button event'"); + if (im->mp->relative_mode) { + assert(!im->vfinger_down); // vfinger must not be used in relative mode + // No pinch-to-zoom simulation return; } - // Pinch-to-zoom simulation. + // Pinch-to-zoom, rotate and tilt 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 @@ -758,11 +787,29 @@ input_manager_process_mouse_button(struct input_manager *im, // // 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); + // + // To simulate a tilt gesture (a vertical slide with two fingers), Shift + // can be used instead of Ctrl. The "virtual finger" has a position + // inverted with respect to the vertical axis of symmetry in the middle of + // the screen. + const SDL_Keymod keymod = SDL_GetModState(); + const bool ctrl_pressed = keymod & KMOD_CTRL; + const bool shift_pressed = keymod & KMOD_SHIFT; + if (event->button == SDL_BUTTON_LEFT && + ((down && !im->vfinger_down && + ((ctrl_pressed && !shift_pressed) || + (!ctrl_pressed && shift_pressed))) || + (!down && im->vfinger_down))) { + struct sc_point mouse = + sc_screen_convert_window_to_frame_coords(im->screen, event->x, + event->y); + if (down) { + im->vfinger_invert_x = ctrl_pressed || shift_pressed; + im->vfinger_invert_y = ctrl_pressed; + } + struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, + im->vfinger_invert_x, + im->vfinger_invert_y); enum android_motionevent_action action = down ? AMOTION_EVENT_ACTION_DOWN : AMOTION_EVENT_ACTION_UP; @@ -773,80 +820,115 @@ input_manager_process_mouse_button(struct input_manager *im, } } -static bool -convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, - struct control_msg *to) { +static void +sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, + const SDL_MouseWheelEvent *event) { + if (!im->mp->ops->process_mouse_scroll) { + // The mouse processor does not support scroll events + return; + } // mouse_x and mouse_y are expressed in pixels relative to the window int mouse_x; int mouse_y; - SDL_GetMouseState(&mouse_x, &mouse_y); - - struct position position = { - .screen_size = screen->frame_size, - .point = screen_convert_window_to_frame_coords(screen, - mouse_x, mouse_y), + uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); + + struct sc_mouse_scroll_event evt = { + .position = { + .screen_size = im->screen->frame_size, + .point = sc_screen_convert_window_to_frame_coords(im->screen, + mouse_x, mouse_y), + }, +#if SDL_VERSION_ATLEAST(2, 0, 18) + .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), + .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), +#else + .hscroll = CLAMP(event->x, -1, 1), + .vscroll = CLAMP(event->y, -1, 1), +#endif + .buttons_state = + sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), }; - to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; - - to->inject_scroll_event.position = position; - to->inject_scroll_event.hscroll = from->x; - to->inject_scroll_event.vscroll = from->y; + im->mp->ops->process_mouse_scroll(im->mp, &evt); +} - return true; +static bool +is_apk(const char *file) { + const char *ext = strrchr(file, '.'); + return ext && !strcmp(ext, ".apk"); } static void -input_manager_process_mouse_wheel(struct input_manager *im, - const SDL_MouseWheelEvent *event) { - struct control_msg msg; - if (convert_mouse_wheel(event, im->screen, &msg)) { - if (!controller_push_msg(im->controller, &msg)) { - LOGW("Could not request 'inject mouse wheel event'"); - } +sc_input_manager_process_file(struct sc_input_manager *im, + const SDL_DropEvent *event) { + char *file = strdup(event->file); + SDL_free(event->file); + if (!file) { + LOG_OOM(); + return; + } + + enum sc_file_pusher_action action; + if (is_apk(file)) { + action = SC_FILE_PUSHER_ACTION_INSTALL_APK; + } else { + action = SC_FILE_PUSHER_ACTION_PUSH_FILE; + } + bool ok = sc_file_pusher_request(im->fp, action, file); + if (!ok) { + free(file); } } -bool -input_manager_handle_event(struct input_manager *im, SDL_Event *event) { +void +sc_input_manager_handle_event(struct sc_input_manager *im, + const SDL_Event *event) { + bool control = im->controller; switch (event->type) { case SDL_TEXTINPUT: - if (!im->control) { - return true; + if (!im->kp) { + break; } - input_manager_process_text_input(im, &event->text); - return true; + sc_input_manager_process_text_input(im, &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(im, &event->key); - return true; + sc_input_manager_process_key(im, &event->key); + break; case SDL_MOUSEMOTION: - if (!im->control) { + if (!im->mp) { break; } - input_manager_process_mouse_motion(im, &event->motion); - return true; + sc_input_manager_process_mouse_motion(im, &event->motion); + break; case SDL_MOUSEWHEEL: - if (!im->control) { + if (!im->mp) { break; } - input_manager_process_mouse_wheel(im, &event->wheel); - return true; + sc_input_manager_process_mouse_wheel(im, &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(im, &event->button); - return true; + sc_input_manager_process_mouse_button(im, &event->button); + break; case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: - input_manager_process_touch(im, &event->tfinger); - return true; + if (!im->mp) { + break; + } + sc_input_manager_process_touch(im, &event->tfinger); + break; + case SDL_DROPFILE: { + if (!control) { + break; + } + sc_input_manager_process_file(im, &event->drop); + } } - - return false; } diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 1dd7825f..2ce11b03 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -1,5 +1,5 @@ -#ifndef INPUTMANAGER_H -#define INPUTMANAGER_H +#ifndef SC_INPUTMANAGER_H +#define SC_INPUTMANAGER_H #include "common.h" @@ -8,23 +8,23 @@ #include #include "controller.h" +#include "file_pusher.h" #include "fps_counter.h" -#include "scrcpy.h" -#include "screen.h" +#include "options.h" +#include "trait/key_processor.h" +#include "trait/mouse_processor.h" -struct input_manager { - struct controller *controller; - struct screen *screen; +struct sc_input_manager { + struct sc_controller *controller; + struct sc_file_pusher *fp; + struct sc_screen *screen; - // SDL reports repeated events as a boolean, but Android expects the actual - // number of repetitions. This variable keeps track of the count. - unsigned repeat; + struct sc_key_processor *kp; + struct sc_mouse_processor *mp; - bool control; - bool forward_key_repeat; - bool prefer_text; bool forward_all_clicks; bool legacy_paste; + bool clipboard_autosync; struct { unsigned data[SC_MAX_SHORTCUT_MODS]; @@ -32,6 +32,8 @@ struct input_manager { } sdl_shortcut_mods; bool vfinger_down; + bool vfinger_invert_x; + bool vfinger_invert_y; // Tracks the number of identical consecutive shortcut key down events. // Not to be confused with event->repeat, which counts the number of @@ -39,13 +41,29 @@ struct input_manager { unsigned key_repeat; SDL_Keycode last_keycode; uint16_t last_mod; + + uint64_t next_sequence; // used for request acknowledgements +}; + +struct sc_input_manager_params { + struct sc_controller *controller; + struct sc_file_pusher *fp; + struct sc_screen *screen; + struct sc_key_processor *kp; + struct sc_mouse_processor *mp; + + bool forward_all_clicks; + bool legacy_paste; + bool clipboard_autosync; + const struct sc_shortcut_mods *shortcut_mods; }; void -input_manager_init(struct input_manager *im, struct controller *controller, - struct screen *screen, const struct scrcpy_options *options); +sc_input_manager_init(struct sc_input_manager *im, + const struct sc_input_manager_params *params); -bool -input_manager_handle_event(struct input_manager *im, SDL_Event *event); +void +sc_input_manager_handle_event(struct sc_input_manager *im, + const SDL_Event *event); #endif diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c new file mode 100644 index 00000000..00b7f92a --- /dev/null +++ b/app/src/keyboard_sdk.c @@ -0,0 +1,345 @@ +#include "keyboard_sdk.h" + +#include + +#include "android/input.h" +#include "control_msg.h" +#include "controller.h" +#include "input_events.h" +#include "util/intmap.h" +#include "util/log.h" + +/** Downcast key processor to sc_keyboard_sdk */ +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_sdk, key_processor) + +static enum android_keyevent_action +convert_keycode_action(enum sc_action action) { + if (action == SC_ACTION_DOWN) { + return AKEY_EVENT_ACTION_DOWN; + } + assert(action == SC_ACTION_UP); + return AKEY_EVENT_ACTION_UP; +} + +static bool +convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, + enum sc_key_inject_mode key_inject_mode) { + // Navigation keys and ENTER. + // Used in all modes. + static const struct sc_intmap_entry special_keys[] = { + {SC_KEYCODE_RETURN, AKEYCODE_ENTER}, + {SC_KEYCODE_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, + {SC_KEYCODE_ESCAPE, AKEYCODE_ESCAPE}, + {SC_KEYCODE_BACKSPACE, AKEYCODE_DEL}, + {SC_KEYCODE_TAB, AKEYCODE_TAB}, + {SC_KEYCODE_PAGEUP, AKEYCODE_PAGE_UP}, + {SC_KEYCODE_DELETE, AKEYCODE_FORWARD_DEL}, + {SC_KEYCODE_HOME, AKEYCODE_MOVE_HOME}, + {SC_KEYCODE_END, AKEYCODE_MOVE_END}, + {SC_KEYCODE_PAGEDOWN, AKEYCODE_PAGE_DOWN}, + {SC_KEYCODE_RIGHT, AKEYCODE_DPAD_RIGHT}, + {SC_KEYCODE_LEFT, AKEYCODE_DPAD_LEFT}, + {SC_KEYCODE_DOWN, AKEYCODE_DPAD_DOWN}, + {SC_KEYCODE_UP, AKEYCODE_DPAD_UP}, + {SC_KEYCODE_LCTRL, AKEYCODE_CTRL_LEFT}, + {SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT}, + {SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT}, + {SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT}, + }; + + // Numpad navigation keys. + // Used in all modes, when NumLock and Shift are disabled. + static const struct sc_intmap_entry kp_nav_keys[] = { + {SC_KEYCODE_KP_0, AKEYCODE_INSERT}, + {SC_KEYCODE_KP_1, AKEYCODE_MOVE_END}, + {SC_KEYCODE_KP_2, AKEYCODE_DPAD_DOWN}, + {SC_KEYCODE_KP_3, AKEYCODE_PAGE_DOWN}, + {SC_KEYCODE_KP_4, AKEYCODE_DPAD_LEFT}, + {SC_KEYCODE_KP_6, AKEYCODE_DPAD_RIGHT}, + {SC_KEYCODE_KP_7, AKEYCODE_MOVE_HOME}, + {SC_KEYCODE_KP_8, AKEYCODE_DPAD_UP}, + {SC_KEYCODE_KP_9, AKEYCODE_PAGE_UP}, + {SC_KEYCODE_KP_PERIOD, AKEYCODE_FORWARD_DEL}, + }; + + // Letters and space. + // Used in non-text mode. + static const struct sc_intmap_entry alphaspace_keys[] = { + {SC_KEYCODE_a, AKEYCODE_A}, + {SC_KEYCODE_b, AKEYCODE_B}, + {SC_KEYCODE_c, AKEYCODE_C}, + {SC_KEYCODE_d, AKEYCODE_D}, + {SC_KEYCODE_e, AKEYCODE_E}, + {SC_KEYCODE_f, AKEYCODE_F}, + {SC_KEYCODE_g, AKEYCODE_G}, + {SC_KEYCODE_h, AKEYCODE_H}, + {SC_KEYCODE_i, AKEYCODE_I}, + {SC_KEYCODE_j, AKEYCODE_J}, + {SC_KEYCODE_k, AKEYCODE_K}, + {SC_KEYCODE_l, AKEYCODE_L}, + {SC_KEYCODE_m, AKEYCODE_M}, + {SC_KEYCODE_n, AKEYCODE_N}, + {SC_KEYCODE_o, AKEYCODE_O}, + {SC_KEYCODE_p, AKEYCODE_P}, + {SC_KEYCODE_q, AKEYCODE_Q}, + {SC_KEYCODE_r, AKEYCODE_R}, + {SC_KEYCODE_s, AKEYCODE_S}, + {SC_KEYCODE_t, AKEYCODE_T}, + {SC_KEYCODE_u, AKEYCODE_U}, + {SC_KEYCODE_v, AKEYCODE_V}, + {SC_KEYCODE_w, AKEYCODE_W}, + {SC_KEYCODE_x, AKEYCODE_X}, + {SC_KEYCODE_y, AKEYCODE_Y}, + {SC_KEYCODE_z, AKEYCODE_Z}, + {SC_KEYCODE_SPACE, AKEYCODE_SPACE}, + }; + + // Numbers and punctuation keys. + // Used in raw mode only. + static const struct sc_intmap_entry numbers_punct_keys[] = { + {SC_KEYCODE_HASH, AKEYCODE_POUND}, + {SC_KEYCODE_PERCENT, AKEYCODE_PERIOD}, + {SC_KEYCODE_QUOTE, AKEYCODE_APOSTROPHE}, + {SC_KEYCODE_ASTERISK, AKEYCODE_STAR}, + {SC_KEYCODE_PLUS, AKEYCODE_PLUS}, + {SC_KEYCODE_COMMA, AKEYCODE_COMMA}, + {SC_KEYCODE_MINUS, AKEYCODE_MINUS}, + {SC_KEYCODE_PERIOD, AKEYCODE_PERIOD}, + {SC_KEYCODE_SLASH, AKEYCODE_SLASH}, + {SC_KEYCODE_0, AKEYCODE_0}, + {SC_KEYCODE_1, AKEYCODE_1}, + {SC_KEYCODE_2, AKEYCODE_2}, + {SC_KEYCODE_3, AKEYCODE_3}, + {SC_KEYCODE_4, AKEYCODE_4}, + {SC_KEYCODE_5, AKEYCODE_5}, + {SC_KEYCODE_6, AKEYCODE_6}, + {SC_KEYCODE_7, AKEYCODE_7}, + {SC_KEYCODE_8, AKEYCODE_8}, + {SC_KEYCODE_9, AKEYCODE_9}, + {SC_KEYCODE_SEMICOLON, AKEYCODE_SEMICOLON}, + {SC_KEYCODE_EQUALS, AKEYCODE_EQUALS}, + {SC_KEYCODE_AT, AKEYCODE_AT}, + {SC_KEYCODE_LEFTBRACKET, AKEYCODE_LEFT_BRACKET}, + {SC_KEYCODE_BACKSLASH, AKEYCODE_BACKSLASH}, + {SC_KEYCODE_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET}, + {SC_KEYCODE_BACKQUOTE, AKEYCODE_GRAVE}, + {SC_KEYCODE_KP_1, AKEYCODE_NUMPAD_1}, + {SC_KEYCODE_KP_2, AKEYCODE_NUMPAD_2}, + {SC_KEYCODE_KP_3, AKEYCODE_NUMPAD_3}, + {SC_KEYCODE_KP_4, AKEYCODE_NUMPAD_4}, + {SC_KEYCODE_KP_5, AKEYCODE_NUMPAD_5}, + {SC_KEYCODE_KP_6, AKEYCODE_NUMPAD_6}, + {SC_KEYCODE_KP_7, AKEYCODE_NUMPAD_7}, + {SC_KEYCODE_KP_8, AKEYCODE_NUMPAD_8}, + {SC_KEYCODE_KP_9, AKEYCODE_NUMPAD_9}, + {SC_KEYCODE_KP_0, AKEYCODE_NUMPAD_0}, + {SC_KEYCODE_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE}, + {SC_KEYCODE_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY}, + {SC_KEYCODE_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT}, + {SC_KEYCODE_KP_PLUS, AKEYCODE_NUMPAD_ADD}, + {SC_KEYCODE_KP_PERIOD, AKEYCODE_NUMPAD_DOT}, + {SC_KEYCODE_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS}, + {SC_KEYCODE_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN}, + {SC_KEYCODE_KP_RIGHTPAREN, AKEYCODE_NUMPAD_RIGHT_PAREN}, + }; + + const struct sc_intmap_entry *entry = + SC_INTMAP_FIND_ENTRY(special_keys, from); + if (entry) { + *to = entry->value; + return true; + } + + if (!(mod & (SC_MOD_NUM | SC_MOD_LSHIFT | SC_MOD_RSHIFT))) { + // Handle Numpad events when Num Lock is disabled + // If SHIFT is pressed, a text event will be sent instead + entry = SC_INTMAP_FIND_ENTRY(kp_nav_keys, from); + if (entry) { + *to = entry->value; + return true; + } + } + + if (key_inject_mode == SC_KEY_INJECT_MODE_TEXT && + !(mod & (SC_MOD_LCTRL | SC_MOD_RCTRL))) { + // do not forward alpha and space key events (unless Ctrl is pressed) + return false; + } + + if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) { + return false; + } + + // if ALT and META are not pressed, also handle letters and space + entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); + if (entry) { + *to = entry->value; + return true; + } + + if (key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + entry = SC_INTMAP_FIND_ENTRY(numbers_punct_keys, from); + if (entry) { + *to = entry->value; + return true; + } + } + + return false; +} + +static enum android_metastate +autocomplete_metastate(enum android_metastate metastate) { + // fill dependent flags + if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { + metastate |= AMETA_SHIFT_ON; + } + if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { + metastate |= AMETA_CTRL_ON; + } + if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { + metastate |= AMETA_ALT_ON; + } + if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { + metastate |= AMETA_META_ON; + } + + return metastate; +} + +static enum android_metastate +convert_meta_state(uint16_t mod) { + enum android_metastate metastate = 0; + if (mod & SC_MOD_LSHIFT) { + metastate |= AMETA_SHIFT_LEFT_ON; + } + if (mod & SC_MOD_RSHIFT) { + metastate |= AMETA_SHIFT_RIGHT_ON; + } + if (mod & SC_MOD_LCTRL) { + metastate |= AMETA_CTRL_LEFT_ON; + } + if (mod & SC_MOD_RCTRL) { + metastate |= AMETA_CTRL_RIGHT_ON; + } + if (mod & SC_MOD_LALT) { + metastate |= AMETA_ALT_LEFT_ON; + } + if (mod & SC_MOD_RALT) { + metastate |= AMETA_ALT_RIGHT_ON; + } + if (mod & SC_MOD_LGUI) { // Windows key + metastate |= AMETA_META_LEFT_ON; + } + if (mod & SC_MOD_RGUI) { // Windows key + metastate |= AMETA_META_RIGHT_ON; + } + if (mod & SC_MOD_NUM) { + metastate |= AMETA_NUM_LOCK_ON; + } + if (mod & SC_MOD_CAPS) { + metastate |= AMETA_CAPS_LOCK_ON; + } + + // fill the dependent fields + return autocomplete_metastate(metastate); +} + +static bool +convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, + enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { + msg->type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; + + if (!convert_keycode(event->keycode, &msg->inject_keycode.keycode, + event->mods_state, key_inject_mode)) { + return false; + } + + msg->inject_keycode.action = convert_keycode_action(event->action); + msg->inject_keycode.repeat = repeat; + msg->inject_keycode.metastate = convert_meta_state(event->mods_state); + + return true; +} + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const struct sc_key_event *event, + uint64_t ack_to_wait) { + // The device clipboard synchronization and the key event messages are + // serialized, there is nothing special to do to ensure that the clipboard + // is set before injecting Ctrl+v. + (void) ack_to_wait; + + struct sc_keyboard_sdk *kb = DOWNCAST(kp); + + if (event->repeat) { + if (!kb->forward_key_repeat) { + return; + } + ++kb->repeat; + } else { + kb->repeat = 0; + } + + struct sc_control_msg msg; + if (convert_input_key(event, &msg, kb->key_inject_mode, kb->repeat)) { + if (!sc_controller_push_msg(kb->controller, &msg)) { + LOGW("Could not request 'inject keycode'"); + } + } +} + +static void +sc_key_processor_process_text(struct sc_key_processor *kp, + const struct sc_text_event *event) { + struct sc_keyboard_sdk *kb = DOWNCAST(kp); + + if (kb->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { + // Never inject text events + return; + } + + if (kb->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { + char c = event->text[0]; + if (isalpha(c) || c == ' ') { + assert(event->text[1] == '\0'); + // Letters and space are handled as raw key events + return; + } + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; + msg.inject_text.text = strdup(event->text); + if (!msg.inject_text.text) { + LOGW("Could not strdup input text"); + return; + } + if (!sc_controller_push_msg(kb->controller, &msg)) { + free(msg.inject_text.text); + LOGW("Could not request 'inject text'"); + } +} + +void +sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, + struct sc_controller *controller, + enum sc_key_inject_mode key_inject_mode, + bool forward_key_repeat) { + kb->controller = controller; + kb->key_inject_mode = key_inject_mode; + kb->forward_key_repeat = forward_key_repeat; + + kb->repeat = 0; + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + .process_text = sc_key_processor_process_text, + }; + + // Key injection and clipboard synchronization are serialized + kb->key_processor.async_paste = false; + kb->key_processor.hid = false; + kb->key_processor.ops = &ops; +} diff --git a/app/src/keyboard_sdk.h b/app/src/keyboard_sdk.h new file mode 100644 index 00000000..700ba90b --- /dev/null +++ b/app/src/keyboard_sdk.h @@ -0,0 +1,31 @@ +#ifndef SC_KEYBOARD_SDK_H +#define SC_KEYBOARD_SDK_H + +#include "common.h" + +#include + +#include "controller.h" +#include "options.h" +#include "trait/key_processor.h" + +struct sc_keyboard_sdk { + struct sc_key_processor key_processor; // key processor trait + + struct sc_controller *controller; + + // SDL reports repeated events as a boolean, but Android expects the actual + // number of repetitions. This variable keeps track of the count. + unsigned repeat; + + enum sc_key_inject_mode key_inject_mode; + bool forward_key_repeat; +}; + +void +sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb, + struct sc_controller *controller, + enum sc_key_inject_mode key_inject_mode, + bool forward_key_repeat); + +#endif diff --git a/app/src/main.c b/app/src/main.c index 2afa3c4e..6050de11 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,5 +1,3 @@ -#include "scrcpy.h" - #include "common.h" #include @@ -13,68 +11,62 @@ #include #include "cli.h" +#include "options.h" +#include "scrcpy.h" +#include "usb/scrcpy_otg.h" #include "util/log.h" +#include "util/net.h" +#include "version.h" -static void -print_version(void) { - fprintf(stderr, "scrcpy %s\n\n", SCRCPY_VERSION); - - fprintf(stderr, "dependencies:\n"); - fprintf(stderr, " - SDL %d.%d.%d\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, - SDL_PATCHLEVEL); - fprintf(stderr, " - libavcodec %d.%d.%d\n", LIBAVCODEC_VERSION_MAJOR, - LIBAVCODEC_VERSION_MINOR, - LIBAVCODEC_VERSION_MICRO); - fprintf(stderr, " - libavformat %d.%d.%d\n", LIBAVFORMAT_VERSION_MAJOR, - LIBAVFORMAT_VERSION_MINOR, - LIBAVFORMAT_VERSION_MICRO); - 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); +#ifdef _WIN32 +#include +#include "util/str.h" #endif -} -int -main(int argc, char *argv[]) { -#ifdef __WINDOWS__ +static int +main_scrcpy(int argc, char *argv[]) { +#ifdef _WIN32 // disable buffering, we want logs immediately // even line buffering (setvbuf() with mode _IOLBF) is not sufficient setbuf(stdout, NULL); setbuf(stderr, NULL); #endif + printf("scrcpy " SCRCPY_VERSION + " \n"); + struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, + .pause_on_exit = SC_PAUSE_ON_EXIT_FALSE, }; #ifndef NDEBUG args.opts.log_level = SC_LOG_LEVEL_DEBUG; #endif + enum scrcpy_exit_code ret; + if (!scrcpy_parse_args(&args, argc, argv)) { - return 1; + ret = SCRCPY_EXIT_FAILURE; + goto end; } sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); - return 0; + ret = SCRCPY_EXIT_SUCCESS; + goto end; } if (args.version) { - print_version(); - return 0; + scrcpy_print_version(); + ret = SCRCPY_EXIT_SUCCESS; + goto end; } - LOGI("scrcpy " SCRCPY_VERSION " "); - #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif @@ -85,13 +77,75 @@ main(int argc, char *argv[]) { } #endif - if (avformat_network_init()) { - return 1; + if (!net_init()) { + ret = SCRCPY_EXIT_FAILURE; + goto end; + } + + sc_log_configure(); + +#ifdef HAVE_USB + ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); +#else + ret = scrcpy(&args.opts); +#endif + +end: + if (args.pause_on_exit == SC_PAUSE_ON_EXIT_TRUE || + (args.pause_on_exit == SC_PAUSE_ON_EXIT_IF_ERROR && + ret != SCRCPY_EXIT_SUCCESS)) { + printf("Press Enter to continue...\n"); + getchar(); + } + + return ret; +} + +int +main(int argc, char *argv[]) { +#ifndef _WIN32 + return main_scrcpy(argc, argv); +#else + (void) argc; + (void) argv; + int wargc; + wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); + if (!wargv) { + LOG_OOM(); + return SCRCPY_EXIT_FAILURE; + } + + char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8)); + if (!argv_utf8) { + LOG_OOM(); + LocalFree(wargv); + return SCRCPY_EXIT_FAILURE; + } + + argv_utf8[wargc] = NULL; + + for (int i = 0; i < wargc; ++i) { + argv_utf8[i] = sc_str_from_wchars(wargv[i]); + if (!argv_utf8[i]) { + LOG_OOM(); + for (int j = 0; j < i; ++j) { + free(argv_utf8[j]); + } + LocalFree(wargv); + free(argv_utf8); + return SCRCPY_EXIT_FAILURE; + } } - int res = scrcpy(&args.opts) ? 0 : 1; + LocalFree(wargv); - avformat_network_deinit(); // ignore failure + int ret = main_scrcpy(wargc, argv_utf8); - return res; + for (int i = 0; i < wargc; ++i) { + free(argv_utf8[i]); + } + free(argv_utf8); + + return ret; +#endif } diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c new file mode 100644 index 00000000..620fb52c --- /dev/null +++ b/app/src/mouse_sdk.c @@ -0,0 +1,161 @@ +#include "mouse_sdk.h" + +#include + +#include "android/input.h" +#include "control_msg.h" +#include "controller.h" +#include "input_events.h" +#include "util/intmap.h" +#include "util/log.h" + +/** Downcast mouse processor to sc_mouse_sdk */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_sdk, mouse_processor) + +static enum android_motionevent_buttons +convert_mouse_buttons(uint32_t state) { + enum android_motionevent_buttons buttons = 0; + if (state & SC_MOUSE_BUTTON_LEFT) { + buttons |= AMOTION_EVENT_BUTTON_PRIMARY; + } + if (state & SC_MOUSE_BUTTON_RIGHT) { + buttons |= AMOTION_EVENT_BUTTON_SECONDARY; + } + if (state & SC_MOUSE_BUTTON_MIDDLE) { + buttons |= AMOTION_EVENT_BUTTON_TERTIARY; + } + if (state & SC_MOUSE_BUTTON_X1) { + buttons |= AMOTION_EVENT_BUTTON_BACK; + } + if (state & SC_MOUSE_BUTTON_X2) { + buttons |= AMOTION_EVENT_BUTTON_FORWARD; + } + return buttons; +} + +static enum android_motionevent_action +convert_mouse_action(enum sc_action action) { + if (action == SC_ACTION_DOWN) { + return AMOTION_EVENT_ACTION_DOWN; + } + assert(action == SC_ACTION_UP); + return AMOTION_EVENT_ACTION_UP; +} + +static enum android_motionevent_action +convert_touch_action(enum sc_touch_action action) { + switch (action) { + case SC_TOUCH_ACTION_MOVE: + return AMOTION_EVENT_ACTION_MOVE; + case SC_TOUCH_ACTION_DOWN: + return AMOTION_EVENT_ACTION_DOWN; + default: + assert(action == SC_TOUCH_ACTION_UP); + return AMOTION_EVENT_ACTION_UP; + } +} + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event) { + if (!event->buttons_state) { + // Do not send motion events when no click is pressed + return; + } + + struct sc_mouse_sdk *m = DOWNCAST(mp); + + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = AMOTION_EVENT_ACTION_MOVE, + .pointer_id = event->pointer_id, + .position = event->position, + .pressure = 1.f, + .buttons = convert_mouse_buttons(event->buttons_state), + }, + }; + + if (!sc_controller_push_msg(m->controller, &msg)) { + LOGW("Could not request 'inject mouse motion event'"); + } +} + +static void +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { + struct sc_mouse_sdk *m = DOWNCAST(mp); + + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = convert_mouse_action(event->action), + .pointer_id = event->pointer_id, + .position = event->position, + .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, + .action_button = convert_mouse_buttons(event->button), + .buttons = convert_mouse_buttons(event->buttons_state), + }, + }; + + if (!sc_controller_push_msg(m->controller, &msg)) { + LOGW("Could not request 'inject mouse click event'"); + } +} + +static void +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { + struct sc_mouse_sdk *m = DOWNCAST(mp); + + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + .inject_scroll_event = { + .position = event->position, + .hscroll = event->hscroll, + .vscroll = event->vscroll, + .buttons = convert_mouse_buttons(event->buttons_state), + }, + }; + + if (!sc_controller_push_msg(m->controller, &msg)) { + LOGW("Could not request 'inject mouse scroll event'"); + } +} + +static void +sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, + const struct sc_touch_event *event) { + struct sc_mouse_sdk *m = DOWNCAST(mp); + + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + .inject_touch_event = { + .action = convert_touch_action(event->action), + .pointer_id = event->pointer_id, + .position = event->position, + .pressure = event->pressure, + .buttons = 0, + }, + }; + + if (!sc_controller_push_msg(m->controller, &msg)) { + LOGW("Could not request 'inject touch event'"); + } +} + +void +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller) { + m->controller = controller; + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + .process_touch = sc_mouse_processor_process_touch, + }; + + m->mouse_processor.ops = &ops; + + m->mouse_processor.relative_mode = false; +} diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h new file mode 100644 index 00000000..444a6ad5 --- /dev/null +++ b/app/src/mouse_sdk.h @@ -0,0 +1,21 @@ +#ifndef SC_MOUSE_SDK_H +#define SC_MOUSE_SDK_H + +#include "common.h" + +#include + +#include "controller.h" +#include "screen.h" +#include "trait/mouse_processor.h" + +struct sc_mouse_sdk { + struct sc_mouse_processor mouse_processor; // mouse processor trait + + struct sc_controller *controller; +}; + +void +sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller); + +#endif diff --git a/app/src/opengl.c b/app/src/opengl.c index da05c082..376690af 100644 --- a/app/src/opengl.c +++ b/app/src/opengl.c @@ -28,7 +28,7 @@ sc_opengl_init(struct sc_opengl *gl) { sizeof(OPENGL_ES_PREFIX) - 1); if (gl->is_opengles) { /* skip the prefix */ - version += sizeof(PREFIX) - 1; + version += sizeof(OPENGL_ES_PREFIX) - 1; } int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); diff --git a/app/src/options.c b/app/src/options.c new file mode 100644 index 00000000..7a885aa5 --- /dev/null +++ b/app/src/options.c @@ -0,0 +1,128 @@ +#include "options.h" + +const struct scrcpy_options scrcpy_options_default = { + .serial = NULL, + .crop = NULL, + .record_filename = NULL, + .window_title = NULL, + .push_target = NULL, + .render_driver = NULL, + .video_codec_options = NULL, + .audio_codec_options = NULL, + .video_encoder = NULL, + .audio_encoder = NULL, + .camera_id = NULL, + .camera_size = NULL, + .camera_ar = NULL, + .camera_fps = 0, + .log_level = SC_LOG_LEVEL_INFO, + .video_codec = SC_CODEC_H264, + .audio_codec = SC_CODEC_OPUS, + .video_source = SC_VIDEO_SOURCE_DISPLAY, + .audio_source = SC_AUDIO_SOURCE_AUTO, + .record_format = SC_RECORD_FORMAT_AUTO, + .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO, + .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO, + .camera_facing = SC_CAMERA_FACING_ANY, + .port_range = { + .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, + .last = DEFAULT_LOCAL_PORT_RANGE_LAST, + }, + .tunnel_host = 0, + .tunnel_port = 0, + .shortcut_mods = { + .data = {SC_SHORTCUT_MOD_LALT, SC_SHORTCUT_MOD_LSUPER}, + .count = 2, + }, + .max_size = 0, + .video_bit_rate = 0, + .audio_bit_rate = 0, + .max_fps = 0, + .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, + .display_orientation = SC_ORIENTATION_0, + .record_orientation = SC_ORIENTATION_0, + .window_x = SC_WINDOW_POSITION_UNDEFINED, + .window_y = SC_WINDOW_POSITION_UNDEFINED, + .window_width = 0, + .window_height = 0, + .display_id = 0, + .display_buffer = 0, + .audio_buffer = -1, // depends on the audio format, + .audio_output_buffer = SC_TICK_FROM_MS(5), + .time_limit = 0, +#ifdef HAVE_V4L2 + .v4l2_device = NULL, + .v4l2_buffer = 0, +#endif +#ifdef HAVE_USB + .otg = false, +#endif + .show_touches = false, + .fullscreen = false, + .always_on_top = false, + .control = true, + .video_playback = true, + .audio_playback = true, + .turn_screen_off = false, + .key_inject_mode = SC_KEY_INJECT_MODE_MIXED, + .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, + .clipboard_autosync = true, + .downsize_on_error = true, + .tcpip = false, + .tcpip_dst = NULL, + .select_tcpip = false, + .select_usb = false, + .cleanup = true, + .start_fps_counter = false, + .power_on = true, + .video = true, + .audio = true, + .require_audio = false, + .kill_adb_on_close = false, + .camera_high_speed = false, + .list = 0, +}; + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) { + assert(!(src & ~7)); + assert(!(transform & ~7)); + + unsigned transform_hflip = transform & 4; + unsigned transform_rotation = transform & 3; + unsigned src_hflip = src & 4; + unsigned src_rotation = src & 3; + unsigned src_swap = src & 1; + if (src_swap && transform_hflip) { + // If the src is rotated by 90 or 270 degrees, applying a flipped + // transformation requires an additional 180 degrees rotation to + // compensate for the inversion of the order of multiplication: + // + // hflip1 × rotate1 × hflip2 × rotate2 + // `--------------' `--------------' + // src transform + // + // In the final result, we want all the hflips then all the rotations, + // so we must move hflip2 to the left: + // + // hflip1 × hflip2 × rotate1' × rotate2 + // + // with rotate1' = | rotate1 if src is 0° or 180° + // | rotate1 + 180° if src is 90° or 270° + + src_rotation += 2; + } + + unsigned result_hflip = src_hflip ^ transform_hflip; + unsigned result_rotation = (transform_rotation + src_rotation) % 4; + enum sc_orientation result = result_hflip | result_rotation; + return result; +} diff --git a/app/src/options.h b/app/src/options.h new file mode 100644 index 00000000..5445e7c8 --- /dev/null +++ b/app/src/options.h @@ -0,0 +1,286 @@ +#ifndef SCRCPY_OPTIONS_H +#define SCRCPY_OPTIONS_H + +#include "common.h" + +#include +#include +#include +#include + +#include "util/tick.h" + +enum sc_log_level { + SC_LOG_LEVEL_VERBOSE, + SC_LOG_LEVEL_DEBUG, + SC_LOG_LEVEL_INFO, + SC_LOG_LEVEL_WARN, + SC_LOG_LEVEL_ERROR, +}; + +enum sc_record_format { + SC_RECORD_FORMAT_AUTO, + SC_RECORD_FORMAT_MP4, + SC_RECORD_FORMAT_MKV, + SC_RECORD_FORMAT_M4A, + SC_RECORD_FORMAT_MKA, + SC_RECORD_FORMAT_OPUS, + SC_RECORD_FORMAT_AAC, + SC_RECORD_FORMAT_FLAC, + SC_RECORD_FORMAT_WAV, +}; + +static inline bool +sc_record_format_is_audio_only(enum sc_record_format fmt) { + return fmt == SC_RECORD_FORMAT_M4A + || fmt == SC_RECORD_FORMAT_MKA + || fmt == SC_RECORD_FORMAT_OPUS + || fmt == SC_RECORD_FORMAT_AAC + || fmt == SC_RECORD_FORMAT_FLAC + || fmt == SC_RECORD_FORMAT_WAV; +} + +enum sc_codec { + SC_CODEC_H264, + SC_CODEC_H265, + SC_CODEC_AV1, + SC_CODEC_OPUS, + SC_CODEC_AAC, + SC_CODEC_FLAC, + SC_CODEC_RAW, +}; + +enum sc_video_source { + SC_VIDEO_SOURCE_DISPLAY, + SC_VIDEO_SOURCE_CAMERA, +}; + +enum sc_audio_source { + SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA + SC_AUDIO_SOURCE_OUTPUT, + SC_AUDIO_SOURCE_MIC, +}; + +enum sc_camera_facing { + SC_CAMERA_FACING_ANY, + SC_CAMERA_FACING_FRONT, + SC_CAMERA_FACING_BACK, + SC_CAMERA_FACING_EXTERNAL, +}; + + // ,----- hflip (applied before the rotation) + // | ,--- 180° + // | | ,- 90° clockwise + // | | | +enum sc_orientation { // v v v + SC_ORIENTATION_0, // 0 0 0 + SC_ORIENTATION_90, // 0 0 1 + SC_ORIENTATION_180, // 0 1 0 + SC_ORIENTATION_270, // 0 1 1 + SC_ORIENTATION_FLIP_0, // 1 0 0 + SC_ORIENTATION_FLIP_90, // 1 0 1 + SC_ORIENTATION_FLIP_180, // 1 1 0 + SC_ORIENTATION_FLIP_270, // 1 1 1 +}; + +static inline bool +sc_orientation_is_mirror(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 4; +} + +// Does the orientation swap width and height? +static inline bool +sc_orientation_is_swap(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 1; +} + +static inline enum sc_orientation +sc_orientation_get_rotation(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 3; +} + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform); + +static inline const char * +sc_orientation_get_name(enum sc_orientation orientation) { + switch (orientation) { + case SC_ORIENTATION_0: + return "0"; + case SC_ORIENTATION_90: + return "90"; + case SC_ORIENTATION_180: + return "180"; + case SC_ORIENTATION_270: + return "270"; + case SC_ORIENTATION_FLIP_0: + return "flip0"; + case SC_ORIENTATION_FLIP_90: + return "flip90"; + case SC_ORIENTATION_FLIP_180: + return "flip180"; + case SC_ORIENTATION_FLIP_270: + return "flip270"; + default: + return "(unknown)"; + } +} + +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_90 = 3, + SC_LOCK_VIDEO_ORIENTATION_180 = 2, + SC_LOCK_VIDEO_ORIENTATION_270 = 1, +}; + +enum sc_keyboard_input_mode { + SC_KEYBOARD_INPUT_MODE_AUTO, + SC_KEYBOARD_INPUT_MODE_DISABLED, + SC_KEYBOARD_INPUT_MODE_SDK, + SC_KEYBOARD_INPUT_MODE_UHID, + SC_KEYBOARD_INPUT_MODE_AOA, +}; + +enum sc_mouse_input_mode { + SC_MOUSE_INPUT_MODE_AUTO, + SC_MOUSE_INPUT_MODE_DISABLED, + SC_MOUSE_INPUT_MODE_SDK, + SC_MOUSE_INPUT_MODE_UHID, + SC_MOUSE_INPUT_MODE_AOA, +}; + +enum sc_key_inject_mode { + // Inject special keys, letters and space as key events. + // Inject numbers and punctuation as text events. + // This is the default mode. + SC_KEY_INJECT_MODE_MIXED, + + // Inject special keys as key events. + // Inject letters and space, numbers and punctuation as text events. + SC_KEY_INJECT_MODE_TEXT, + + // Inject everything as key events. + SC_KEY_INJECT_MODE_RAW, +}; + +#define SC_MAX_SHORTCUT_MODS 8 + +enum sc_shortcut_mod { + SC_SHORTCUT_MOD_LCTRL = 1 << 0, + SC_SHORTCUT_MOD_RCTRL = 1 << 1, + SC_SHORTCUT_MOD_LALT = 1 << 2, + SC_SHORTCUT_MOD_RALT = 1 << 3, + SC_SHORTCUT_MOD_LSUPER = 1 << 4, + SC_SHORTCUT_MOD_RSUPER = 1 << 5, +}; + +struct sc_shortcut_mods { + unsigned data[SC_MAX_SHORTCUT_MODS]; + unsigned count; +}; + +struct sc_port_range { + uint16_t first; + uint16_t last; +}; + +#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) + +struct scrcpy_options { + const char *serial; + const char *crop; + const char *record_filename; + const char *window_title; + const char *push_target; + const char *render_driver; + const char *video_codec_options; + const char *audio_codec_options; + const char *video_encoder; + const char *audio_encoder; + const char *camera_id; + const char *camera_size; + const char *camera_ar; + uint16_t camera_fps; + enum sc_log_level log_level; + enum sc_codec video_codec; + enum sc_codec audio_codec; + enum sc_video_source video_source; + enum sc_audio_source audio_source; + enum sc_record_format record_format; + enum sc_keyboard_input_mode keyboard_input_mode; + enum sc_mouse_input_mode mouse_input_mode; + enum sc_camera_facing camera_facing; + struct sc_port_range port_range; + uint32_t tunnel_host; + uint16_t tunnel_port; + struct sc_shortcut_mods shortcut_mods; + uint16_t max_size; + uint32_t video_bit_rate; + uint32_t audio_bit_rate; + uint16_t max_fps; + enum sc_lock_video_orientation lock_video_orientation; + enum sc_orientation display_orientation; + enum sc_orientation record_orientation; + int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" + int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" + uint16_t window_width; + uint16_t window_height; + uint32_t display_id; + sc_tick display_buffer; + sc_tick audio_buffer; + sc_tick audio_output_buffer; + sc_tick time_limit; +#ifdef HAVE_V4L2 + const char *v4l2_device; + sc_tick v4l2_buffer; +#endif +#ifdef HAVE_USB + bool otg; +#endif + bool show_touches; + bool fullscreen; + bool always_on_top; + bool control; + bool video_playback; + bool audio_playback; + bool turn_screen_off; + enum sc_key_inject_mode key_inject_mode; + 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; + bool clipboard_autosync; + bool downsize_on_error; + bool tcpip; + const char *tcpip_dst; + bool select_usb; + bool select_tcpip; + bool cleanup; + bool start_fps_counter; + bool power_on; + bool video; + bool audio; + bool require_audio; + bool kill_adb_on_close; + bool camera_high_speed; +#define SC_OPTION_LIST_ENCODERS 0x1 +#define SC_OPTION_LIST_DISPLAYS 0x2 +#define SC_OPTION_LIST_CAMERAS 0x4 +#define SC_OPTION_LIST_CAMERA_SIZES 0x8 + uint8_t list; +}; + +extern const struct scrcpy_options scrcpy_options_default; + +#endif diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c new file mode 100644 index 00000000..81b02d2c --- /dev/null +++ b/app/src/packet_merger.c @@ -0,0 +1,48 @@ +#include "packet_merger.h" + +#include "util/log.h" + +void +sc_packet_merger_init(struct sc_packet_merger *merger) { + merger->config = NULL; +} + +void +sc_packet_merger_destroy(struct sc_packet_merger *merger) { + free(merger->config); +} + +bool +sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) { + bool is_config = packet->pts == AV_NOPTS_VALUE; + + if (is_config) { + free(merger->config); + + merger->config = malloc(packet->size); + if (!merger->config) { + LOG_OOM(); + return false; + } + + memcpy(merger->config, packet->data, packet->size); + merger->config_size = packet->size; + } else if (merger->config) { + size_t config_size = merger->config_size; + size_t media_size = packet->size; + + if (av_grow_packet(packet, config_size)) { + LOG_OOM(); + return false; + } + + memmove(packet->data + config_size, packet->data, media_size); + memcpy(packet->data, merger->config, config_size); + + free(merger->config); + merger->config = NULL; + // merger->size is meaningless when merger->config is NULL + } + + return true; +} diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h new file mode 100644 index 00000000..e1824c2c --- /dev/null +++ b/app/src/packet_merger.h @@ -0,0 +1,43 @@ +#ifndef SC_PACKET_MERGER_H +#define SC_PACKET_MERGER_H + +#include "common.h" + +#include +#include +#include + +/** + * Config packets (containing the SPS/PPS) are sent in-band. A new config + * packet is sent whenever a new encoding session is started (on start and on + * device orientation change). + * + * Every time a config packet is received, it must be sent alone (for recorder + * extradata), then concatenated to the next media packet (for correct decoding + * and recording). + * + * This helper reads every input packet and modifies each media packet which + * immediately follows a config packet to prepend the config packet payload. + */ + +struct sc_packet_merger { + uint8_t *config; + size_t config_size; +}; + +void +sc_packet_merger_init(struct sc_packet_merger *merger); + +void +sc_packet_merger_destroy(struct sc_packet_merger *merger); + +/** + * If the packet is a config packet, then keep its data for later. + * Otherwise (if the packet is a media packet), then if a config packet is + * pending, prepend the config packet to this packet (so the packet is + * modified!). + */ +bool +sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet); + +#endif diff --git a/app/src/receiver.c b/app/src/receiver.c index 337d2a17..f4ebd3f8 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -1,28 +1,35 @@ #include "receiver.h" #include +#include +#include #include #include "device_msg.h" #include "util/log.h" +#include "util/str.h" bool -receiver_init(struct receiver *receiver, socket_t control_socket) { +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; } + receiver->control_socket = control_socket; + receiver->acksync = NULL; + receiver->uhid_devices = NULL; + return true; } void -receiver_destroy(struct receiver *receiver) { +sc_receiver_destroy(struct sc_receiver *receiver) { sc_mutex_destroy(&receiver->mutex); } static void -process_msg(struct device_msg *msg) { +process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { char *current = SDL_GetClipboardText(); @@ -37,15 +44,66 @@ process_msg(struct device_msg *msg) { SDL_SetClipboardText(msg->clipboard.text); break; } + case DEVICE_MSG_TYPE_ACK_CLIPBOARD: + LOGD("Ack device clipboard sequence=%" PRIu64_, + msg->ack_clipboard.sequence); + + // This is a programming error to receive this message if there is + // no ACK synchronization mechanism + assert(receiver->acksync); + + // Also check at runtime (do not trust the server) + if (!receiver->acksync) { + LOGE("Received unexpected ack"); + return; + } + + sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); + break; + case DEVICE_MSG_TYPE_UHID_OUTPUT: + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + char *hex = sc_str_to_hex_string(msg->uhid_output.data, + msg->uhid_output.size); + if (hex) { + LOGV("UHID output [%" PRIu16 "] %s", + msg->uhid_output.id, hex); + free(hex); + } else { + LOGV("UHID output [%" PRIu16 "] size=%" PRIu16, + msg->uhid_output.id, msg->uhid_output.size); + } + } + + // This is a programming error to receive this message if there is + // no uhid_devices instance + assert(receiver->uhid_devices); + + // Also check at runtime (do not trust the server) + if (!receiver->uhid_devices) { + LOGE("Received unexpected HID output message"); + return; + } + + struct sc_uhid_receiver *uhid_receiver = + sc_uhid_devices_get_receiver(receiver->uhid_devices, + msg->uhid_output.id); + if (uhid_receiver) { + uhid_receiver->ops->process_output(uhid_receiver, + msg->uhid_output.data, + msg->uhid_output.size); + } else { + LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id); + } + break; } } static ssize_t -process_msgs(const unsigned char *buf, size_t len) { +process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) { size_t head = 0; for (;;) { - struct device_msg msg; - ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(&buf[head], len - head, &msg); if (r == -1) { return -1; } @@ -53,8 +111,8 @@ process_msgs(const unsigned char *buf, size_t len) { return head; } - process_msg(&msg); - device_msg_destroy(&msg); + process_msg(receiver, &msg); + sc_device_msg_destroy(&msg); head += r; assert(head <= len); @@ -66,9 +124,9 @@ process_msgs(const unsigned char *buf, size_t len) { static int run_receiver(void *data) { - struct receiver *receiver = data; + struct sc_receiver *receiver = data; - static unsigned char buf[DEVICE_MSG_MAX_SIZE]; + static uint8_t buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { @@ -81,7 +139,7 @@ run_receiver(void *data) { } head += r; - ssize_t consumed = process_msgs(buf, head); + ssize_t consumed = process_msgs(receiver, buf, head); if (consumed == -1) { // an error occurred break; @@ -98,13 +156,13 @@ run_receiver(void *data) { } bool -receiver_start(struct receiver *receiver) { +sc_receiver_start(struct sc_receiver *receiver) { LOGD("Starting receiver thread"); - bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver", - receiver); + bool ok = sc_thread_create(&receiver->thread, run_receiver, + "scrcpy-receiver", receiver); if (!ok) { - LOGC("Could not start receiver thread"); + LOGE("Could not start receiver thread"); return false; } @@ -112,6 +170,6 @@ receiver_start(struct receiver *receiver) { } void -receiver_join(struct receiver *receiver) { +sc_receiver_join(struct sc_receiver *receiver) { sc_thread_join(&receiver->thread, NULL); } diff --git a/app/src/receiver.h b/app/src/receiver.h index 36523b62..ba84c0ab 100644 --- a/app/src/receiver.h +++ b/app/src/receiver.h @@ -1,33 +1,38 @@ -#ifndef RECEIVER_H -#define RECEIVER_H +#ifndef SC_RECEIVER_H +#define SC_RECEIVER_H #include "common.h" #include +#include "uhid/uhid_output.h" +#include "util/acksync.h" #include "util/net.h" #include "util/thread.h" // receive events from the device // managed by the controller -struct receiver { - socket_t control_socket; +struct sc_receiver { + sc_socket control_socket; sc_thread thread; sc_mutex mutex; + + struct sc_acksync *acksync; + struct sc_uhid_devices *uhid_devices; }; bool -receiver_init(struct receiver *receiver, socket_t control_socket); +sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket); void -receiver_destroy(struct receiver *receiver); +sc_receiver_destroy(struct sc_receiver *receiver); bool -receiver_start(struct receiver *receiver); +sc_receiver_start(struct sc_receiver *receiver); -// no receiver_stop(), it will automatically stop on control_socket shutdown +// no sc_receiver_stop(), it will automatically stop on control_socket shutdown void -receiver_join(struct receiver *receiver); +sc_receiver_join(struct sc_receiver *receiver); #endif diff --git a/app/src/recorder.c b/app/src/recorder.c index c98b6b8c..9e0b3395 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,13 +1,19 @@ #include "recorder.h" #include +#include +#include #include +#include #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" -/** Downcast packet_sink to recorder */ -#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) +/** Downcast packet sinks to recorder */ +#define DOWNCAST_VIDEO(SINK) \ + container_of(SINK, struct sc_recorder, video_packet_sink) +#define DOWNCAST_AUDIO(SINK) \ + container_of(SINK, struct sc_recorder, audio_packet_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us @@ -24,62 +30,60 @@ find_muxer(const char *name) { oformat = av_oformat_next(oformat); #endif // until null or containing the requested name - } while (oformat && !strlist_contains(oformat->name, ',', name)); + } while (oformat && !sc_str_list_contains(oformat->name, ',', name)); return oformat; } -static struct record_packet * -record_packet_new(const AVPacket *packet) { - struct record_packet *rec = malloc(sizeof(*rec)); - if (!rec) { +static AVPacket * +sc_recorder_packet_ref(const AVPacket *packet) { + AVPacket *p = av_packet_alloc(); + if (!p) { + LOG_OOM(); return NULL; } - rec->packet = av_packet_alloc(); - if (!rec->packet) { - free(rec); + if (av_packet_ref(p, packet)) { + av_packet_free(&p); return NULL; } - if (av_packet_ref(rec->packet, packet)) { - av_packet_free(&rec->packet); - free(rec); - return NULL; - } - return rec; -} - -static void -record_packet_delete(struct record_packet *rec) { - av_packet_free(&rec->packet); - free(rec); + return p; } static void -recorder_queue_clear(struct recorder_queue *queue) { - while (!sc_queue_is_empty(queue)) { - struct record_packet *rec; - sc_queue_take(queue, next, &rec); - record_packet_delete(rec); +sc_recorder_queue_clear(struct sc_recorder_queue *queue) { + while (!sc_vecdeque_is_empty(queue)) { + AVPacket *p = sc_vecdeque_pop(queue); + av_packet_free(&p); } } static const char * -recorder_get_format_name(enum sc_record_format format) { +sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { - case SC_RECORD_FORMAT_MP4: return "mp4"; - case SC_RECORD_FORMAT_MKV: return "matroska"; - default: return NULL; + case SC_RECORD_FORMAT_MP4: + case SC_RECORD_FORMAT_M4A: + case SC_RECORD_FORMAT_AAC: + return "mp4"; + case SC_RECORD_FORMAT_MKV: + case SC_RECORD_FORMAT_MKA: + return "matroska"; + case SC_RECORD_FORMAT_OPUS: + return "opus"; + case SC_RECORD_FORMAT_FLAC: + return "flac"; + case SC_RECORD_FORMAT_WAV: + return "wav"; + default: + return NULL; } } static bool -recorder_write_header(struct recorder *recorder, const AVPacket *packet) { - AVStream *ostream = recorder->ctx->streams[0]; - +sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) { uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { - LOGC("Could not allocate extradata"); + LOG_OOM(); return false; } @@ -88,310 +92,753 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) { ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; + return true; +} + +static inline void +sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) { + av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base); +} + +static bool +sc_recorder_write_stream(struct sc_recorder *recorder, + struct sc_recorder_stream *st, AVPacket *packet) { + AVStream *stream = recorder->ctx->streams[st->index]; + sc_recorder_rescale_packet(stream, packet); + if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { + LOGD("Fixing PTS non monotonically increasing in stream %d " + "(%" PRIi64 " >= %" PRIi64 ")", + st->index, st->last_pts, packet->pts); + packet->pts = ++st->last_pts; + packet->dts = packet->pts; + } else { + st->last_pts = packet->pts; + } + return av_interleaved_write_frame(recorder->ctx, packet) >= 0; +} + +static inline bool +sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) { + return sc_recorder_write_stream(recorder, &recorder->video_stream, packet); +} + +static inline bool +sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) { + return sc_recorder_write_stream(recorder, &recorder->audio_stream, packet); +} - int ret = avformat_write_header(recorder->ctx, NULL); +static bool +sc_recorder_open_output_file(struct sc_recorder *recorder) { + const char *format_name = sc_recorder_get_format_name(recorder->format); + assert(format_name); + const AVOutputFormat *format = find_muxer(format_name); + if (!format) { + LOGE("Could not find muxer"); + return false; + } + + recorder->ctx = avformat_alloc_context(); + if (!recorder->ctx) { + LOG_OOM(); + return false; + } + + int ret = avio_open(&recorder->ctx->pb, recorder->filename, + AVIO_FLAG_WRITE); if (ret < 0) { - LOGE("Failed to write header to %s", recorder->filename); + LOGE("Failed to open output file: %s", recorder->filename); + avformat_free_context(recorder->ctx); 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); + + LOGI("Recording started to %s file: %s", format_name, recorder->filename); return true; } static void -recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) { - AVStream *ostream = recorder->ctx->streams[0]; - av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); +sc_recorder_close_output_file(struct sc_recorder *recorder) { + avio_close(recorder->ctx->pb); + avformat_free_context(recorder->ctx); +} + +static inline bool +sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) { + if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { + // The video queue is empty + return true; + } + + if (recorder->audio && recorder->audio_expects_config_packet + && sc_vecdeque_is_empty(&recorder->audio_queue)) { + // The audio queue is empty (when audio is enabled) + return true; + } + + // No queue is empty + return false; } static bool -recorder_write(struct recorder *recorder, AVPacket *packet) { - if (!recorder->header_written) { - if (packet->pts != AV_NOPTS_VALUE) { - LOGE("The first packet is not a config packet"); - return false; +sc_recorder_process_header(struct sc_recorder *recorder) { + sc_mutex_lock(&recorder->mutex); + + while (!recorder->stopped && + ((recorder->video && !recorder->video_init) + || (recorder->audio && !recorder->audio_init) + || sc_recorder_must_wait_for_config_packets(recorder))) { + sc_cond_wait(&recorder->cond, &recorder->mutex); + } + + if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { + assert(recorder->stopped); + // If the recorder is stopped, don't process anything if there are not + // at least video packets + sc_mutex_unlock(&recorder->mutex); + return false; + } + + AVPacket *video_pkt = NULL; + if (!sc_vecdeque_is_empty(&recorder->video_queue)) { + assert(recorder->video); + video_pkt = sc_vecdeque_pop(&recorder->video_queue); + } + + AVPacket *audio_pkt = NULL; + if (recorder->audio_expects_config_packet && + !sc_vecdeque_is_empty(&recorder->audio_queue)) { + assert(recorder->audio); + audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); + } + + sc_mutex_unlock(&recorder->mutex); + + int ret = false; + + if (video_pkt) { + if (video_pkt->pts != AV_NOPTS_VALUE) { + LOGE("The first video packet is not a config packet"); + goto end; } - bool ok = recorder_write_header(recorder, packet); + + assert(recorder->video_stream.index >= 0); + AVStream *video_stream = + recorder->ctx->streams[recorder->video_stream.index]; + bool ok = sc_recorder_set_extradata(video_stream, video_pkt); if (!ok) { - return false; + goto end; } - recorder->header_written = true; - return true; } - if (packet->pts == AV_NOPTS_VALUE) { - // ignore config packets - return true; + if (audio_pkt) { + if (audio_pkt->pts != AV_NOPTS_VALUE) { + LOGE("The first audio packet is not a config packet"); + goto end; + } + + assert(recorder->audio_stream.index >= 0); + AVStream *audio_stream = + recorder->ctx->streams[recorder->audio_stream.index]; + bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt); + if (!ok) { + goto end; + } + } + + bool ok = avformat_write_header(recorder->ctx, NULL) >= 0; + if (!ok) { + LOGE("Failed to write header to %s", recorder->filename); + goto end; + } + + ret = true; + +end: + if (video_pkt) { + av_packet_free(&video_pkt); + } + if (audio_pkt) { + av_packet_free(&audio_pkt); } - recorder_rescale_packet(recorder, packet); - return av_write_frame(recorder->ctx, packet) >= 0; + return ret; } -static int -run_recorder(void *data) { - struct recorder *recorder = data; +static bool +sc_recorder_process_packets(struct sc_recorder *recorder) { + int64_t pts_origin = AV_NOPTS_VALUE; + + bool header_written = sc_recorder_process_header(recorder); + if (!header_written) { + return false; + } + + AVPacket *video_pkt = NULL; + AVPacket *audio_pkt = NULL; + + // We can write a video packet only once we received the next one so that + // we can set its duration (next_pts - current_pts) + AVPacket *video_pkt_previous = NULL; + + bool error = false; for (;;) { sc_mutex_lock(&recorder->mutex); - while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) { - sc_cond_wait(&recorder->queue_cond, &recorder->mutex); + while (!recorder->stopped) { + if (recorder->video && !video_pkt && + !sc_vecdeque_is_empty(&recorder->video_queue)) { + // A new packet may be assigned to video_pkt and be processed + break; + } + if (recorder->audio && !audio_pkt + && !sc_vecdeque_is_empty(&recorder->audio_queue)) { + // A new packet may be assigned to audio_pkt and be processed + break; + } + sc_cond_wait(&recorder->cond, &recorder->mutex); + } + + // If stopped is set, continue to process the remaining events (to + // finish the recording) before actually stopping. + + // If there is no video, then the video_queue will remain empty forever + // and video_pkt will always be NULL. + assert(recorder->video || (!video_pkt + && sc_vecdeque_is_empty(&recorder->video_queue))); + + // If there is no audio, then the audio_queue will remain empty forever + // and audio_pkt will always be NULL. + assert(recorder->audio || (!audio_pkt + && sc_vecdeque_is_empty(&recorder->audio_queue))); + + if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { + video_pkt = sc_vecdeque_pop(&recorder->video_queue); } - // if stopped is set, continue to process the remaining events (to - // finish the recording) before actually stopping + if (!audio_pkt && !sc_vecdeque_is_empty(&recorder->audio_queue)) { + audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); + } - if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { + if (recorder->stopped && !video_pkt && !audio_pkt) { + assert(sc_vecdeque_is_empty(&recorder->video_queue)); + assert(sc_vecdeque_is_empty(&recorder->audio_queue)); 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); - if (!ok) { - // failing to write the last frame is not very serious, no - // future frame may depend on it, so the resulting file - // will still be valid - LOGW("Could not record last packet"); - } - record_packet_delete(last); - } break; } - struct record_packet *rec; - sc_queue_take(&recorder->queue, next, &rec); + assert(video_pkt || audio_pkt); // at least one sc_mutex_unlock(&recorder->mutex); - // recorder->previous is only written from this thread, no need to lock - struct record_packet *previous = recorder->previous; - recorder->previous = rec; + // Ignore further config packets (e.g. on device orientation + // change). The next non-config packet will have the config packet + // data prepended. + if (video_pkt && video_pkt->pts == AV_NOPTS_VALUE) { + av_packet_free(&video_pkt); + video_pkt = NULL; + } - if (!previous) { - // we just received the first packet - continue; + if (audio_pkt && audio_pkt->pts == AV_NOPTS_VALUE) { + av_packet_free(&audio_pkt); + audio_pkt = NULL; } - // config packets have no PTS, we must ignore them - if (rec->packet->pts != AV_NOPTS_VALUE - && previous->packet->pts != AV_NOPTS_VALUE) { - // we now know the duration of the previous packet - previous->packet->duration = - rec->packet->pts - previous->packet->pts; + if (pts_origin == AV_NOPTS_VALUE) { + if (!recorder->audio) { + assert(video_pkt); + pts_origin = video_pkt->pts; + } else if (!recorder->video) { + assert(audio_pkt); + pts_origin = audio_pkt->pts; + } else if (video_pkt && audio_pkt) { + pts_origin = MIN(video_pkt->pts, audio_pkt->pts); + } else if (recorder->stopped) { + if (video_pkt) { + // The recorder is stopped without audio, record the video + // packets + pts_origin = video_pkt->pts; + } else { + // Fail if there is no video + error = true; + goto end; + } + } else { + // We need both video and audio packets to initialize pts_origin + continue; + } } - bool ok = recorder_write(recorder, previous->packet); - record_packet_delete(previous); - if (!ok) { - LOGE("Could not record packet"); + assert(pts_origin != AV_NOPTS_VALUE); - sc_mutex_lock(&recorder->mutex); - recorder->failed = true; - // discard pending packets - recorder_queue_clear(&recorder->queue); - sc_mutex_unlock(&recorder->mutex); - break; + if (video_pkt) { + video_pkt->pts -= pts_origin; + video_pkt->dts = video_pkt->pts; + + if (video_pkt_previous) { + // we now know the duration of the previous packet + video_pkt_previous->duration = video_pkt->pts + - video_pkt_previous->pts; + + bool ok = sc_recorder_write_video(recorder, video_pkt_previous); + av_packet_free(&video_pkt_previous); + if (!ok) { + LOGE("Could not record video packet"); + error = true; + goto end; + } + } + + video_pkt_previous = video_pkt; + video_pkt = NULL; } - } - 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; + if (audio_pkt) { + audio_pkt->pts -= pts_origin; + audio_pkt->dts = audio_pkt->pts; + + bool ok = sc_recorder_write_audio(recorder, audio_pkt); + if (!ok) { + LOGE("Could not record audio packet"); + error = true; + goto end; } - } else { - // the recorded file is empty - recorder->failed = true; + + av_packet_free(&audio_pkt); + audio_pkt = NULL; } } - if (recorder->failed) { - LOGE("Recording failed to %s", recorder->filename); - } else { - const char *format_name = recorder_get_format_name(recorder->format); + // Write the last video packet + AVPacket *last = video_pkt_previous; + if (last) { + // assign an arbitrary duration to the last packet + last->duration = 100000; + bool ok = sc_recorder_write_video(recorder, last); + if (!ok) { + // failing to write the last frame is not very serious, no + // future frame may depend on it, so the resulting file + // will still be valid + LOGW("Could not record last packet"); + } + av_packet_free(&last); + } + + int ret = av_write_trailer(recorder->ctx); + if (ret < 0) { + LOGE("Failed to write trailer to %s", recorder->filename); + error = false; + } + +end: + if (video_pkt) { + av_packet_free(&video_pkt); + } + if (audio_pkt) { + av_packet_free(&audio_pkt); + } + + return !error; +} + +static bool +sc_recorder_record(struct sc_recorder *recorder) { + bool ok = sc_recorder_open_output_file(recorder); + if (!ok) { + return false; + } + + ok = sc_recorder_process_packets(recorder); + sc_recorder_close_output_file(recorder); + return ok; +} + +static int +run_recorder(void *data) { + struct sc_recorder *recorder = data; + + // Recording is a background task + bool ok = sc_thread_set_priority(SC_THREAD_PRIORITY_LOW); + (void) ok; // We don't care if it worked + + bool success = sc_recorder_record(recorder); + + sc_mutex_lock(&recorder->mutex); + // Prevent the producer to push any new packet + recorder->stopped = true; + // Discard pending packets + sc_recorder_queue_clear(&recorder->video_queue); + sc_recorder_queue_clear(&recorder->audio_queue); + sc_mutex_unlock(&recorder->mutex); + + if (success) { + const char *format_name = sc_recorder_get_format_name(recorder->format); LOGI("Recording complete to %s file: %s", format_name, recorder->filename); + } else { + LOGE("Recording failed to %s", recorder->filename); } LOGD("Recorder thread ended"); + recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata); + return 0; } static bool -recorder_open(struct recorder *recorder, const AVCodec *input_codec) { - bool ok = sc_mutex_init(&recorder->mutex); - if (!ok) { - LOGC("Could not create mutex"); +sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) { + assert(!sc_orientation_is_mirror(orientation)); + + uint8_t *raw_data; +#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA + AVPacketSideData *sd = + av_packet_side_data_new(&stream->codecpar->coded_side_data, + &stream->codecpar->nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9, 0); + if (!sd) { + LOG_OOM(); return false; } - ok = sc_cond_init(&recorder->queue_cond); - if (!ok) { - LOGC("Could not create cond"); - goto error_mutex_destroy; + raw_data = sd->data; +#else + raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9); + if (!raw_data) { + LOG_OOM(); + return false; } +#endif - sc_queue_init(&recorder->queue); - recorder->stopped = false; - recorder->failed = false; - recorder->header_written = false; - recorder->previous = NULL; + int32_t *matrix = (int32_t *) raw_data; - 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; + unsigned rotation = orientation; + unsigned angle = rotation * 90; + + av_display_rotation_set(matrix, angle); + + return true; +} + +static bool +sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, + AVCodecContext *ctx) { + struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); + // only written from this thread, no need to lock + assert(!recorder->video_init); + + sc_mutex_lock(&recorder->mutex); + if (recorder->stopped) { + sc_mutex_unlock(&recorder->mutex); + return false; } - recorder->ctx = avformat_alloc_context(); - if (!recorder->ctx) { - LOGE("Could not allocate output context"); - goto error_cond_destroy; + AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec); + if (!stream) { + sc_mutex_unlock(&recorder->mutex); + 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; + int r = avcodec_parameters_from_context(stream->codecpar, ctx); + if (r < 0) { + sc_mutex_unlock(&recorder->mutex); + return false; + } - av_dict_set(&recorder->ctx->metadata, "comment", - "Recorded by scrcpy " SCRCPY_VERSION, 0); + recorder->video_stream.index = stream->index; + + if (recorder->orientation != SC_ORIENTATION_0) { + if (!sc_recorder_set_orientation(stream, recorder->orientation)) { + sc_mutex_unlock(&recorder->mutex); + return false; + } - AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); - if (!ostream) { - goto error_avformat_free_context; + LOGI("Record orientation set to %s", + sc_orientation_get_name(recorder->orientation)); } - 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; + recorder->video_init = true; + sc_cond_signal(&recorder->cond); + sc_mutex_unlock(&recorder->mutex); - 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; + return true; +} + +static void +sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); + // only written from this thread, no need to lock + assert(recorder->video_init); + + sc_mutex_lock(&recorder->mutex); + // EOS also stops the recorder + recorder->stopped = true; + sc_cond_signal(&recorder->cond); + sc_mutex_unlock(&recorder->mutex); +} + +static bool +sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_recorder *recorder = DOWNCAST_VIDEO(sink); + // only written from this thread, no need to lock + assert(recorder->video_init); + + sc_mutex_lock(&recorder->mutex); + + if (recorder->stopped) { + // reject any new packet + sc_mutex_unlock(&recorder->mutex); + return false; + } + + AVPacket *rec = sc_recorder_packet_ref(packet); + if (!rec) { + LOG_OOM(); + sc_mutex_unlock(&recorder->mutex); + return false; } - LOGD("Starting recorder thread"); - ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", - recorder); + rec->stream_index = recorder->video_stream.index; + + bool ok = sc_vecdeque_push(&recorder->video_queue, rec); if (!ok) { - LOGC("Could not start recorder thread"); - goto error_avio_close; + LOG_OOM(); + sc_mutex_unlock(&recorder->mutex); + return false; } - LOGI("Recording started to %s file: %s", format_name, recorder->filename); + sc_cond_signal(&recorder->cond); + sc_mutex_unlock(&recorder->mutex); return true; +} -error_avio_close: - avio_close(recorder->ctx->pb); -error_avformat_free_context: - avformat_free_context(recorder->ctx); -error_cond_destroy: - sc_cond_destroy(&recorder->queue_cond); -error_mutex_destroy: - sc_mutex_destroy(&recorder->mutex); +static bool +sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, + AVCodecContext *ctx) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + // only written from this thread, no need to lock + assert(!recorder->audio_init); - return false; + sc_mutex_lock(&recorder->mutex); + + AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec); + if (!stream) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + int r = avcodec_parameters_from_context(stream->codecpar, ctx); + if (r < 0) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + recorder->audio_stream.index = stream->index; + + // A config packet is provided for all supported formats except raw audio + recorder->audio_expects_config_packet = + ctx->codec_id != AV_CODEC_ID_PCM_S16LE; + + recorder->audio_init = true; + sc_cond_signal(&recorder->cond); + sc_mutex_unlock(&recorder->mutex); + + return true; } static void -recorder_close(struct recorder *recorder) { +sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + // only written from this thread, no need to lock + assert(recorder->audio_init); + sc_mutex_lock(&recorder->mutex); + // EOS also stops the recorder recorder->stopped = true; - sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); - - sc_thread_join(&recorder->thread, NULL); - - avio_close(recorder->ctx->pb); - avformat_free_context(recorder->ctx); - sc_cond_destroy(&recorder->queue_cond); - sc_mutex_destroy(&recorder->mutex); } static bool -recorder_push(struct recorder *recorder, const AVPacket *packet) { +sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink, + const AVPacket *packet) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + // only written from this thread, no need to lock + assert(recorder->audio_init); + sc_mutex_lock(&recorder->mutex); - assert(!recorder->stopped); - if (recorder->failed) { - // reject any new packet (this will stop the stream) + if (recorder->stopped) { + // reject any new packet sc_mutex_unlock(&recorder->mutex); return false; } - struct record_packet *rec = record_packet_new(packet); + AVPacket *rec = sc_recorder_packet_ref(packet); if (!rec) { - LOGC("Could not allocate record packet"); + LOG_OOM(); + sc_mutex_unlock(&recorder->mutex); + return false; + } + + rec->stream_index = recorder->audio_stream.index; + + bool ok = sc_vecdeque_push(&recorder->audio_queue, rec); + if (!ok) { + LOG_OOM(); sc_mutex_unlock(&recorder->mutex); return false; } - sc_queue_push(&recorder->queue, next, rec); - sc_cond_signal(&recorder->queue_cond); + sc_cond_signal(&recorder->cond); 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); +sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) { + struct sc_recorder *recorder = DOWNCAST_AUDIO(sink); + assert(recorder->audio); + // only written from this thread, no need to lock + assert(!recorder->audio_init); + + LOGW("Audio stream recording disabled"); + + sc_mutex_lock(&recorder->mutex); + recorder->audio = false; + recorder->audio_init = true; + sc_cond_signal(&recorder->cond); + sc_mutex_unlock(&recorder->mutex); } -static bool -recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { - struct recorder *recorder = DOWNCAST(sink); - return recorder_push(recorder, packet); +static void +sc_recorder_stream_init(struct sc_recorder_stream *stream) { + stream->index = -1; + stream->last_pts = AV_NOPTS_VALUE; } bool -recorder_init(struct recorder *recorder, - const char *filename, - enum sc_record_format format, - struct size declared_frame_size) { +sc_recorder_init(struct sc_recorder *recorder, const char *filename, + enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, + const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { + assert(!sc_orientation_is_mirror(orientation)); + recorder->filename = strdup(filename); if (!recorder->filename) { - LOGE("Could not strdup filename"); + LOG_OOM(); return false; } + bool ok = sc_mutex_init(&recorder->mutex); + if (!ok) { + goto error_free_filename; + } + + ok = sc_cond_init(&recorder->cond); + if (!ok) { + goto error_mutex_destroy; + } + + assert(video || audio); + recorder->video = video; + recorder->audio = audio; + + recorder->orientation = orientation; + + sc_vecdeque_init(&recorder->video_queue); + sc_vecdeque_init(&recorder->audio_queue); + recorder->stopped = false; + + recorder->video_init = false; + recorder->audio_init = false; + + recorder->audio_expects_config_packet = false; + + sc_recorder_stream_init(&recorder->video_stream); + sc_recorder_stream_init(&recorder->audio_stream); + 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, - }; + assert(cbs && cbs->on_ended); + recorder->cbs = cbs; + recorder->cbs_userdata = cbs_userdata; + + if (video) { + static const struct sc_packet_sink_ops video_ops = { + .open = sc_recorder_video_packet_sink_open, + .close = sc_recorder_video_packet_sink_close, + .push = sc_recorder_video_packet_sink_push, + }; + + recorder->video_packet_sink.ops = &video_ops; + } + + if (audio) { + static const struct sc_packet_sink_ops audio_ops = { + .open = sc_recorder_audio_packet_sink_open, + .close = sc_recorder_audio_packet_sink_close, + .push = sc_recorder_audio_packet_sink_push, + .disable = sc_recorder_audio_packet_sink_disable, + }; + + recorder->audio_packet_sink.ops = &audio_ops; + } + + return true; + +error_mutex_destroy: + sc_mutex_destroy(&recorder->mutex); +error_free_filename: + free(recorder->filename); + + return false; +} - recorder->packet_sink.ops = &ops; +bool +sc_recorder_start(struct sc_recorder *recorder) { + bool ok = sc_thread_create(&recorder->thread, run_recorder, + "scrcpy-recorder", recorder); + if (!ok) { + LOGE("Could not start recorder thread"); + return false; + } return true; } void -recorder_destroy(struct recorder *recorder) { +sc_recorder_stop(struct sc_recorder *recorder) { + sc_mutex_lock(&recorder->mutex); + recorder->stopped = true; + sc_cond_signal(&recorder->cond); + sc_mutex_unlock(&recorder->mutex); +} + +void +sc_recorder_join(struct sc_recorder *recorder) { + sc_thread_join(&recorder->thread, NULL); +} + +void +sc_recorder_destroy(struct sc_recorder *recorder) { + sc_cond_destroy(&recorder->cond); + sc_mutex_destroy(&recorder->mutex); free(recorder->filename); } diff --git a/app/src/recorder.h b/app/src/recorder.h index 96caaf5f..d096e79a 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -1,5 +1,5 @@ -#ifndef RECORDER_H -#define RECORDER_H +#ifndef SC_RECORDER_H +#define SC_RECORDER_H #include "common.h" @@ -7,46 +7,81 @@ #include #include "coords.h" -#include "scrcpy.h" +#include "options.h" #include "trait/packet_sink.h" -#include "util/queue.h" #include "util/thread.h" +#include "util/vecdeque.h" -struct record_packet { - AVPacket *packet; - struct record_packet *next; +struct sc_recorder_queue SC_VECDEQUE(AVPacket *); + +struct sc_recorder_stream { + int index; + int64_t last_pts; }; -struct recorder_queue SC_QUEUE(struct record_packet); +struct sc_recorder { + struct sc_packet_sink video_packet_sink; + struct sc_packet_sink audio_packet_sink; + + /* The audio flag is unprotected: + * - it is initialized from sc_recorder_init() from the main thread; + * - it may be reset once from the recorder thread if the audio is + * disabled dynamically. + * + * Therefore, once the recorder thread is started, only the recorder thread + * may access it without data races. + */ + bool audio; + bool video; -struct recorder { - struct sc_packet_sink packet_sink; // packet sink trait + enum sc_orientation orientation; char *filename; enum sc_record_format format; AVFormatContext *ctx; - struct size declared_frame_size; - bool header_written; 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; - - // we can write a packet only once we received the next one so that we can - // set its duration (next_pts - current_pts) - // "previous" is only accessed from the recorder thread, so it does not - // need to be protected by the mutex - struct record_packet *previous; + sc_cond cond; + // set on sc_recorder_stop(), packet_sink close or recording failure + bool stopped; + struct sc_recorder_queue video_queue; + struct sc_recorder_queue audio_queue; + + // wake up the recorder thread once the video or audio codec is known + bool video_init; + bool audio_init; + + bool audio_expects_config_packet; + + struct sc_recorder_stream video_stream; + struct sc_recorder_stream audio_stream; + + const struct sc_recorder_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_recorder_callbacks { + void (*on_ended)(struct sc_recorder *recorder, bool success, + void *userdata); }; bool -recorder_init(struct recorder *recorder, const char *filename, - enum sc_record_format format, struct size declared_frame_size); +sc_recorder_init(struct sc_recorder *recorder, const char *filename, + enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, + const struct sc_recorder_callbacks *cbs, void *cbs_userdata); + +bool +sc_recorder_start(struct sc_recorder *recorder); + +void +sc_recorder_stop(struct sc_recorder *recorder); + +void +sc_recorder_join(struct sc_recorder *recorder); void -recorder_destroy(struct recorder *recorder); +sc_recorder_destroy(struct sc_recorder *recorder); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 6a285788..f43af35e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -13,72 +13,99 @@ # include #endif +#include "audio_player.h" #include "controller.h" #include "decoder.h" +#include "delay_buffer.h" +#include "demuxer.h" #include "events.h" -#include "file_handler.h" -#include "input_manager.h" +#include "file_pusher.h" +#include "keyboard_sdk.h" +#include "mouse_sdk.h" #include "recorder.h" #include "screen.h" #include "server.h" -#include "stream.h" -#include "tiny_xpm.h" +#include "uhid/keyboard_uhid.h" +#include "uhid/mouse_uhid.h" +#ifdef HAVE_USB +# include "usb/aoa_hid.h" +# include "usb/keyboard_aoa.h" +# include "usb/mouse_aoa.h" +# include "usb/usb.h" +#endif +#include "util/acksync.h" #include "util/log.h" #include "util/net.h" +#include "util/rand.h" +#include "util/timeout.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif struct scrcpy { - struct server server; - struct screen screen; - struct stream stream; - struct decoder decoder; - struct recorder recorder; + struct sc_server server; + struct sc_screen screen; + struct sc_audio_player audio_player; + struct sc_demuxer video_demuxer; + struct sc_demuxer audio_demuxer; + struct sc_decoder video_decoder; + struct sc_decoder audio_decoder; + struct sc_recorder recorder; + struct sc_delay_buffer display_buffer; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; + struct sc_delay_buffer v4l2_buffer; +#endif + struct sc_controller controller; + struct sc_file_pusher file_pusher; +#ifdef HAVE_USB + struct sc_usb usb; + struct sc_aoa aoa; + // sequence/ack helper to synchronize clipboard and Ctrl+v via HID + struct sc_acksync acksync; + struct sc_uhid_devices uhid_devices; #endif - struct controller controller; - struct file_handler file_handler; - struct input_manager input_manager; + union { + struct sc_keyboard_sdk keyboard_sdk; + struct sc_keyboard_uhid keyboard_uhid; +#ifdef HAVE_USB + struct sc_keyboard_aoa keyboard_aoa; +#endif + }; + union { + struct sc_mouse_sdk mouse_sdk; + struct sc_mouse_uhid mouse_uhid; +#ifdef HAVE_USB + struct sc_mouse_aoa mouse_aoa; +#endif + }; + struct sc_timeout timeout; }; +static inline void +push_event(uint32_t type, const char *name) { + SDL_Event event; + event.type = type; + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGE("Could not post %s event: %s", name, SDL_GetError()); + // What could we do? + } +} +#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) + #ifdef _WIN32 -BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { +static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { - SDL_Event event; - event.type = SDL_QUIT; - SDL_PushEvent(&event); + PUSH_EVENT(SDL_QUIT); return TRUE; } return FALSE; } #endif // _WIN32 -// init SDL and set appropriate hints -static bool -sdl_init_and_configure(bool display, const char *render_driver, - bool disable_screensaver) { - uint32_t flags = display ? SDL_INIT_VIDEO : SDL_INIT_EVENTS; - if (SDL_Init(flags)) { - LOGC("Could not initialize SDL: %s", SDL_GetError()); - return false; - } - - atexit(SDL_Quit); - -#ifdef _WIN32 - // Clean up properly on Ctrl+C on Windows - bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); - if (!ok) { - LOGW("Could not set Ctrl+C handler"); - } -#endif // _WIN32 - - if (!display) { - return true; - } - +static void +sdl_set_hints(const char *render_driver) { if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { LOGW("Could not set render driver"); } @@ -88,11 +115,18 @@ sdl_init_and_configure(bool display, const char *render_driver, LOGW("Could not enable linear filtering"); } -#ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH // Handle a click to gain focus as any other click if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { LOGW("Could not enable mouse focus clickthrough"); } + +#ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS + // Disable synthetic mouse events from touch events + // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is + // better not to generate them in the first place. + if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) { + LOGW("Could not disable synthetic mouse events"); + } #endif #ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR @@ -106,343 +140,730 @@ sdl_init_and_configure(bool display, const char *render_driver, if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } +} + +static void +sdl_configure(bool video_playback, bool disable_screensaver) { +#ifdef _WIN32 + // Clean up properly on Ctrl+C on Windows + bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); + if (!ok) { + LOGW("Could not set Ctrl+C handler"); + } +#endif // _WIN32 + + if (!video_playback) { + return; + } if (disable_screensaver) { - LOGD("Screensaver disabled"); SDL_DisableScreenSaver(); } else { - LOGD("Screensaver enabled"); SDL_EnableScreenSaver(); } - - return true; } -static bool -is_apk(const char *file) { - const char *ext = strrchr(file, '.'); - return ext && !strcmp(ext, ".apk"); -} - -enum event_result { - EVENT_RESULT_CONTINUE, - EVENT_RESULT_STOPPED_BY_USER, - EVENT_RESULT_STOPPED_BY_EOS, -}; - -static enum event_result -handle_event(struct scrcpy *s, const struct scrcpy_options *options, - SDL_Event *event) { - switch (event->type) { - case EVENT_STREAM_STOPPED: - LOGD("Video stream stopped"); - return EVENT_RESULT_STOPPED_BY_EOS; - case SDL_QUIT: - LOGD("User requested to quit"); - return EVENT_RESULT_STOPPED_BY_USER; - case SDL_DROPFILE: { - if (!options->control) { - break; - } - char *file = strdup(event->drop.file); - SDL_free(event->drop.file); - if (!file) { - LOGW("Could not strdup drop filename\n"); +static enum scrcpy_exit_code +event_loop(struct scrcpy *s) { + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case SC_EVENT_DEVICE_DISCONNECTED: + LOGW("Device disconnected"); + return SCRCPY_EXIT_DISCONNECTED; + case SC_EVENT_DEMUXER_ERROR: + LOGE("Demuxer error"); + return SCRCPY_EXIT_FAILURE; + case SC_EVENT_RECORDER_ERROR: + LOGE("Recorder error"); + return SCRCPY_EXIT_FAILURE; + case SC_EVENT_TIME_LIMIT_REACHED: + LOGI("Time limit reached"); + return SCRCPY_EXIT_SUCCESS; + case SDL_QUIT: + LOGD("User requested to quit"); + return SCRCPY_EXIT_SUCCESS; + default: + if (!sc_screen_handle_event(&s->screen, &event)) { + return SCRCPY_EXIT_FAILURE; + } break; - } - - file_handler_action_t action; - if (is_apk(file)) { - action = ACTION_INSTALL_APK; - } else { - action = ACTION_PUSH_FILE; - } - 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; + return SCRCPY_EXIT_FAILURE; } +// Return true on success, false on error static bool -event_loop(struct scrcpy *s, const struct scrcpy_options *options) { +await_for_server(bool *connected) { SDL_Event event; while (SDL_WaitEvent(&event)) { - enum event_result result = handle_event(s, options, &event); - switch (result) { - case EVENT_RESULT_STOPPED_BY_USER: + switch (event.type) { + case SDL_QUIT: + if (connected) { + *connected = false; + } return true; - case EVENT_RESULT_STOPPED_BY_EOS: - LOGW("Device disconnected"); + case SC_EVENT_SERVER_CONNECTION_FAILED: return false; - case EVENT_RESULT_CONTINUE: + case SC_EVENT_SERVER_CONNECTED: + if (connected) { + *connected = true; + } + return true; + default: break; } } + + LOGE("SDL_WaitEvent() error: %s", SDL_GetError()); return false; } -static SDL_LogPriority -sdl_priority_from_av_level(int level) { - switch (level) { - case AV_LOG_PANIC: - case AV_LOG_FATAL: - return SDL_LOG_PRIORITY_CRITICAL; - case AV_LOG_ERROR: - return SDL_LOG_PRIORITY_ERROR; - case AV_LOG_WARNING: - return SDL_LOG_PRIORITY_WARN; - case AV_LOG_INFO: - return SDL_LOG_PRIORITY_INFO; - } - // do not forward others, which are too verbose - return 0; +static void +sc_recorder_on_ended(struct sc_recorder *recorder, bool success, + void *userdata) { + (void) recorder; + (void) userdata; + + if (!success) { + PUSH_EVENT(SC_EVENT_RECORDER_ERROR); + } } static void -av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { - (void) avcl; - SDL_LogPriority priority = sdl_priority_from_av_level(level); - if (priority == 0) { - return; +sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, + enum sc_demuxer_status status, void *userdata) { + (void) demuxer; + (void) userdata; + + // The device may not decide to disable the video + assert(status != SC_DEMUXER_STATUS_DISABLED); + + if (status == SC_DEMUXER_STATUS_EOS) { + PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + } else { + PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); } +} - size_t fmt_len = strlen(fmt); - char *local_fmt = malloc(fmt_len + 10); - if (!local_fmt) { - LOGC("Could not allocate string"); - return; +static void +sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, + enum sc_demuxer_status status, void *userdata) { + (void) demuxer; + + const struct scrcpy_options *options = userdata; + + // Contrary to the video demuxer, keep mirroring if only the audio fails + // (unless --require-audio is set). + if (status == SC_DEMUXER_STATUS_EOS) { + PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED); + } else if (status == SC_DEMUXER_STATUS_ERROR + || (status == SC_DEMUXER_STATUS_DISABLED + && options->require_audio)) { + PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); } - 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); - free(local_fmt); } static void -stream_on_eos(struct stream *stream, void *userdata) { - (void) stream; +sc_server_on_connection_failed(struct sc_server *server, void *userdata) { + (void) server; (void) userdata; - SDL_Event stop_event; - stop_event.type = EVENT_STREAM_STOPPED; - SDL_PushEvent(&stop_event); + PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED); } -bool -scrcpy(const struct scrcpy_options *options) { +static void +sc_server_on_connected(struct sc_server *server, void *userdata) { + (void) server; + (void) userdata; + + PUSH_EVENT(SC_EVENT_SERVER_CONNECTED); +} + +static void +sc_server_on_disconnected(struct sc_server *server, void *userdata) { + (void) server; + (void) userdata; + + LOGD("Server disconnected"); + // Do nothing, the disconnection will be handled by the "stream stopped" + // event +} + +static void +sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) { + (void) timeout; + (void) userdata; + + PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED); +} + +// Generate a scrcpy id to differentiate multiple running scrcpy instances +static uint32_t +scrcpy_generate_scid(void) { + struct sc_rand rand; + sc_rand_init(&rand); + // Only use 31 bits to avoid issues with signed values on the Java-side + return sc_rand_u32(&rand) & 0x7FFFFFFF; +} + +enum scrcpy_exit_code +scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; +#ifndef NDEBUG + // Detect missing initializations + memset(&scrcpy, 42, sizeof(scrcpy)); +#endif struct scrcpy *s = &scrcpy; - if (!server_init(&s->server)) { - return false; + // Minimal SDL initialization + if (SDL_Init(SDL_INIT_EVENTS)) { + LOGE("Could not initialize SDL: %s", SDL_GetError()); + return SCRCPY_EXIT_FAILURE; } - bool ret = false; + atexit(SDL_Quit); + + enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; bool server_started = false; - bool file_handler_initialized = false; + bool file_pusher_initialized = false; bool recorder_initialized = false; + bool recorder_started = false; #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif - bool stream_started = false; + bool video_demuxer_started = false; + bool audio_demuxer_started = false; +#ifdef HAVE_USB + bool aoa_hid_initialized = false; + bool keyboard_aoa_initialized = false; + bool mouse_aoa_initialized = false; +#endif bool controller_initialized = false; bool controller_started = false; bool screen_initialized = false; + bool timeout_initialized = false; + bool timeout_started = false; - bool record = !!options->record_filename; - struct server_params params = { - .serial = options->serial, + struct sc_acksync *acksync = NULL; + struct sc_uhid_devices *uhid_devices = NULL; + + uint32_t scid = scrcpy_generate_scid(); + + struct sc_server_params params = { + .scid = scid, + .req_serial = options->serial, + .select_usb = options->select_usb, + .select_tcpip = options->select_tcpip, .log_level = options->log_level, + .video_codec = options->video_codec, + .audio_codec = options->audio_codec, + .video_source = options->video_source, + .audio_source = options->audio_source, + .camera_facing = options->camera_facing, .crop = options->crop, .port_range = options->port_range, + .tunnel_host = options->tunnel_host, + .tunnel_port = options->tunnel_port, .max_size = options->max_size, - .bit_rate = options->bit_rate, + .video_bit_rate = options->video_bit_rate, + .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .video = options->video, + .audio = options->audio, .show_touches = options->show_touches, .stay_awake = options->stay_awake, - .codec_options = options->codec_options, - .encoder_name = options->encoder_name, + .video_codec_options = options->video_codec_options, + .audio_codec_options = options->audio_codec_options, + .video_encoder = options->video_encoder, + .audio_encoder = options->audio_encoder, + .camera_id = options->camera_id, + .camera_size = options->camera_size, + .camera_ar = options->camera_ar, + .camera_fps = options->camera_fps, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, + .clipboard_autosync = options->clipboard_autosync, + .downsize_on_error = options->downsize_on_error, + .tcpip = options->tcpip, + .tcpip_dst = options->tcpip_dst, + .cleanup = options->cleanup, + .power_on = options->power_on, + .kill_adb_on_close = options->kill_adb_on_close, + .camera_high_speed = options->camera_high_speed, + .list = options->list, }; - if (!server_start(&s->server, ¶ms)) { + + static const struct sc_server_callbacks cbs = { + .on_connection_failed = sc_server_on_connection_failed, + .on_connected = sc_server_on_connected, + .on_disconnected = sc_server_on_disconnected, + }; + if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) { + return SCRCPY_EXIT_FAILURE; + } + + if (options->video_playback) { + // Set hints before starting the server thread to avoid race conditions + // in SDL + sdl_set_hints(options->render_driver); + } + + if (!sc_server_start(&s->server)) { goto end; } server_started = true; - if (!sdl_init_and_configure(options->display, options->render_driver, - options->disable_screensaver)) { + if (options->list) { + bool ok = await_for_server(NULL); + ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; goto end; } - char device_name[DEVICE_NAME_FIELD_LENGTH]; - struct size frame_size; + // playback implies capture + assert(!options->video_playback || options->video); + assert(!options->audio_playback || options->audio); + + if (options->video_playback || + (options->control && options->clipboard_autosync)) { + // Initialize the video subsystem even if --no-video or + // --no-video-playback is passed so that clipboard synchronization + // still works. + // + if (SDL_Init(SDL_INIT_VIDEO)) { + // If it fails, it is an error only if video playback is enabled + if (options->video_playback) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; + } else { + LOGW("Could not initialize SDL video: %s", SDL_GetError()); + } + } + } + + if (options->audio_playback) { + if (SDL_Init(SDL_INIT_AUDIO)) { + LOGE("Could not initialize SDL audio: %s", SDL_GetError()); + goto end; + } + } + + sdl_configure(options->video_playback, options->disable_screensaver); + + // Await for server without blocking Ctrl+C handling + bool connected; + if (!await_for_server(&connected)) { + LOGE("Server connection failed"); + goto end; + } - if (!server_connect_to(&s->server, device_name, &frame_size)) { + if (!connected) { + // This is not an error, user requested to quit + LOGD("User requested to quit"); + ret = SCRCPY_EXIT_SUCCESS; goto end; } - if (options->display && options->control) { - if (!file_handler_init(&s->file_handler, s->server.serial, - options->push_target)) { + LOGD("Server connected"); + + // It is necessarily initialized here, since the device is connected + struct sc_server_info *info = &s->server.info; + + const char *serial = s->server.serial; + assert(serial); + + struct sc_file_pusher *fp = NULL; + + if (options->video_playback && options->control) { + if (!sc_file_pusher_init(&s->file_pusher, serial, + options->push_target)) { goto end; } - file_handler_initialized = true; + fp = &s->file_pusher; + file_pusher_initialized = true; + } + + if (options->video) { + static const struct sc_demuxer_callbacks video_demuxer_cbs = { + .on_ended = sc_video_demuxer_on_ended, + }; + sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket, + &video_demuxer_cbs, NULL); + } + + if (options->audio) { + static const struct sc_demuxer_callbacks audio_demuxer_cbs = { + .on_ended = sc_audio_demuxer_on_ended, + }; + sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket, + &audio_demuxer_cbs, options); } - struct decoder *dec = NULL; - bool needs_decoder = options->display; + bool needs_video_decoder = options->video_playback; + bool needs_audio_decoder = options->audio_playback; #ifdef HAVE_V4L2 - needs_decoder |= !!options->v4l2_device; + needs_video_decoder |= !!options->v4l2_device; #endif - if (needs_decoder) { - decoder_init(&s->decoder); - dec = &s->decoder; + if (needs_video_decoder) { + sc_decoder_init(&s->video_decoder, "video"); + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->video_decoder.packet_sink); + } + if (needs_audio_decoder) { + sc_decoder_init(&s->audio_decoder, "audio"); + sc_packet_source_add_sink(&s->audio_demuxer.packet_source, + &s->audio_decoder.packet_sink); } - struct recorder *rec = NULL; - if (record) { - if (!recorder_init(&s->recorder, - options->record_filename, - options->record_format, - frame_size)) { + if (options->record_filename) { + static const struct sc_recorder_callbacks recorder_cbs = { + .on_ended = sc_recorder_on_ended, + }; + if (!sc_recorder_init(&s->recorder, options->record_filename, + options->record_format, options->video, + options->audio, options->record_orientation, + &recorder_cbs, NULL)) { goto end; } - rec = &s->recorder; recorder_initialized = true; - } - - av_log_set_callback(av_log_callback); - static const struct stream_callbacks stream_cbs = { - .on_eos = stream_on_eos, - }; - stream_init(&s->stream, s->server.video_socket, &stream_cbs, NULL); + if (!sc_recorder_start(&s->recorder)) { + goto end; + } + recorder_started = true; - if (dec) { - stream_add_sink(&s->stream, &dec->packet_sink); + if (options->video) { + sc_packet_source_add_sink(&s->video_demuxer.packet_source, + &s->recorder.video_packet_sink); + } + if (options->audio) { + sc_packet_source_add_sink(&s->audio_demuxer.packet_source, + &s->recorder.audio_packet_sink); + } } - if (rec) { - stream_add_sink(&s->stream, &rec->packet_sink); - } + struct sc_controller *controller = NULL; + struct sc_key_processor *kp = NULL; + struct sc_mouse_processor *mp = NULL; if (options->control) { - if (!controller_init(&s->controller, s->server.control_socket)) { + if (!sc_controller_init(&s->controller, s->server.control_socket)) { goto end; } controller_initialized = true; - if (!controller_start(&s->controller)) { - goto end; + controller = &s->controller; + +#ifdef HAVE_USB + bool use_keyboard_aoa = + options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; + bool use_mouse_aoa = + options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; + if (use_keyboard_aoa || use_mouse_aoa) { + bool ok = sc_acksync_init(&s->acksync); + if (!ok) { + goto end; + } + + ok = sc_usb_init(&s->usb); + if (!ok) { + LOGE("Failed to initialize USB"); + sc_acksync_destroy(&s->acksync); + goto end; + } + + assert(serial); + struct sc_usb_device usb_device; + ok = sc_usb_select_device(&s->usb, serial, &usb_device); + if (!ok) { + sc_usb_destroy(&s->usb); + goto end; + } + + LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", + usb_device.serial, usb_device.vid, usb_device.pid, + usb_device.manufacturer, usb_device.product); + + ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL); + sc_usb_device_destroy(&usb_device); + if (!ok) { + LOGE("Failed to connect to USB device %s", serial); + sc_usb_destroy(&s->usb); + sc_acksync_destroy(&s->acksync); + goto end; + } + + ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); + if (!ok) { + LOGE("Failed to enable HID over AOA"); + sc_usb_disconnect(&s->usb); + sc_usb_destroy(&s->usb); + sc_acksync_destroy(&s->acksync); + goto end; + } + + if (use_keyboard_aoa) { + if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { + keyboard_aoa_initialized = true; + kp = &s->keyboard_aoa.key_processor; + } else { + LOGE("Could not initialize HID keyboard"); + } + } + + if (use_mouse_aoa) { + if (sc_mouse_aoa_init(&s->mouse_aoa, &s->aoa)) { + mouse_aoa_initialized = true; + mp = &s->mouse_aoa.mouse_processor; + } else { + LOGE("Could not initialized HID mouse"); + } + } + + bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized; + + if (!need_aoa || !sc_aoa_start(&s->aoa)) { + sc_acksync_destroy(&s->acksync); + sc_usb_disconnect(&s->usb); + sc_usb_destroy(&s->usb); + sc_aoa_destroy(&s->aoa); + goto end; + } + + acksync = &s->acksync; + + aoa_hid_initialized = true; } - controller_started = true; +#else + assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_AOA); + assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA); +#endif - 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 (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) { + sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller, + options->key_inject_mode, + options->forward_key_repeat); + kp = &s->keyboard_sdk.key_processor; + } else if (options->keyboard_input_mode + == SC_KEYBOARD_INPUT_MODE_UHID) { + sc_uhid_devices_init(&s->uhid_devices); + bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller, + &s->uhid_devices); + if (!ok) { + goto end; + } + uhid_devices = &s->uhid_devices; + kp = &s->keyboard_uhid.key_processor; + } - if (!controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); + if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) { + sc_mouse_sdk_init(&s->mouse_sdk, &s->controller); + mp = &s->mouse_sdk.mouse_processor; + } else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) { + bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller); + if (!ok) { + goto end; } + mp = &s->mouse_uhid.mouse_processor; + } + + sc_controller_configure(&s->controller, acksync, uhid_devices); + + if (!sc_controller_start(&s->controller)) { + goto end; } + controller_started = true; } - if (options->display) { - const char *window_title = - options->window_title ? options->window_title : device_name; + // There is a controller if and only if control is enabled + assert(options->control == !!controller); - struct screen_params screen_params = { + if (options->video_playback) { + const char *window_title = + options->window_title ? options->window_title : info->device_name; + + struct sc_screen_params screen_params = { + .controller = controller, + .fp = fp, + .kp = kp, + .mp = mp, + .forward_all_clicks = options->forward_all_clicks, + .legacy_paste = options->legacy_paste, + .clipboard_autosync = options->clipboard_autosync, + .shortcut_mods = &options->shortcut_mods, .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, + .orientation = options->display_orientation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, - .buffering_time = options->display_buffer, + .start_fps_counter = options->start_fps_counter, }; - if (!screen_init(&s->screen, &screen_params)) { + struct sc_frame_source *src = &s->video_decoder.frame_source; + if (options->display_buffer) { + sc_delay_buffer_init(&s->display_buffer, options->display_buffer, + true); + sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); + src = &s->display_buffer.frame_source; + } + + if (!sc_screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; - decoder_add_sink(&s->decoder, &s->screen.frame_sink); + sc_frame_source_add_sink(src, &s->screen.frame_sink); + } + + if (options->audio_playback) { + sc_audio_player_init(&s->audio_player, options->audio_buffer, + options->audio_output_buffer); + sc_frame_source_add_sink(&s->audio_decoder.frame_source, + &s->audio_player.frame_sink); } #ifdef HAVE_V4L2 if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size, - options->v4l2_buffer)) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device)) { goto end; } - decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink); + struct sc_frame_source *src = &s->video_decoder.frame_source; + if (options->v4l2_buffer) { + sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer, true); + sc_frame_source_add_sink(src, &s->v4l2_buffer.frame_sink); + src = &s->v4l2_buffer.frame_source; + } + + sc_frame_source_add_sink(src, &s->v4l2_sink.frame_sink); v4l2_sink_initialized = true; } #endif - // now we consumed the header values, the socket receives the video stream - // start the stream - if (!stream_start(&s->stream)) { - goto end; + // Now that the header values have been consumed, the socket(s) will + // receive the stream(s). Start the demuxer(s). + + if (options->video) { + if (!sc_demuxer_start(&s->video_demuxer)) { + goto end; + } + video_demuxer_started = true; + } + + if (options->audio) { + if (!sc_demuxer_start(&s->audio_demuxer)) { + goto end; + } + audio_demuxer_started = true; + } + + // If the device screen is to be turned off, send the control message after + // everything is set up + if (options->control && options->turn_screen_off) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; + msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; + + if (!sc_controller_push_msg(&s->controller, &msg)) { + LOGW("Could not request 'set screen power mode'"); + } } - stream_started = true; - input_manager_init(&s->input_manager, &s->controller, &s->screen, options); + if (options->time_limit) { + bool ok = sc_timeout_init(&s->timeout); + if (!ok) { + goto end; + } + + timeout_initialized = true; - ret = event_loop(s, options); + sc_tick deadline = sc_tick_now() + options->time_limit; + static const struct sc_timeout_callbacks cbs = { + .on_timeout = sc_timeout_on_timeout, + }; + + ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL); + if (!ok) { + goto end; + } + + timeout_started = true; + } + + ret = event_loop(s); LOGD("quit..."); // 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); + // only be called once the video demuxer thread is joined (it may take time) + sc_screen_hide_window(&s->screen); end: - // The stream is not stopped explicitly, because it will stop by itself on + if (timeout_started) { + sc_timeout_stop(&s->timeout); + } + + // The demuxer is not stopped explicitly, because it will stop by itself on // end-of-stream +#ifdef HAVE_USB + if (aoa_hid_initialized) { + if (keyboard_aoa_initialized) { + sc_keyboard_aoa_destroy(&s->keyboard_aoa); + } + if (mouse_aoa_initialized) { + sc_mouse_aoa_destroy(&s->mouse_aoa); + } + sc_aoa_stop(&s->aoa); + sc_usb_stop(&s->usb); + } + if (acksync) { + sc_acksync_destroy(acksync); + } +#endif if (controller_started) { - controller_stop(&s->controller); + sc_controller_stop(&s->controller); } - if (file_handler_initialized) { - file_handler_stop(&s->file_handler); + if (file_pusher_initialized) { + sc_file_pusher_stop(&s->file_pusher); + } + if (recorder_initialized) { + sc_recorder_stop(&s->recorder); } if (screen_initialized) { - screen_interrupt(&s->screen); + sc_screen_interrupt(&s->screen); } if (server_started) { // shutdown the sockets and kill the server - server_stop(&s->server); + sc_server_stop(&s->server); + } + + if (timeout_started) { + sc_timeout_join(&s->timeout); + } + if (timeout_initialized) { + sc_timeout_destroy(&s->timeout); } - // now that the sockets are shutdown, the stream and controller are + // now that the sockets are shutdown, the demuxer and controller are // interrupted, we can join them - if (stream_started) { - stream_join(&s->stream); + if (video_demuxer_started) { + sc_demuxer_join(&s->video_demuxer); + } + + if (audio_demuxer_started) { + sc_demuxer_join(&s->audio_demuxer); } #ifdef HAVE_V4L2 @@ -451,30 +872,48 @@ end: } #endif - // Destroy the screen only after the stream is guaranteed to be finished, - // because otherwise the screen could receive new frames after destruction +#ifdef HAVE_USB + if (aoa_hid_initialized) { + sc_aoa_join(&s->aoa); + sc_aoa_destroy(&s->aoa); + sc_usb_join(&s->usb); + sc_usb_disconnect(&s->usb); + sc_usb_destroy(&s->usb); + } +#endif + + // Destroy the screen only after the video demuxer 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); + sc_screen_join(&s->screen); + sc_screen_destroy(&s->screen); } if (controller_started) { - controller_join(&s->controller); + sc_controller_join(&s->controller); } if (controller_initialized) { - controller_destroy(&s->controller); + sc_controller_destroy(&s->controller); } + if (recorder_started) { + sc_recorder_join(&s->recorder); + } if (recorder_initialized) { - recorder_destroy(&s->recorder); + sc_recorder_destroy(&s->recorder); } - if (file_handler_initialized) { - file_handler_join(&s->file_handler); - file_handler_destroy(&s->file_handler); + if (file_pusher_initialized) { + sc_file_pusher_join(&s->file_pusher); + sc_file_pusher_destroy(&s->file_pusher); + } + + if (server_started) { + sc_server_join(&s->server); } - server_destroy(&s->server); + sc_server_destroy(&s->server); return ret; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 8b76fb25..d4d494a3 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -4,153 +4,20 @@ #include "common.h" #include -#include -#include +#include "options.h" -#include "util/tick.h" +enum scrcpy_exit_code { + // Normal program termination + SCRCPY_EXIT_SUCCESS, -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; -}; + // No connection could be established + SCRCPY_EXIT_FAILURE, -struct sc_port_range { - uint16_t first; - uint16_t last; + // Device was disconnected while running + SCRCPY_EXIT_DISCONNECTED, }; -#define SC_WINDOW_POSITION_UNDEFINED (-0x8000) - -struct scrcpy_options { - const char *serial; - const char *crop; - const char *record_filename; - const char *window_title; - const char *push_target; - const char *render_driver; - const char *codec_options; - const char *encoder_name; - const char *v4l2_device; - enum sc_log_level log_level; - enum sc_record_format record_format; - struct sc_port_range port_range; - struct sc_shortcut_mods shortcut_mods; - uint16_t max_size; - uint32_t bit_rate; - uint16_t max_fps; - enum sc_lock_video_orientation lock_video_orientation; - uint8_t rotation; - int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" - int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" - uint16_t window_width; - uint16_t window_height; - uint32_t display_id; - sc_tick display_buffer; - sc_tick v4l2_buffer; - bool show_touches; - bool fullscreen; - bool always_on_top; - bool control; - bool display; - bool turn_screen_off; - bool prefer_text; - bool window_borderless; - bool mipmaps; - bool stay_awake; - bool force_adb_forward; - bool disable_screensaver; - bool forward_key_repeat; - bool forward_all_clicks; - bool legacy_paste; - bool power_off_on_close; -}; - -#define SCRCPY_OPTIONS_DEFAULT { \ - .serial = NULL, \ - .crop = NULL, \ - .record_filename = NULL, \ - .window_title = NULL, \ - .push_target = NULL, \ - .render_driver = NULL, \ - .codec_options = NULL, \ - .encoder_name = NULL, \ - .v4l2_device = NULL, \ - .log_level = SC_LOG_LEVEL_INFO, \ - .record_format = SC_RECORD_FORMAT_AUTO, \ - .port_range = { \ - .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, \ - .last = DEFAULT_LOCAL_PORT_RANGE_LAST, \ - }, \ - .shortcut_mods = { \ - .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ - .count = 2, \ - }, \ - .max_size = 0, \ - .bit_rate = DEFAULT_BIT_RATE, \ - .max_fps = 0, \ - .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \ - .rotation = 0, \ - .window_x = SC_WINDOW_POSITION_UNDEFINED, \ - .window_y = SC_WINDOW_POSITION_UNDEFINED, \ - .window_width = 0, \ - .window_height = 0, \ - .display_id = 0, \ - .display_buffer = 0, \ - .v4l2_buffer = 0, \ - .show_touches = false, \ - .fullscreen = false, \ - .always_on_top = false, \ - .control = true, \ - .display = true, \ - .turn_screen_off = false, \ - .prefer_text = false, \ - .window_borderless = false, \ - .mipmaps = true, \ - .stay_awake = false, \ - .force_adb_forward = false, \ - .disable_screensaver = false, \ - .forward_key_repeat = true, \ - .forward_all_clicks = false, \ - .legacy_paste = false, \ - .power_off_on_close = false, \ -} - -bool -scrcpy(const struct scrcpy_options *options); +enum scrcpy_exit_code +scrcpy(struct scrcpy_options *options); #endif diff --git a/app/src/screen.c b/app/src/screen.c index 3cd4329f..091001bc 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -5,49 +5,47 @@ #include #include "events.h" -#include "icon.xpm" -#include "scrcpy.h" -#include "tiny_xpm.h" -#include "video_buffer.h" +#include "icon.h" +#include "options.h" #include "util/log.h" #define DISPLAY_MARGINS 96 -#define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink) +#define DOWNCAST(SINK) container_of(SINK, struct sc_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; +static inline struct sc_size +get_oriented_size(struct sc_size size, enum sc_orientation orientation) { + struct sc_size oriented_size; + if (sc_orientation_is_swap(orientation)) { + oriented_size.width = size.height; + oriented_size.height = size.width; } else { - rotated_size.width = size.width; - rotated_size.height = size.height; + oriented_size.width = size.width; + oriented_size.height = size.height; } - return rotated_size; + return oriented_size; } -// get the window size in a struct size -static struct size -get_window_size(const struct screen *screen) { +// get the window size in a struct sc_size +static struct sc_size +get_window_size(const struct sc_screen *screen) { int width; int height; SDL_GetWindowSize(screen->window, &width, &height); - struct size size; + struct sc_size size; size.width = width; size.height = height; return size; } -static struct point -get_window_position(const struct screen *screen) { +static struct sc_point +get_window_position(const struct sc_screen *screen) { int x; int y; SDL_GetWindowPosition(screen->window, &x, &y); - struct point point; + struct sc_point point; point.x = x; point.y = y; return point; @@ -55,22 +53,18 @@ get_window_position(const struct screen *screen) { // set the window size to be applied when fullscreen is disabled static void -set_window_size(struct screen *screen, struct size new_size) { +set_window_size(struct sc_screen *screen, struct sc_size new_size) { assert(!screen->fullscreen); assert(!screen->maximized); + assert(!screen->minimized); SDL_SetWindowSize(screen->window, new_size.width, new_size.height); } // get the preferred display bounds (i.e. the screen bounds with some margins) static bool -get_preferred_display_bounds(struct size *bounds) { +get_preferred_display_bounds(struct sc_size *bounds) { SDL_Rect rect; -#ifdef SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS -# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r)) -#else -# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r)) -#endif - if (GET_DISPLAY_BOUNDS(0, &rect)) { + if (SDL_GetDisplayUsableBounds(0, &rect)) { LOGW("Could not get display usable bounds: %s", SDL_GetError()); return false; } @@ -81,7 +75,7 @@ get_preferred_display_bounds(struct size *bounds) { } static bool -is_optimal_size(struct size current_size, struct size content_size) { +is_optimal_size(struct sc_size current_size, struct sc_size content_size) { // The size is optimal if we can recompute one dimension of the current // size from the other return current_size.height == current_size.width * content_size.height @@ -95,20 +89,21 @@ is_optimal_size(struct size current_size, struct size content_size) { // crops the black borders) // - it keeps the aspect ratio // - it scales down to make it fit in the display_size -static struct size -get_optimal_size(struct size current_size, struct size content_size) { +static struct sc_size +get_optimal_size(struct sc_size current_size, struct sc_size content_size, + bool within_display_bounds) { if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } - struct size window_size; + struct sc_size window_size; - struct size display_size; - if (!get_preferred_display_bounds(&display_size)) { - // could not get display bounds, do not constraint the size - window_size.width = current_size.width; - window_size.height = current_size.height; + struct sc_size display_size; + if (!within_display_bounds || + !get_preferred_display_bounds(&display_size)) { + // do not constraint the size + window_size = current_size; } else { window_size.width = MIN(current_size.width, display_size.width); window_size.height = MIN(current_size.height, display_size.height); @@ -136,12 +131,12 @@ get_optimal_size(struct size current_size, struct size content_size) { // initially, there is no current size, so use the frame size as current size // req_width and req_height, if not 0, are the sizes requested by the user -static inline struct size -get_initial_optimal_size(struct size content_size, uint16_t req_width, +static inline struct sc_size +get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, uint16_t req_height) { - struct size window_size; + struct sc_size window_size; if (!req_width && !req_height) { - window_size = get_optimal_size(content_size, content_size); + window_size = get_optimal_size(content_size, content_size, true); } else { if (req_width) { window_size.width = req_width; @@ -161,15 +156,62 @@ get_initial_optimal_size(struct size content_size, uint16_t req_width, return window_size; } +static inline bool +sc_screen_is_relative_mode(struct sc_screen *screen) { + // screen->im.mp may be NULL if --no-control + return screen->im.mp && screen->im.mp->relative_mode; +} + +static void +sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(screen->window, &x, &y); + SDL_GetWindowSize(screen->window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); + } + } +#else + (void) screen; +#endif + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + } +} + +static inline bool +sc_screen_get_mouse_capture(struct sc_screen *screen) { + (void) screen; + return SDL_GetRelativeMouseMode(); +} + +static inline void +sc_screen_toggle_mouse_capture(struct sc_screen *screen) { + (void) screen; + bool new_value = !sc_screen_get_mouse_capture(screen); + sc_screen_set_mouse_capture(screen, new_value); +} + static void -screen_update_content_rect(struct screen *screen) { +sc_screen_update_content_rect(struct sc_screen *screen) { int dw; int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); - struct size content_size = screen->content_size; + struct sc_size content_size = screen->content_size; // The drawable size is the window size * the HiDPI scale - struct size drawable_size = {dw, dh}; + struct sc_size drawable_size = {dw, dh}; SDL_Rect *rect = &screen->rect; @@ -198,31 +240,19 @@ screen_update_content_rect(struct screen *screen) { } } -static inline SDL_Texture * -create_texture(struct screen *screen) { - SDL_Renderer *renderer = screen->renderer; - struct size size = screen->frame_size; - 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); +// render the texture to the renderer +// +// Set the update_content_rect flag if the window or content size may have +// changed, so that the content rectangle is recomputed +static void +sc_screen_render(struct sc_screen *screen, bool update_content_rect) { + if (update_content_rect) { + sc_screen_update_content_rect(screen); } - return texture; + enum sc_display_result res = + sc_display_render(&screen->display, &screen->rect, screen->orientation); + (void) res; // any error already logged } #if defined(__APPLE__) || defined(__WINDOWS__) @@ -237,21 +267,43 @@ create_texture(struct screen *screen) { // static int event_watcher(void *data, SDL_Event *event) { - struct screen *screen = data; + struct sc_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); + sc_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; +sc_screen_frame_sink_open(struct sc_frame_sink *sink, + const AVCodecContext *ctx) { + assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); + (void) ctx; + + struct sc_screen *screen = DOWNCAST(sink); + + assert(ctx->width > 0 && ctx->width <= 0xFFFF); + assert(ctx->height > 0 && ctx->height <= 0xFFFF); + // screen->frame_size is never used before the event is pushed, and the + // event acts as a memory barrier so it is safe without mutex + screen->frame_size.width = ctx->width; + screen->frame_size.height = ctx->height; + + static SDL_Event event = { + .type = SC_EVENT_SCREEN_INIT_SIZE, + }; + + // Post the event on the UI thread (the texture must be created from there) + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGW("Could not post init size event: %s", SDL_GetError()); + return false; + } + #ifndef NDEBUG screen->open = true; #endif @@ -261,8 +313,8 @@ screen_frame_sink_open(struct sc_frame_sink *sink) { } static void -screen_frame_sink_close(struct sc_frame_sink *sink) { - struct screen *screen = DOWNCAST(sink); +sc_screen_frame_sink_close(struct sc_frame_sink *sink) { + struct sc_screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG screen->open = false; @@ -272,180 +324,126 @@ screen_frame_sink_close(struct sc_frame_sink *sink) { } static bool -screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { - struct screen *screen = DOWNCAST(sink); - return sc_video_buffer_push(&screen->vb, frame); -} +sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { + struct sc_screen *screen = DOWNCAST(sink); -static void -sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, - void *userdata) { - (void) vb; - struct screen *screen = userdata; + bool previous_skipped; + bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); + if (!ok) { + return false; + } if (previous_skipped) { - fps_counter_add_skipped_frame(&screen->fps_counter); - // The EVENT_NEW_FRAME triggered for the previous frame will consume + sc_fps_counter_add_skipped_frame(&screen->fps_counter); + // The SC_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, + .type = SC_EVENT_NEW_FRAME, }; // Post the event on the UI thread - SDL_PushEvent(&new_frame_event); + int ret = SDL_PushEvent(&new_frame_event); + if (ret < 0) { + LOGW("Could not post new frame event: %s", SDL_GetError()); + return false; + } } + + return true; } bool -screen_init(struct screen *screen, const struct screen_params *params) { +sc_screen_init(struct sc_screen *screen, + const struct sc_screen_params *params) { screen->resize_pending = false; screen->has_frame = false; screen->fullscreen = false; screen->maximized = false; + screen->minimized = false; + screen->mouse_capture_key_pressed = 0; - static const struct sc_video_buffer_callbacks cbs = { - .on_new_frame = sc_video_buffer_on_new_frame, - }; + screen->req.x = params->window_x; + screen->req.y = params->window_y; + screen->req.width = params->window_width; + screen->req.height = params->window_height; + screen->req.fullscreen = params->fullscreen; + screen->req.start_fps_counter = params->start_fps_counter; - bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, - screen); + bool ok = sc_frame_buffer_init(&screen->fb); if (!ok) { - LOGE("Could not initialize video buffer"); return false; } - ok = sc_video_buffer_start(&screen->vb); - if (!ok) { - LOGE("Could not start video_buffer"); - goto error_destroy_video_buffer; - } - - if (!fps_counter_init(&screen->fps_counter)) { - LOGE("Could not initialize FPS counter"); - goto error_stop_and_join_video_buffer; + if (!sc_fps_counter_init(&screen->fps_counter)) { + goto error_destroy_frame_buffer; } - screen->frame_size = params->frame_size; - screen->rotation = params->rotation; - if (screen->rotation) { - LOGI("Initial display rotation set to %u", screen->rotation); + screen->orientation = params->orientation; + if (screen->orientation != SC_ORIENTATION_0) { + LOGI("Initial display orientation set to %s", + sc_orientation_get_name(screen->orientation)); } - 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 - LOGW("The 'always on top' flag is not available " - "(compile with SDL >= 2.0.5 to enable it)"); -#endif } if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } - 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); + // The window will be positioned and sized on first video frame + screen->window = + SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); if (!screen->window) { - LOGC("Could not create window: %s", SDL_GetError()); + LOGE("Could not create window: %s", SDL_GetError()); 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()); + ok = sc_display_init(&screen->display, screen->window, params->mipmaps); + if (!ok) { goto error_destroy_window; } - 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); + SDL_Surface *icon = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, icon); - SDL_FreeSurface(icon); + scrcpy_icon_destroy(icon); } else { LOGW("Could not load icon"); } - 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()); - goto error_destroy_renderer; - } - 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); + LOG_OOM(); + goto error_destroy_display; + } + + struct sc_input_manager_params im_params = { + .controller = params->controller, + .fp = params->fp, + .screen = screen, + .kp = params->kp, + .mp = params->mp, + .forward_all_clicks = params->forward_all_clicks, + .legacy_paste = params->legacy_paste, + .clipboard_autosync = params->clipboard_autosync, + .shortcut_mods = params->shortcut_mods, + }; - if (params->fullscreen) { - screen_switch_fullscreen(screen); - } + sc_input_manager_init(&screen->im, &im_params); #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, + .open = sc_screen_frame_sink_open, + .close = sc_screen_frame_sink_close, + .push = sc_screen_frame_sink_push, }; screen->frame_sink.ops = &ops; @@ -456,79 +454,92 @@ screen_init(struct screen *screen, const struct screen_params *params) { return true; -error_destroy_texture: - SDL_DestroyTexture(screen->texture); -error_destroy_renderer: - SDL_DestroyRenderer(screen->renderer); +error_destroy_display: + sc_display_destroy(&screen->display); error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_fps_counter: - fps_counter_destroy(&screen->fps_counter); -error_stop_and_join_video_buffer: - sc_video_buffer_stop(&screen->vb); - sc_video_buffer_join(&screen->vb); -error_destroy_video_buffer: - sc_video_buffer_destroy(&screen->vb); + sc_fps_counter_destroy(&screen->fps_counter); +error_destroy_frame_buffer: + sc_frame_buffer_destroy(&screen->fb); return false; } static void -screen_show_window(struct screen *screen) { +sc_screen_show_initial_window(struct sc_screen *screen) { + int x = screen->req.x != SC_WINDOW_POSITION_UNDEFINED + ? screen->req.x : (int) SDL_WINDOWPOS_CENTERED; + int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED + ? screen->req.y : (int) SDL_WINDOWPOS_CENTERED; + + struct sc_size window_size = + get_initial_optimal_size(screen->content_size, screen->req.width, + screen->req.height); + + set_window_size(screen, window_size); + SDL_SetWindowPosition(screen->window, x, y); + + if (screen->req.fullscreen) { + sc_screen_switch_fullscreen(screen); + } + + if (screen->req.start_fps_counter) { + sc_fps_counter_start(&screen->fps_counter); + } + SDL_ShowWindow(screen->window); + sc_screen_update_content_rect(screen); } void -screen_hide_window(struct screen *screen) { +sc_screen_hide_window(struct sc_screen *screen) { SDL_HideWindow(screen->window); } void -screen_interrupt(struct screen *screen) { - sc_video_buffer_stop(&screen->vb); - fps_counter_interrupt(&screen->fps_counter); +sc_screen_interrupt(struct sc_screen *screen) { + sc_fps_counter_interrupt(&screen->fps_counter); } void -screen_join(struct screen *screen) { - sc_video_buffer_join(&screen->vb); - fps_counter_join(&screen->fps_counter); +sc_screen_join(struct sc_screen *screen) { + sc_fps_counter_join(&screen->fps_counter); } void -screen_destroy(struct screen *screen) { +sc_screen_destroy(struct sc_screen *screen) { #ifndef NDEBUG assert(!screen->open); #endif + sc_display_destroy(&screen->display); av_frame_free(&screen->frame); - SDL_DestroyTexture(screen->texture); - SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); - fps_counter_destroy(&screen->fps_counter); - sc_video_buffer_destroy(&screen->vb); + sc_fps_counter_destroy(&screen->fps_counter); + sc_frame_buffer_destroy(&screen->fb); } static void -resize_for_content(struct screen *screen, struct size old_content_size, - struct size new_content_size) { - struct size window_size = get_window_size(screen); - struct size target_size = { +resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, + struct sc_size new_content_size) { + struct sc_size window_size = get_window_size(screen); + struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width / old_content_size.width, .height = (uint32_t) window_size.height * new_content_size.height / old_content_size.height, }; - target_size = get_optimal_size(target_size, new_content_size); + target_size = get_optimal_size(target_size, new_content_size, true); set_window_size(screen, target_size); } static void -set_content_size(struct screen *screen, struct size new_content_size) { - if (!screen->fullscreen && !screen->maximized) { +set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { + if (!screen->fullscreen && !screen->maximized && !screen->minimized) { 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 + // fullscreen/maximized/minimized are disabled screen->windowed_content_size = screen->content_size; screen->resize_pending = true; } @@ -537,9 +548,10 @@ set_content_size(struct screen *screen, struct size new_content_size) { } static void -apply_pending_resize(struct screen *screen) { +apply_pending_resize(struct sc_screen *screen) { assert(!screen->fullscreen); assert(!screen->maximized); + assert(!screen->minimized); if (screen->resize_pending) { resize_for_content(screen, screen->windowed_content_size, screen->content_size); @@ -548,120 +560,103 @@ apply_pending_resize(struct screen *screen) { } void -screen_set_rotation(struct screen *screen, unsigned rotation) { - assert(rotation < 4); - if (rotation == screen->rotation) { +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation) { + if (orientation == screen->orientation) { return; } - struct size new_content_size = - get_rotated_size(screen->frame_size, rotation); + struct sc_size new_content_size = + get_oriented_size(screen->frame_size, orientation); set_content_size(screen, new_content_size); - screen->rotation = rotation; - LOGI("Display rotation set to %u", rotation); + screen->orientation = orientation; + LOGI("Display orientation set to %s", sc_orientation_get_name(orientation)); - screen_render(screen, true); + sc_screen_render(screen, true); } -// recreate the texture and resize the window if the frame size has changed static bool -prepare_for_frame(struct screen *screen, struct size new_frame_size) { - if (screen->frame_size.width != new_frame_size.width - || screen->frame_size.height != new_frame_size.height) { - // frame dimension changed, destroy texture - SDL_DestroyTexture(screen->texture); +sc_screen_init_size(struct sc_screen *screen) { + // Before first frame + assert(!screen->has_frame); - screen->frame_size = new_frame_size; + // The requested size is passed via screen->frame_size - struct size new_content_size = - get_rotated_size(new_frame_size, screen->rotation); - set_content_size(screen, new_content_size); + struct sc_size content_size = + get_oriented_size(screen->frame_size, screen->orientation); + screen->content_size = content_size; - screen_update_content_rect(screen); + enum sc_display_result res = + sc_display_set_texture_size(&screen->display, screen->frame_size); + return res != SC_DISPLAY_RESULT_ERROR; +} - LOGI("New texture: %" PRIu16 "x%" PRIu16, - screen->frame_size.width, screen->frame_size.height); - screen->texture = create_texture(screen); - if (!screen->texture) { - LOGC("Could not create texture: %s", SDL_GetError()); - return false; - } +// recreate the texture and resize the window if the frame size has changed +static enum sc_display_result +prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { + if (screen->frame_size.width == new_frame_size.width + && screen->frame_size.height == new_frame_size.height) { + return SC_DISPLAY_RESULT_OK; } - return true; -} + // frame dimension changed + screen->frame_size = new_frame_size; -// write the frame into the texture -static void -update_texture(struct screen *screen, const AVFrame *frame) { - SDL_UpdateYUVTexture(screen->texture, NULL, - frame->data[0], frame->linesize[0], - frame->data[1], frame->linesize[1], - frame->data[2], frame->linesize[2]); + struct sc_size new_content_size = + get_oriented_size(new_frame_size, screen->orientation); + set_content_size(screen, new_content_size); - if (screen->mipmaps) { - SDL_GL_BindTexture(screen->texture, NULL, NULL); - screen->gl.GenerateMipmap(GL_TEXTURE_2D); - SDL_GL_UnbindTexture(screen->texture); - } + sc_screen_update_content_rect(screen); + + return sc_display_set_texture_size(&screen->display, screen->frame_size); } static bool -screen_update_frame(struct screen *screen) { +sc_screen_update_frame(struct sc_screen *screen) { av_frame_unref(screen->frame); - sc_video_buffer_consume(&screen->vb, screen->frame); + sc_frame_buffer_consume(&screen->fb, screen->frame); AVFrame *frame = screen->frame; - fps_counter_add_rendered_frame(&screen->fps_counter); + sc_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)) { + struct sc_size new_frame_size = {frame->width, frame->height}; + enum sc_display_result res = prepare_for_frame(screen, new_frame_size); + if (res == SC_DISPLAY_RESULT_ERROR) { return false; } - update_texture(screen, frame); - - screen_render(screen, false); - return true; -} + if (res == SC_DISPLAY_RESULT_PENDING) { + // Not an error, but do not continue + return true; + } -void -screen_render(struct screen *screen, bool update_content_rect) { - if (update_content_rect) { - screen_update_content_rect(screen); + res = sc_display_update_texture(&screen->display, frame); + if (res == SC_DISPLAY_RESULT_ERROR) { + return false; + } + if (res == SC_DISPLAY_RESULT_PENDING) { + // Not an error, but do not continue + return true; } - SDL_RenderClear(screen->renderer); - if (screen->rotation == 0) { - SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect); - } else { - // rotation in RenderCopyEx() is clockwise, while screen->rotation is - // counterclockwise (to be consistent with --lock-video-orientation) - int cw_rotation = (4 - screen->rotation) % 4; - double angle = 90 * cw_rotation; - - SDL_Rect *dstrect = NULL; - SDL_Rect rect; - if (screen->rotation & 1) { - rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2; - rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2; - rect.w = screen->rect.h; - rect.h = screen->rect.w; - dstrect = ▭ - } else { - assert(screen->rotation == 2); - dstrect = &screen->rect; - } + if (!screen->has_frame) { + screen->has_frame = true; + // this is the very first frame, show the window + sc_screen_show_initial_window(screen); - SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, - angle, NULL, 0); + if (sc_screen_is_relative_mode(screen)) { + // Capture mouse on start + sc_screen_set_mouse_capture(screen, true); + } } - SDL_RenderPresent(screen->renderer); + + sc_screen_render(screen, false); + return true; } void -screen_switch_fullscreen(struct screen *screen) { +sc_screen_switch_fullscreen(struct sc_screen *screen) { uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); @@ -669,25 +664,25 @@ screen_switch_fullscreen(struct screen *screen) { } screen->fullscreen = !screen->fullscreen; - if (!screen->fullscreen && !screen->maximized) { + if (!screen->fullscreen && !screen->maximized && !screen->minimized) { apply_pending_resize(screen); } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); - screen_render(screen, true); + sc_screen_render(screen, true); } void -screen_resize_to_fit(struct screen *screen) { - if (screen->fullscreen || screen->maximized) { +sc_screen_resize_to_fit(struct sc_screen *screen) { + if (screen->fullscreen || screen->maximized || screen->minimized) { return; } - struct point point = get_window_position(screen); - struct size window_size = get_window_size(screen); + struct sc_point point = get_window_position(screen); + struct sc_size window_size = get_window_size(screen); - struct size optimal_size = - get_optimal_size(window_size, screen->content_size); + struct sc_size optimal_size = + get_optimal_size(window_size, screen->content_size, false); // Center the window related to the device screen assert(optimal_size.width <= window_size.width); @@ -702,8 +697,8 @@ screen_resize_to_fit(struct screen *screen) { } void -screen_resize_to_pixel_perfect(struct screen *screen) { - if (screen->fullscreen) { +sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { + if (screen->fullscreen || screen->minimized) { return; } @@ -712,26 +707,39 @@ screen_resize_to_pixel_perfect(struct screen *screen) { screen->maximized = false; } - struct size content_size = screen->content_size; + struct sc_size content_size = screen->content_size; SDL_SetWindowSize(screen->window, content_size.width, content_size.height); LOGD("Resized to pixel-perfect: %ux%u", content_size.width, content_size.height); } +static inline bool +sc_screen_is_mouse_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + bool -screen_handle_event(struct screen *screen, SDL_Event *event) { +sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { + bool relative_mode = sc_screen_is_relative_mode(screen); + 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); + case SC_EVENT_SCREEN_INIT_SIZE: { + // The initial size is passed via screen->frame_size + bool ok = sc_screen_init_size(screen); + if (!ok) { + LOGE("Could not initialize screen size"); + return false; } - bool ok = screen_update_frame(screen); + return true; + } + case SC_EVENT_NEW_FRAME: { + bool ok = sc_screen_update_frame(screen); if (!ok) { - LOGW("Frame update failed\n"); + LOGE("Frame update failed\n"); + return false; } return true; + } case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing @@ -739,14 +747,17 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { } switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: - screen_render(screen, true); + sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: - screen_render(screen, true); + sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_MAXIMIZED: screen->maximized = true; break; + case SDL_WINDOWEVENT_MINIMIZED: + screen->minimized = true; + break; case SDL_WINDOWEVENT_RESTORED: if (screen->fullscreen) { // On Windows, in maximized+fullscreen, disabling @@ -757,62 +768,142 @@ screen_handle_event(struct screen *screen, SDL_Event *event) { break; } screen->maximized = false; + screen->minimized = false; apply_pending_resize(screen); - screen_render(screen, true); + sc_screen_render(screen, true); + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (relative_mode) { + sc_screen_set_mouse_capture(screen, false); + } break; } return true; + case SDL_KEYDOWN: + if (relative_mode) { + SDL_Keycode key = event->key.keysym.sym; + if (sc_screen_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + screen->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return true; + } + } + break; + case SDL_KEYUP: + if (relative_mode) { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (sc_screen_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_toggle_mouse_capture(screen); + } + // Mouse capture keys are never forwarded to the device + return true; + } + } + break; + case SDL_MOUSEWHEEL: + case SDL_MOUSEMOTION: + case SDL_MOUSEBUTTONDOWN: + if (relative_mode && !sc_screen_get_mouse_capture(screen)) { + // Do not forward to input manager, the mouse will be captured + // on SDL_MOUSEBUTTONUP + return true; + } + break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + if (relative_mode) { + // Touch events are not compatible with relative mode + // (coordinates are not relative) + return true; + } + break; + case SDL_MOUSEBUTTONUP: + if (relative_mode && !sc_screen_get_mouse_capture(screen)) { + sc_screen_set_mouse_capture(screen, true); + return true; + } + break; } - return false; + sc_input_manager_handle_event(&screen->im, event); + return true; } -struct point -screen_convert_drawable_to_frame_coords(struct screen *screen, - int32_t x, int32_t y) { - unsigned rotation = screen->rotation; - assert(rotation < 4); +struct sc_point +sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, + int32_t x, int32_t y) { + enum sc_orientation orientation = screen->orientation; int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; + // screen->rect must be initialized to avoid a division by zero + assert(screen->rect.w && screen->rect.h); 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: + struct sc_point result; + switch (orientation) { + case SC_ORIENTATION_0: result.x = x; result.y = y; break; - case 1: + case SC_ORIENTATION_90: + result.x = y; + result.y = w - x; + break; + case SC_ORIENTATION_180: + result.x = w - x; + result.y = h - y; + break; + case SC_ORIENTATION_270: result.x = h - y; result.y = x; break; - case 2: + case SC_ORIENTATION_FLIP_0: result.x = w - x; + result.y = y; + break; + case SC_ORIENTATION_FLIP_90: + result.x = h - y; + result.y = w - x; + break; + case SC_ORIENTATION_FLIP_180: + result.x = x; result.y = h - y; break; default: - assert(rotation == 3); + assert(orientation == SC_ORIENTATION_FLIP_270); result.x = y; - result.y = w - x; + result.y = 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); +struct sc_point +sc_screen_convert_window_to_frame_coords(struct sc_screen *screen, + int32_t x, int32_t y) { + sc_screen_hidpi_scale_coords(screen, &x, &y); + return sc_screen_convert_drawable_to_frame_coords(screen, x, y); } void -screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { +sc_screen_hidpi_scale_coords(struct sc_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); diff --git a/app/src/screen.h b/app/src/screen.h index 86aa1183..46591be5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -7,134 +7,156 @@ #include #include +#include "controller.h" #include "coords.h" +#include "display.h" #include "fps_counter.h" +#include "frame_buffer.h" +#include "input_manager.h" #include "opengl.h" +#include "options.h" +#include "trait/key_processor.h" #include "trait/frame_sink.h" -#include "video_buffer.h" +#include "trait/mouse_processor.h" -struct screen { +struct sc_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 sc_video_buffer vb; - struct fps_counter fps_counter; + struct sc_display display; + struct sc_input_manager im; + struct sc_frame_buffer fb; + struct sc_fps_counter fps_counter; + + // The initial requested window properties + struct { + int16_t x; + int16_t y; + uint16_t width; + uint16_t height; + bool fullscreen; + bool start_fps_counter; + } req; SDL_Window *window; - SDL_Renderer *renderer; - SDL_Texture *texture; - struct sc_opengl gl; - struct size frame_size; - struct size content_size; // rotated frame_size + struct sc_size frame_size; + struct sc_size content_size; // rotated frame_size bool resize_pending; // resize requested while fullscreen or maximized // The content size the last time the window was not maximized or // fullscreen (meaningful only when resize_pending is true) - struct size windowed_content_size; + struct sc_size windowed_content_size; - // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) - unsigned rotation; + // client orientation + enum sc_orientation orientation; // rectangle of the content (excluding black borders) struct SDL_Rect rect; bool has_frame; bool fullscreen; bool maximized; - bool mipmaps; + bool minimized; + + // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or + // RGUI) must be pressed. This variable tracks the pressed capture key. + SDL_Keycode mouse_capture_key_pressed; AVFrame *frame; }; -struct screen_params { +struct sc_screen_params { + struct sc_controller *controller; + struct sc_file_pusher *fp; + struct sc_key_processor *kp; + struct sc_mouse_processor *mp; + + bool forward_all_clicks; + bool legacy_paste; + bool clipboard_autosync; + const struct sc_shortcut_mods *shortcut_mods; + const char *window_title; - struct size frame_size; bool always_on_top; - 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 + int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_width; + uint16_t window_height; bool window_borderless; - uint8_t rotation; + enum sc_orientation orientation; bool mipmaps; bool fullscreen; - - sc_tick buffering_time; + bool start_fps_counter; }; // initialize screen, create window, renderer and texture (window is hidden) bool -screen_init(struct screen *screen, const struct screen_params *params); +sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params); // request to interrupt any inner thread // must be called before screen_join() void -screen_interrupt(struct screen *screen); +sc_screen_interrupt(struct sc_screen *screen); // join any inner thread void -screen_join(struct screen *screen); +sc_screen_join(struct sc_screen *screen); // destroy window, renderer and texture (if any) void -screen_destroy(struct screen *screen); +sc_screen_destroy(struct sc_screen *screen); // 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, bool update_content_rect); +sc_screen_hide_window(struct sc_screen *screen); // switch the fullscreen mode void -screen_switch_fullscreen(struct screen *screen); +sc_screen_switch_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void -screen_resize_to_fit(struct screen *screen); +sc_screen_resize_to_fit(struct sc_screen *screen); // resize window to 1:1 (pixel-perfect) void -screen_resize_to_pixel_perfect(struct screen *screen); +sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); -// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +// set the display orientation void -screen_set_rotation(struct screen *screen, unsigned rotation); +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation); // react to SDL events +// If this function returns false, scrcpy must exit with an error. bool -screen_handle_event(struct screen *screen, SDL_Event *event); +sc_screen_handle_event(struct sc_screen *screen, const 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); +struct sc_point +sc_screen_convert_window_to_frame_coords(struct sc_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); +struct sc_point +sc_screen_convert_drawable_to_frame_coords(struct sc_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); +sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y); #endif diff --git a/app/src/server.c b/app/src/server.c index e3c8c344..4d55e994 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -3,21 +3,25 @@ #include #include #include -#include #include #include #include -#include "adb.h" +#include "adb/adb.h" +#include "util/binary.h" +#include "util/file.h" #include "util/log.h" -#include "util/net.h" -#include "util/str_util.h" +#include "util/net_intr.h" +#include "util/process_intr.h" +#include "util/str.h" -#define SOCKET_NAME "scrcpy" -#define SERVER_FILENAME "scrcpy-server" +#define SC_SERVER_FILENAME "scrcpy-server" -#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME -#define DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" +#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME +#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" + +#define SC_ADB_PORT_DEFAULT 5555 +#define SC_SOCKET_NAME_PREFIX "scrcpy_" static char * get_server_path(void) { @@ -29,12 +33,12 @@ get_server_path(void) { if (server_path_env) { // if the envvar is set, use it #ifdef __WINDOWS__ - char *server_path = utf8_from_wide_char(server_path_env); + char *server_path = sc_str_from_wchars(server_path_env); #else char *server_path = strdup(server_path_env); #endif if (!server_path) { - LOGE("Could not allocate memory"); + LOG_OOM(); return NULL; } LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); @@ -42,207 +46,90 @@ get_server_path(void) { } #ifndef PORTABLE - LOGD("Using server: " DEFAULT_SERVER_PATH); - char *server_path = strdup(DEFAULT_SERVER_PATH); + LOGD("Using server: " SC_SERVER_PATH_DEFAULT); + char *server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { - LOGE("Could not allocate memory"); + LOG_OOM(); return NULL; } - // the absolute path is hardcoded - return server_path; #else - - // use scrcpy-server in the same directory as the executable - char *executable_path = get_executable_path(); - if (!executable_path) { - LOGE("Could not get executable path, " - "using " SERVER_FILENAME " from current directory"); - // not found, use current directory - return strdup(SERVER_FILENAME); - } - - // dirname() does not work correctly everywhere, so get the parent - // directory manually. - // See - char *p = strrchr(executable_path, PATH_SEPARATOR); - if (!p) { - LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", - executable_path, PATH_SEPARATOR); - free(executable_path); - return strdup(SERVER_FILENAME); - } - - *p = '\0'; // modify executable_path in place - char *dir = executable_path; - size_t dirlen = strlen(dir); - - // sizeof(SERVER_FILENAME) gives statically the size including the null byte - size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); - char *server_path = malloc(len); + char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME); if (!server_path) { - LOGE("Could not alloc server path string, " - "using " SERVER_FILENAME " from current directory"); - free(executable_path); - return strdup(SERVER_FILENAME); + LOGE("Could not get local file path, " + "using " SC_SERVER_FILENAME " from current directory"); + return strdup(SC_SERVER_FILENAME); } - memcpy(server_path, dir, dirlen); - server_path[dirlen] = PATH_SEPARATOR; - memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); - // the final null byte has been copied with SERVER_FILENAME - - free(executable_path); - LOGD("Using server (portable): %s", server_path); - return server_path; #endif -} - -static bool -push_server(const char *serial) { - 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); - free(server_path); - return process_check_success(process, "adb push", true); -} - -static bool -enable_tunnel_reverse(const char *serial, uint16_t local_port) { - process_t process = adb_reverse(serial, SOCKET_NAME, local_port); - return process_check_success(process, "adb reverse", true); -} -static bool -disable_tunnel_reverse(const char *serial) { - process_t process = adb_reverse_remove(serial, SOCKET_NAME); - return process_check_success(process, "adb reverse --remove", true); + return server_path; } -static bool -enable_tunnel_forward(const char *serial, uint16_t local_port) { - process_t process = adb_forward(serial, local_port, SOCKET_NAME); - return process_check_success(process, "adb forward", true); +static void +sc_server_params_destroy(struct sc_server_params *params) { + // The server stores a copy of the params provided by the user + free((char *) params->req_serial); + free((char *) params->crop); + free((char *) params->video_codec_options); + free((char *) params->audio_codec_options); + free((char *) params->video_encoder); + free((char *) params->audio_encoder); + free((char *) params->tcpip_dst); + free((char *) params->camera_id); + free((char *) params->camera_ar); } static bool -disable_tunnel_forward(const char *serial, uint16_t local_port) { - process_t process = adb_forward_remove(serial, local_port); - return process_check_success(process, "adb forward --remove", true); -} +sc_server_params_copy(struct sc_server_params *dst, + const struct sc_server_params *src) { + *dst = *src; + + // The params reference user-allocated memory, so we must copy them to + // handle them from another thread + +#define COPY(FIELD) do { \ + dst->FIELD = NULL; \ + if (src->FIELD) { \ + dst->FIELD = strdup(src->FIELD); \ + if (!dst->FIELD) { \ + goto error; \ + } \ + } \ +} while(0) + + COPY(req_serial); + COPY(crop); + COPY(video_codec_options); + COPY(audio_codec_options); + COPY(video_encoder); + COPY(audio_encoder); + COPY(tcpip_dst); + COPY(camera_id); + COPY(camera_ar); +#undef COPY -static bool -disable_tunnel(struct server *server) { - if (server->tunnel_forward) { - return disable_tunnel_forward(server->serial, server->local_port); - } - return disable_tunnel_reverse(server->serial); -} + return true; -static socket_t -listen_on_port(uint16_t port) { -#define IPV4_LOCALHOST 0x7F000001 - return net_listen(IPV4_LOCALHOST, port, 1); +error: + sc_server_params_destroy(dst); + return false; } 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); - } +push_server(struct sc_intr *intr, const char *serial) { + char *server_path = get_server_path(); + if (!server_path) { 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); - } + if (!sc_file_is_regular(server_path)) { + LOGE("'%s' does not exist or is not a regular file\n", server_path); + free(server_path); return false; } -} - -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); + bool ok = sc_adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0); + free(server_path); + return ok; } static const char * @@ -264,25 +151,76 @@ log_level_to_server_string(enum sc_log_level level) { } } -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", +static bool +sc_server_sleep(struct sc_server *server, sc_tick deadline) { + sc_mutex_lock(&server->mutex); + bool timed_out = false; + while (!server->stopped && !timed_out) { + timed_out = !sc_cond_timedwait(&server->cond_stopped, + &server->mutex, deadline); + } + bool stopped = server->stopped; + sc_mutex_unlock(&server->mutex); + + return !stopped; +} + +static const char * +sc_server_get_codec_name(enum sc_codec codec) { + switch (codec) { + case SC_CODEC_H264: + return "h264"; + case SC_CODEC_H265: + return "h265"; + case SC_CODEC_AV1: + return "av1"; + case SC_CODEC_OPUS: + return "opus"; + case SC_CODEC_AAC: + return "aac"; + case SC_CODEC_FLAC: + return "flac"; + case SC_CODEC_RAW: + return "raw"; + default: + return NULL; + } +} + +static const char * +sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { + switch (camera_facing) { + case SC_CAMERA_FACING_FRONT: + return "front"; + case SC_CAMERA_FACING_BACK: + return "back"; + case SC_CAMERA_FACING_EXTERNAL: + return "external"; + default: + return NULL; + } +} + +static sc_pid +execute_server(struct sc_server *server, + const struct sc_server_params *params) { + sc_pid pid = SC_PROCESS_NONE; + + const char *serial = server->serial; + assert(serial); + + const char *cmd[128]; + unsigned count = 0; + cmd[count++] = sc_adb_get_executable(); + cmd[count++] = "-s"; + cmd[count++] = serial; + cmd[count++] = "shell"; + cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; + cmd[count++] = "app_process"; + #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" + cmd[count++] = # ifdef SERVER_DEBUGGER_METHOD_NEW /* Android 9 and above */ "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y," @@ -291,27 +229,147 @@ execute_server(struct server *server, const struct server_params *params) { /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" # endif - SERVER_DEBUGGER_PORT, + 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", - }; + cmd[count++] = "/"; // unused + cmd[count++] = "com.genymobile.scrcpy.Server"; + cmd[count++] = SCRCPY_VERSION; + + unsigned dyn_idx = count; // from there, the strings are allocated +#define ADD_PARAM(fmt, ...) do { \ + char *p; \ + if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ + goto end; \ + } \ + cmd[count++] = p; \ + } while(0) + + ADD_PARAM("scid=%08x", params->scid); + ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); + + if (!params->video) { + ADD_PARAM("video=false"); + } + if (params->video_bit_rate) { + ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate); + } + if (!params->audio) { + ADD_PARAM("audio=false"); + } + if (params->audio_bit_rate) { + ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate); + } + if (params->video_codec != SC_CODEC_H264) { + ADD_PARAM("video_codec=%s", + sc_server_get_codec_name(params->video_codec)); + } + if (params->audio_codec != SC_CODEC_OPUS) { + ADD_PARAM("audio_codec=%s", + sc_server_get_codec_name(params->audio_codec)); + } + if (params->video_source != SC_VIDEO_SOURCE_DISPLAY) { + assert(params->video_source == SC_VIDEO_SOURCE_CAMERA); + ADD_PARAM("video_source=camera"); + } + if (params->audio_source == SC_AUDIO_SOURCE_MIC) { + ADD_PARAM("audio_source=mic"); + } + if (params->max_size) { + ADD_PARAM("max_size=%" PRIu16, params->max_size); + } + if (params->max_fps) { + ADD_PARAM("max_fps=%" PRIu16, params->max_fps); + } + if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + ADD_PARAM("lock_video_orientation=%" PRIi8, + params->lock_video_orientation); + } + if (server->tunnel.forward) { + ADD_PARAM("tunnel_forward=true"); + } + if (params->crop) { + ADD_PARAM("crop=%s", params->crop); + } + if (!params->control) { + // By default, control is true + ADD_PARAM("control=false"); + } + if (params->display_id) { + ADD_PARAM("display_id=%" PRIu32, params->display_id); + } + if (params->camera_id) { + ADD_PARAM("camera_id=%s", params->camera_id); + } + if (params->camera_size) { + ADD_PARAM("camera_size=%s", params->camera_size); + } + if (params->camera_facing != SC_CAMERA_FACING_ANY) { + ADD_PARAM("camera_facing=%s", + sc_server_get_camera_facing_name(params->camera_facing)); + } + if (params->camera_ar) { + ADD_PARAM("camera_ar=%s", params->camera_ar); + } + if (params->camera_fps) { + ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps); + } + if (params->camera_high_speed) { + ADD_PARAM("camera_high_speed=true"); + } + if (params->show_touches) { + ADD_PARAM("show_touches=true"); + } + if (params->stay_awake) { + ADD_PARAM("stay_awake=true"); + } + if (params->video_codec_options) { + ADD_PARAM("video_codec_options=%s", params->video_codec_options); + } + if (params->audio_codec_options) { + ADD_PARAM("audio_codec_options=%s", params->audio_codec_options); + } + if (params->video_encoder) { + ADD_PARAM("video_encoder=%s", params->video_encoder); + } + if (params->audio_encoder) { + ADD_PARAM("audio_encoder=%s", params->audio_encoder); + } + if (params->power_off_on_close) { + ADD_PARAM("power_off_on_close=true"); + } + if (!params->clipboard_autosync) { + // By default, clipboard_autosync is true + ADD_PARAM("clipboard_autosync=false"); + } + if (!params->downsize_on_error) { + // By default, downsize_on_error is true + ADD_PARAM("downsize_on_error=false"); + } + if (!params->cleanup) { + // By default, cleanup is true + ADD_PARAM("cleanup=false"); + } + if (!params->power_on) { + // By default, power_on is true + ADD_PARAM("power_on=false"); + } + if (params->list & SC_OPTION_LIST_ENCODERS) { + ADD_PARAM("list_encoders=true"); + } + if (params->list & SC_OPTION_LIST_DISPLAYS) { + ADD_PARAM("list_displays=true"); + } + if (params->list & SC_OPTION_LIST_CAMERAS) { + ADD_PARAM("list_cameras=true"); + } + if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { + ADD_PARAM("list_camera_sizes=true"); + } + +#undef ADD_PARAM + + cmd[count++] = NULL; + #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); @@ -323,276 +381,728 @@ execute_server(struct server *server, const struct server_params *params) { // Port: 5005 // Then click on "Debug" #endif - return adb_execute(server->serial, cmd, ARRAY_LEN(cmd)); + // Inherit both stdout and stderr (all server logs are printed to stdout) + pid = sc_adb_execute(cmd, 0); + +end: + for (unsigned i = dyn_idx; i < count; ++i) { + free((char *) cmd[i]); + } + + return pid; } -static socket_t -connect_and_read_byte(uint16_t port) { - socket_t socket = net_connect(IPV4_LOCALHOST, port); - if (socket == INVALID_SOCKET) { - return INVALID_SOCKET; +static bool +connect_and_read_byte(struct sc_intr *intr, sc_socket socket, + uint32_t tunnel_host, uint16_t tunnel_port) { + bool ok = net_connect_intr(intr, socket, tunnel_host, tunnel_port); + if (!ok) { + return false; } char byte; // the connection may succeed even if the server behind the "adb tunnel" // is not listening, so read one byte to detect a working connection - if (net_recv(socket, &byte, 1) != 1) { + if (net_recv_intr(intr, socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel - net_close(socket); - return INVALID_SOCKET; + return false; } - return socket; + + return true; } -static socket_t -connect_to_server(uint16_t port, uint32_t attempts, uint32_t delay) { +static sc_socket +connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, + uint32_t host, uint16_t port) { do { - LOGD("Remaining connection attempts: %d", (int) attempts); - socket_t socket = connect_and_read_byte(port); - if (socket != INVALID_SOCKET) { - // it worked! - return socket; + LOGD("Remaining connection attempts: %u", attempts); + sc_socket socket = net_socket(); + if (socket != SC_SOCKET_NONE) { + bool ok = connect_and_read_byte(&server->intr, socket, host, port); + if (ok) { + // it worked! + return socket; + } + + net_close(socket); } + + if (sc_intr_is_interrupted(&server->intr)) { + // Stop immediately + break; + } + if (attempts) { - SDL_Delay(delay); + sc_tick deadline = sc_tick_now() + delay; + bool ok = sc_server_sleep(server, deadline); + if (!ok) { + LOGI("Connection attempt stopped"); + break; + } } - } while (--attempts > 0); - return INVALID_SOCKET; + } while (--attempts); + return SC_SOCKET_NONE; } -static void -close_socket(socket_t socket) { - assert(socket != INVALID_SOCKET); - net_shutdown(socket, SHUT_RDWR); - if (!net_close(socket)) { - LOGW("Could not close socket"); +bool +sc_server_init(struct sc_server *server, const struct sc_server_params *params, + const struct sc_server_callbacks *cbs, void *cbs_userdata) { + bool ok = sc_server_params_copy(&server->params, params); + if (!ok) { + LOG_OOM(); + return false; } -} -bool -server_init(struct server *server) { - server->serial = NULL; - server->process = PROCESS_NONE; - atomic_flag_clear_explicit(&server->server_socket_closed, - memory_order_relaxed); + ok = sc_mutex_init(&server->mutex); + if (!ok) { + sc_server_params_destroy(&server->params); + return false; + } - bool ok = sc_mutex_init(&server->mutex); + ok = sc_cond_init(&server->cond_stopped); if (!ok) { + sc_mutex_destroy(&server->mutex); + sc_server_params_destroy(&server->params); return false; } - ok = sc_cond_init(&server->process_terminated_cond); + ok = sc_intr_init(&server->intr); if (!ok) { + sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); + sc_server_params_destroy(&server->params); return false; } - server->process_terminated = false; + server->serial = NULL; + server->device_socket_name = NULL; + server->stopped = false; + + server->video_socket = SC_SOCKET_NONE; + server->audio_socket = SC_SOCKET_NONE; + server->control_socket = SC_SOCKET_NONE; - server->server_socket = INVALID_SOCKET; - server->video_socket = INVALID_SOCKET; - server->control_socket = INVALID_SOCKET; + sc_adb_tunnel_init(&server->tunnel); - server->local_port = 0; + assert(cbs); + assert(cbs->on_connection_failed); + assert(cbs->on_connected); + assert(cbs->on_disconnected); - server->tunnel_enabled = false; - server->tunnel_forward = false; + server->cbs = cbs; + server->cbs_userdata = cbs_userdata; return true; } -static int -run_wait_server(void *data) { - struct server *server = data; - process_wait(server->process, false); // ignore exit code +static bool +device_read_info(struct sc_intr *intr, sc_socket device_socket, + struct sc_server_info *info) { + uint8_t buf[SC_DEVICE_NAME_FIELD_LENGTH]; + ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); + if (r < SC_DEVICE_NAME_FIELD_LENGTH) { + LOGE("Could not retrieve device information"); + return false; + } + // in case the client sends garbage + buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; + memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); - sc_mutex_lock(&server->mutex); - server->process_terminated = true; - sc_cond_signal(&server->process_terminated_cond); - sc_mutex_unlock(&server->mutex); + return true; +} + +static bool +sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { + struct sc_adb_tunnel *tunnel = &server->tunnel; + + assert(tunnel->enabled); + + const char *serial = server->serial; + assert(serial); + + bool video = server->params.video; + bool audio = server->params.audio; + bool control = server->params.control; + + sc_socket video_socket = SC_SOCKET_NONE; + sc_socket audio_socket = SC_SOCKET_NONE; + sc_socket control_socket = SC_SOCKET_NONE; + if (!tunnel->forward) { + if (video) { + video_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (video_socket == SC_SOCKET_NONE) { + goto fail; + } + } + + if (audio) { + audio_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (audio_socket == SC_SOCKET_NONE) { + goto fail; + } + } - // 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); + if (control) { + control_socket = + net_accept_intr(&server->intr, tunnel->server_socket); + if (control_socket == SC_SOCKET_NONE) { + goto fail; + } + } + } else { + uint32_t tunnel_host = server->params.tunnel_host; + if (!tunnel_host) { + tunnel_host = IPV4_LOCALHOST; + } + + uint16_t tunnel_port = server->params.tunnel_port; + if (!tunnel_port) { + tunnel_port = tunnel->local_port; + } + + unsigned attempts = 100; + sc_tick delay = SC_TICK_FROM_MS(100); + sc_socket first_socket = connect_to_server(server, attempts, delay, + tunnel_host, tunnel_port); + if (first_socket == SC_SOCKET_NONE) { + goto fail; + } + + if (video) { + video_socket = first_socket; + } + + if (audio) { + if (!video) { + audio_socket = first_socket; + } else { + audio_socket = net_socket(); + if (audio_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, audio_socket, + tunnel_host, tunnel_port); + if (!ok) { + goto fail; + } + } + } + + if (control) { + if (!video && !audio) { + control_socket = first_socket; + } else { + control_socket = net_socket(); + if (control_socket == SC_SOCKET_NONE) { + goto fail; + } + bool ok = net_connect_intr(&server->intr, control_socket, + tunnel_host, tunnel_port); + if (!ok) { + goto fail; + } + } + } } - 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; + // we don't need the adb tunnel anymore + sc_adb_tunnel_close(tunnel, &server->intr, serial, + server->device_socket_name); + + sc_socket first_socket = video ? video_socket + : audio ? audio_socket + : control_socket; + + // The sockets will be closed on stop if device_read_info() fails + bool ok = device_read_info(&server->intr, first_socket, info); + if (!ok) { + goto fail; + } + + assert(!video || video_socket != SC_SOCKET_NONE); + assert(!audio || audio_socket != SC_SOCKET_NONE); + assert(!control || control_socket != SC_SOCKET_NONE); + + server->video_socket = video_socket; + server->audio_socket = audio_socket; + server->control_socket = control_socket; + + return true; + +fail: + if (video_socket != SC_SOCKET_NONE) { + if (!net_close(video_socket)) { + LOGW("Could not close video socket"); } } - if (!push_server(params->serial)) { - /* server->serial will be freed on server_destroy() */ - return false; + if (audio_socket != SC_SOCKET_NONE) { + if (!net_close(audio_socket)) { + LOGW("Could not close audio socket"); + } } - if (!enable_tunnel_any_port(server, params->port_range, - params->force_adb_forward)) { - return false; + if (control_socket != SC_SOCKET_NONE) { + if (!net_close(control_socket)) { + LOGW("Could not close control socket"); + } } - // server will connect to our server socket - server->process = execute_server(server, params); - if (server->process == PROCESS_NONE) { - goto error; - } - - // If the server process dies before connecting to the server socket, then - // the client will be stuck forever on accept(). To avoid the problem, we - // must be able to wake up the accept() call when the server dies. To keep - // things simple and multiplatform, just spawn a new thread waiting for the - // server process and calling shutdown()/close() on the server socket if - // necessary to wake up any accept() blocking call. - bool ok = sc_thread_create(&server->wait_server_thread, run_wait_server, - "wait-server", server); + if (tunnel->enabled) { + // Always leave this function with tunnel disabled + sc_adb_tunnel_close(tunnel, &server->intr, serial, + server->device_socket_name); + } + + return false; +} + +static void +sc_server_on_terminated(void *userdata) { + struct sc_server *server = userdata; + + // If the server process dies before connecting to the server socket, + // then the client will be stuck forever on accept(). To avoid the problem, + // wake up the accept() call (or any other) when the server dies, like on + // stop() (it is safe to call interrupt() twice). + sc_intr_interrupt(&server->intr); + + server->cbs->on_disconnected(server, server->cbs_userdata); + + LOGD("Server terminated"); +} + +static uint16_t +get_adb_tcp_port(struct sc_server *server, const char *serial) { + struct sc_intr *intr = &server->intr; + + char *current_port = + sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); + if (!current_port) { + return 0; + } + + long value; + bool ok = sc_str_parse_integer(current_port, &value); + free(current_port); if (!ok) { - process_terminate(server->process); - process_wait(server->process, true); // ignore exit code - goto error; + return 0; } - server->tunnel_enabled = true; + if (value < 0 || value > 0xFFFF) { + return 0; + } - return true; + return value; +} -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); +static bool +wait_tcpip_mode_enabled(struct sc_server *server, const char *serial, + uint16_t expected_port, unsigned attempts, + sc_tick delay) { + uint16_t adb_port = get_adb_tcp_port(server, serial); + if (adb_port == expected_port) { + return true; } - disable_tunnel(server); + // Only print this log if TCP/IP is not enabled + LOGI("Waiting for TCP/IP mode enabled..."); + + do { + sc_tick deadline = sc_tick_now() + delay; + if (!sc_server_sleep(server, deadline)) { + LOGI("TCP/IP mode waiting interrupted"); + return false; + } + + adb_port = get_adb_tcp_port(server, serial); + if (adb_port == expected_port) { + return true; + } + } while (--attempts); return false; } +static char * +append_port(const char *ip, uint16_t port) { + char *ip_port; + int ret = asprintf(&ip_port, "%s:%" PRIu16, ip, port); + if (ret == -1) { + LOG_OOM(); + return NULL; + } + + return ip_port; +} + +static char * +sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { + assert(serial); + + struct sc_intr *intr = &server->intr; + + LOGI("Switching device %s to TCP/IP...", serial); + + char *ip = sc_adb_get_device_ip(intr, serial, 0); + if (!ip) { + LOGE("Device IP not found"); + return NULL; + } + + uint16_t adb_port = get_adb_tcp_port(server, serial); + if (adb_port) { + LOGI("TCP/IP mode already enabled on port %" PRIu16, adb_port); + } else { + LOGI("Enabling TCP/IP mode on port " SC_STR(SC_ADB_PORT_DEFAULT) "..."); + + bool ok = sc_adb_tcpip(intr, serial, SC_ADB_PORT_DEFAULT, + SC_ADB_NO_STDOUT); + if (!ok) { + LOGE("Could not restart adbd in TCP/IP mode"); + free(ip); + return NULL; + } + + unsigned attempts = 40; + sc_tick delay = SC_TICK_FROM_MS(250); + ok = wait_tcpip_mode_enabled(server, serial, SC_ADB_PORT_DEFAULT, + attempts, delay); + if (!ok) { + free(ip); + return NULL; + } + + adb_port = SC_ADB_PORT_DEFAULT; + LOGI("TCP/IP mode enabled on port " SC_STR(SC_ADB_PORT_DEFAULT)); + } + + char *ip_port = append_port(ip, adb_port); + free(ip); + return ip_port; +} + static bool -device_read_info(socket_t device_socket, char *device_name, struct size *size) { - unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4]; - ssize_t r = net_recv_all(device_socket, buf, sizeof(buf)); - if (r < DEVICE_NAME_FIELD_LENGTH + 4) { - LOGE("Could not retrieve device information"); +sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { + struct sc_intr *intr = &server->intr; + + // Error expected if not connected, do not report any error + sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); + + LOGI("Connecting to %s...", ip_port); + + bool ok = sc_adb_connect(intr, ip_port, 0); + if (!ok) { + LOGE("Could not connect to %s", ip_port); 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]; + + LOGI("Connected to %s", ip_port); return true; } -bool -server_connect_to(struct server *server, char *device_name, struct size *size) { - if (!server->tunnel_forward) { - server->video_socket = net_accept(server->server_socket); - if (server->video_socket == INVALID_SOCKET) { +static bool +sc_server_configure_tcpip_known_address(struct sc_server *server, + const char *addr) { + // Append ":5555" if no port is present + bool contains_port = strchr(addr, ':'); + char *ip_port = contains_port ? strdup(addr) + : append_port(addr, SC_ADB_PORT_DEFAULT); + if (!ip_port) { + LOG_OOM(); + return false; + } + + server->serial = ip_port; + return sc_server_connect_to_tcpip(server, ip_port); +} + +static bool +sc_server_configure_tcpip_unknown_address(struct sc_server *server, + const char *serial) { + bool is_already_tcpip = + sc_adb_device_get_type(serial) == SC_ADB_DEVICE_TYPE_TCPIP; + if (is_already_tcpip) { + // Nothing to do + LOGI("Device already connected via TCP/IP: %s", serial); + server->serial = strdup(serial); + if (!server->serial) { + LOG_OOM(); return false; } + return true; + } - server->control_socket = net_accept(server->server_socket); - if (server->control_socket == INVALID_SOCKET) { - // the video_socket will be cleaned up on destroy - return false; + char *ip_port = sc_server_switch_to_tcpip(server, serial); + if (!ip_port) { + return false; + } + + server->serial = ip_port; + return sc_server_connect_to_tcpip(server, ip_port); +} + +static void +sc_server_kill_adb_if_requested(struct sc_server *server) { + if (server->params.kill_adb_on_close) { + LOGI("Killing adb server..."); + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + sc_adb_kill_server(&server->intr, flags); + } +} + +static int +run_server(void *data) { + struct sc_server *server = data; + + const struct sc_server_params *params = &server->params; + + // Execute "adb start-server" before "adb devices" so that daemon starting + // output/errors is correctly printed in the console ("adb devices" output + // is parsed, so it is not output) + bool ok = sc_adb_start_server(&server->intr, 0); + if (!ok) { + LOGE("Could not start adb server"); + goto error_connection_failed; + } + + // params->tcpip_dst implies params->tcpip + assert(!params->tcpip_dst || params->tcpip); + + // If tcpip_dst parameter is given, then it must connect to this address. + // Therefore, the device is unknown, so serial is meaningless at this point. + assert(!params->req_serial || !params->tcpip_dst); + + // A device must be selected via a serial in all cases except when --tcpip= + // is called with a parameter (in that case, the device may initially not + // exist, and scrcpy will execute "adb connect"). + bool need_initial_serial = !params->tcpip_dst; + + if (need_initial_serial) { + // At most one of the 3 following parameters may be set + assert(!!params->req_serial + + params->select_usb + + params->select_tcpip <= 1); + + struct sc_adb_device_selector selector; + if (params->req_serial) { + selector.type = SC_ADB_DEVICE_SELECT_SERIAL; + selector.serial = params->req_serial; + } else if (params->select_usb) { + selector.type = SC_ADB_DEVICE_SELECT_USB; + } else if (params->select_tcpip) { + selector.type = SC_ADB_DEVICE_SELECT_TCPIP; + } else { + // No explicit selection, check $ANDROID_SERIAL + const char *env_serial = getenv("ANDROID_SERIAL"); + if (env_serial) { + LOGI("Using ANDROID_SERIAL: %s", env_serial); + selector.type = SC_ADB_DEVICE_SELECT_SERIAL; + selector.serial = env_serial; + } else { + selector.type = SC_ADB_DEVICE_SELECT_ALL; + } + } + struct sc_adb_device device; + ok = sc_adb_select_device(&server->intr, &selector, 0, &device); + if (!ok) { + goto error_connection_failed; } - // we don't need the server socket anymore - if (!atomic_flag_test_and_set(&server->server_socket_closed)) { - // close it from here - close_socket(server->server_socket); - // otherwise, it is closed by run_wait_server() + if (params->tcpip) { + assert(!params->tcpip_dst); + ok = sc_server_configure_tcpip_unknown_address(server, + device.serial); + sc_adb_device_destroy(&device); + if (!ok) { + goto error_connection_failed; + } + assert(server->serial); + } else { + // "move" the device.serial without copy + server->serial = device.serial; + // the serial must not be freed by the destructor + device.serial = NULL; + sc_adb_device_destroy(&device); } } else { - uint32_t attempts = 100; - uint32_t delay = 100; // ms - server->video_socket = - connect_to_server(server->local_port, attempts, delay); - if (server->video_socket == INVALID_SOCKET) { - return false; + ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); + if (!ok) { + goto error_connection_failed; } + } - // we know that the device is listening, we don't need several attempts - server->control_socket = - net_connect(IPV4_LOCALHOST, server->local_port); - if (server->control_socket == INVALID_SOCKET) { - return false; + const char *serial = server->serial; + assert(serial); + LOGD("Device serial: %s", serial); + + ok = push_server(&server->intr, serial); + if (!ok) { + goto error_connection_failed; + } + + // If --list-* is passed, then the server just prints the requested data + // then exits. + if (params->list) { + sc_pid pid = execute_server(server, params); + if (pid == SC_PROCESS_NONE) { + goto error_connection_failed; } + sc_process_wait(pid, NULL); // ignore exit code + sc_process_close(pid); + // Wake up await_for_server() + server->cbs->on_connected(server, server->cbs_userdata); + return 0; } - // we don't need the adb tunnel anymore - disable_tunnel(server); // ignore failure - server->tunnel_enabled = false; + int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x", + params->scid); + if (r == -1) { + LOG_OOM(); + goto error_connection_failed; + } + assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8); + assert(server->device_socket_name); - // The sockets will be closed on stop if device_read_info() fails - return device_read_info(server->video_socket, device_name, size); -} + ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial, + server->device_socket_name, params->port_range, + params->force_adb_forward); + if (!ok) { + goto error_connection_failed; + } -void -server_stop(struct server *server) { - if (server->server_socket != INVALID_SOCKET - && !atomic_flag_test_and_set(&server->server_socket_closed)) { - close_socket(server->server_socket); + // server will connect to our server socket + sc_pid pid = execute_server(server, params); + if (pid == SC_PROCESS_NONE) { + sc_adb_tunnel_close(&server->tunnel, &server->intr, serial, + server->device_socket_name); + goto error_connection_failed; + } + + static const struct sc_process_listener listener = { + .on_terminated = sc_server_on_terminated, + }; + struct sc_process_observer observer; + ok = sc_process_observer_init(&observer, pid, &listener, server); + if (!ok) { + sc_process_terminate(pid); + sc_process_wait(pid, true); // ignore exit code + sc_adb_tunnel_close(&server->tunnel, &server->intr, serial, + server->device_socket_name); + goto error_connection_failed; } - if (server->video_socket != INVALID_SOCKET) { - close_socket(server->video_socket); + + ok = sc_server_connect_to(server, &server->info); + // The tunnel is always closed by server_connect_to() + if (!ok) { + sc_process_terminate(pid); + sc_process_wait(pid, true); // ignore exit code + sc_process_observer_join(&observer); + sc_process_observer_destroy(&observer); + goto error_connection_failed; + } + + // Now connected + server->cbs->on_connected(server, server->cbs_userdata); + + // Wait for server_stop() + sc_mutex_lock(&server->mutex); + while (!server->stopped) { + sc_cond_wait(&server->cond_stopped, &server->mutex); } - if (server->control_socket != INVALID_SOCKET) { - close_socket(server->control_socket); + sc_mutex_unlock(&server->mutex); + + // Interrupt sockets to wake up socket blocking calls on the server + + if (server->video_socket != SC_SOCKET_NONE) { + // There is no video_socket if --no-video is set + net_interrupt(server->video_socket); } - assert(server->process != PROCESS_NONE); + if (server->audio_socket != SC_SOCKET_NONE) { + // There is no audio_socket if --no-audio is set + net_interrupt(server->audio_socket); + } - if (server->tunnel_enabled) { - // ignore failure - disable_tunnel(server); + if (server->control_socket != SC_SOCKET_NONE) { + // There is no control_socket if --no-control is set + net_interrupt(server->control_socket); } // Give some delay for the server to terminate properly - sc_mutex_lock(&server->mutex); - bool signaled = false; - if (!server->process_terminated) { #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) - signaled = sc_cond_timedwait(&server->process_terminated_cond, - &server->mutex, - sc_tick_now() + WATCHDOG_DELAY); - } - sc_mutex_unlock(&server->mutex); + sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY; + bool terminated = sc_process_observer_timedwait(&observer, deadline); // After this delay, kill the server if it's not dead already. // On some devices, closing the sockets is not sufficient to wake up the // blocking calls while the device is asleep. - if (!signaled) { - // The process is terminated, but not reaped (closed) yet, so its PID - // is still valid. + if (!terminated) { + // The process may have terminated since the check, but it is not + // reaped (closed) yet, so its PID is still valid, and it is ok to call + // sc_process_terminate() even in that case. LOGW("Killing the server..."); - process_terminate(server->process); + sc_process_terminate(pid); } - sc_thread_join(&server->wait_server_thread, NULL); - process_close(server->process); + sc_process_observer_join(&observer); + sc_process_observer_destroy(&observer); + + sc_process_close(pid); + + sc_server_kill_adb_if_requested(server); + + return 0; + +error_connection_failed: + sc_server_kill_adb_if_requested(server); + server->cbs->on_connection_failed(server, server->cbs_userdata); + return -1; +} + +bool +sc_server_start(struct sc_server *server) { + bool ok = + sc_thread_create(&server->thread, run_server, "scrcpy-server", server); + if (!ok) { + LOGE("Could not create server thread"); + return false; + } + + return true; } void -server_destroy(struct server *server) { +sc_server_stop(struct sc_server *server) { + sc_mutex_lock(&server->mutex); + server->stopped = true; + sc_cond_signal(&server->cond_stopped); + sc_intr_interrupt(&server->intr); + sc_mutex_unlock(&server->mutex); +} + +void +sc_server_join(struct sc_server *server) { + sc_thread_join(&server->thread, NULL); +} + +void +sc_server_destroy(struct sc_server *server) { + if (server->video_socket != SC_SOCKET_NONE) { + net_close(server->video_socket); + } + if (server->audio_socket != SC_SOCKET_NONE) { + net_close(server->audio_socket); + } + if (server->control_socket != SC_SOCKET_NONE) { + net_close(server->control_socket); + } + free(server->serial); - sc_cond_destroy(&server->process_terminated_cond); + free(server->device_socket_name); + sc_server_params_destroy(&server->params); + sc_intr_destroy(&server->intr); + sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); } diff --git a/app/src/server.h b/app/src/server.h index c249b374..062af0a9 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -7,70 +7,129 @@ #include #include -#include "adb.h" +#include "adb/adb_tunnel.h" #include "coords.h" -#include "scrcpy.h" +#include "options.h" +#include "util/intr.h" #include "util/log.h" #include "util/net.h" #include "util/thread.h" -struct server { - char *serial; - process_t process; - sc_thread wait_server_thread; - atomic_flag server_socket_closed; - - sc_mutex mutex; - sc_cond process_terminated_cond; - bool process_terminated; - - socket_t server_socket; // only used if !tunnel_forward - socket_t video_socket; - socket_t control_socket; - uint16_t local_port; // selected from port_range - bool tunnel_enabled; - bool tunnel_forward; // use "adb forward" instead of "adb reverse" +#define SC_DEVICE_NAME_FIELD_LENGTH 64 +struct sc_server_info { + char device_name[SC_DEVICE_NAME_FIELD_LENGTH]; }; -struct server_params { - const char *serial; +struct sc_server_params { + uint32_t scid; + const char *req_serial; enum sc_log_level log_level; + enum sc_codec video_codec; + enum sc_codec audio_codec; + enum sc_video_source video_source; + enum sc_audio_source audio_source; + enum sc_camera_facing camera_facing; const char *crop; - const char *codec_options; - const char *encoder_name; + const char *video_codec_options; + const char *audio_codec_options; + const char *video_encoder; + const char *audio_encoder; + const char *camera_id; + const char *camera_size; + const char *camera_ar; + uint16_t camera_fps; struct sc_port_range port_range; + uint32_t tunnel_host; + uint16_t tunnel_port; uint16_t max_size; - uint32_t bit_rate; + uint32_t video_bit_rate; + uint32_t audio_bit_rate; uint16_t max_fps; int8_t lock_video_orientation; bool control; uint32_t display_id; + bool video; + bool audio; bool show_touches; bool stay_awake; bool force_adb_forward; bool power_off_on_close; + bool clipboard_autosync; + bool downsize_on_error; + bool tcpip; + const char *tcpip_dst; + bool select_usb; + bool select_tcpip; + bool cleanup; + bool power_on; + bool kill_adb_on_close; + bool camera_high_speed; + uint8_t list; }; -// init default values -bool -server_init(struct server *server); +struct sc_server { + // The internal allocated strings are copies owned by the server + struct sc_server_params params; + char *serial; + char *device_socket_name; + + sc_thread thread; + struct sc_server_info info; // initialized once connected + + sc_mutex mutex; + sc_cond cond_stopped; + bool stopped; + + struct sc_intr intr; + struct sc_adb_tunnel tunnel; -// push, enable tunnel et start the server + sc_socket video_socket; + sc_socket audio_socket; + sc_socket control_socket; + + const struct sc_server_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_server_callbacks { + /** + * Called when the server failed to connect + * + * If it is called, then on_connected() and on_disconnected() will never be + * called. + */ + void (*on_connection_failed)(struct sc_server *server, void *userdata); + + /** + * Called on server connection + */ + void (*on_connected)(struct sc_server *server, void *userdata); + + /** + * Called on server disconnection (after it has been connected) + */ + void (*on_disconnected)(struct sc_server *server, void *userdata); +}; + +// init the server with the given params bool -server_start(struct server *server, const struct server_params *params); +sc_server_init(struct sc_server *server, const struct sc_server_params *params, + const struct sc_server_callbacks *cbs, void *cbs_userdata); -#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 +// start the server asynchronously bool -server_connect_to(struct server *server, char *device_name, struct size *size); +sc_server_start(struct sc_server *server); // disconnect and kill the server process void -server_stop(struct server *server); +sc_server_stop(struct sc_server *server); + +// join the server thread +void +sc_server_join(struct sc_server *server); // close and release sockets void -server_destroy(struct server *server); +sc_server_destroy(struct sc_server *server); #endif diff --git a/app/src/stream.c b/app/src/stream.c deleted file mode 100644 index adc6277f..00000000 --- a/app/src/stream.c +++ /dev/null @@ -1,298 +0,0 @@ -#include "stream.h" - -#include -#include -#include -#include - -#include "decoder.h" -#include "events.h" -#include "recorder.h" -#include "util/buffer_util.h" -#include "util/log.h" - -#define BUFSIZE 0x10000 - -#define HEADER_SIZE 12 -#define NO_PTS UINT64_C(-1) - -static bool -stream_recv_packet(struct stream *stream, AVPacket *packet) { - // The video stream contains raw packets, without time information. When we - // record, we retrieve the timestamps separately, from a "meta" header - // added by the server before each raw packet. - // - // The "meta" header length is 12 bytes: - // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... - // <-------------> <-----> <-----------------------------... - // PTS packet raw packet - // size - // - // It is followed by bytes containing the packet/frame. - - uint8_t header[HEADER_SIZE]; - ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE); - if (r < HEADER_SIZE) { - return false; - } - - uint64_t pts = buffer_read64be(header); - uint32_t len = buffer_read32be(&header[8]); - assert(pts == NO_PTS || (pts & 0x8000000000000000) == 0); - assert(len); - - if (av_new_packet(packet, len)) { - LOGE("Could not allocate packet"); - return false; - } - - r = net_recv_all(stream->socket, packet->data, len); - if (r < 0 || ((uint32_t) r) < len) { - av_packet_unref(packet); - return false; - } - - packet->pts = pts != NO_PTS ? (int64_t) pts : AV_NOPTS_VALUE; - - return true; -} - -static bool -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; - } - } - - return true; -} - -static bool -stream_parse(struct stream *stream, AVPacket *packet) { - uint8_t *in_data = packet->data; - int in_len = packet->size; - uint8_t *out_data = NULL; - int out_len = 0; - int r = av_parser_parse2(stream->parser, stream->codec_ctx, - &out_data, &out_len, in_data, in_len, - AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1); - - // PARSER_FLAG_COMPLETE_FRAMES is set - assert(r == in_len); - (void) r; - assert(out_len == in_len); - - if (stream->parser->key_frame == 1) { - packet->flags |= AV_PKT_FLAG_KEY; - } - - packet->dts = packet->pts; - - bool ok = push_packet_to_sinks(stream, packet); - if (!ok) { - LOGE("Could not process packet"); - return false; - } - - return true; -} - -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 immediately (it contains no - // frame); instead, it must be concatenated with the future data packet. - if (stream->pending || is_config) { - size_t offset; - 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; - 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; - } - } - - 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; - } - } - - if (is_config) { - // config packet - bool ok = push_packet_to_sinks(stream, packet); - if (!ok) { - return false; - } - } else { - // data packet - bool ok = stream_parse(stream, packet); - - if (stream->pending) { - // the pending packet must be discarded (consumed or error) - av_packet_free(&stream->pending); - } - - if (!ok) { - return false; - } - } - 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; - - AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); - if (!codec) { - LOGE("H.264 decoder not found"); - goto end; - } - - stream->codec_ctx = avcodec_alloc_context3(codec); - if (!stream->codec_ctx) { - LOGC("Could not allocate codec context"); - goto end; - } - - if (!stream_open_sinks(stream, codec)) { - LOGE("Could not open stream sinks"); - goto finally_free_codec_ctx; - } - - stream->parser = av_parser_init(AV_CODEC_ID_H264); - if (!stream->parser) { - LOGE("Could not initialize parser"); - goto finally_close_sinks; - } - - // We must only pass complete frames to av_parser_parse2()! - // It's more complicated, but this allows to reduce the latency by 1 frame! - stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; - - AVPacket *packet = av_packet_alloc(); - if (!packet) { - LOGE("Could not allocate packet"); - goto finally_close_parser; - } - - for (;;) { - bool ok = stream_recv_packet(stream, packet); - if (!ok) { - // end of stream - break; - } - - ok = stream_push_packet(stream, packet); - av_packet_unref(packet); - if (!ok) { - // cannot process packet (error already logged) - break; - } - } - - LOGD("End of frames"); - - if (stream->pending) { - av_packet_free(&stream->pending); - } - - av_packet_free(&packet); -finally_close_parser: - av_parser_close(stream->parser); -finally_close_sinks: - stream_close_sinks(stream); -finally_free_codec_ctx: - avcodec_free_context(&stream->codec_ctx); -end: - stream->cbs->on_eos(stream, stream->cbs_userdata); - - return 0; -} - -void -stream_init(struct stream *stream, socket_t socket, - const struct stream_callbacks *cbs, void *cbs_userdata) { - stream->socket = socket; - 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"); - - 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_join(struct stream *stream) { - sc_thread_join(&stream->thread, NULL); -} diff --git a/app/src/stream.h b/app/src/stream.h deleted file mode 100644 index d7047c95..00000000 --- a/app/src/stream.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef STREAM_H -#define STREAM_H - -#include "common.h" - -#include -#include -#include - -#include "trait/packet_sink.h" -#include "util/net.h" -#include "util/thread.h" - -#define STREAM_MAX_SINKS 2 - -struct stream { - socket_t socket; - 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 - 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, - 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_join(struct stream *stream); - -#endif diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c new file mode 100644 index 00000000..9c3f7333 --- /dev/null +++ b/app/src/sys/unix/file.c @@ -0,0 +1,81 @@ +#include "util/file.h" + +#include +#include +#include +#include +#include +#include + +#include "util/log.h" + +bool +sc_file_executable_exists(const char *file) { + char *path = getenv("PATH"); + if (!path) + return false; + path = strdup(path); + if (!path) + return false; + + bool ret = false; + size_t file_len = strlen(file); + char *saveptr; + for (char *dir = strtok_r(path, ":", &saveptr); dir; + dir = strtok_r(NULL, ":", &saveptr)) { + size_t dir_len = strlen(dir); + char *fullpath = malloc(dir_len + file_len + 2); + if (!fullpath) + { + LOG_OOM(); + continue; + } + memcpy(fullpath, dir, dir_len); + fullpath[dir_len] = '/'; + memcpy(fullpath + dir_len + 1, file, file_len + 1); + + struct stat sb; + bool fullpath_executable = stat(fullpath, &sb) == 0 && + sb.st_mode & S_IXUSR; + free(fullpath); + if (fullpath_executable) { + ret = true; + break; + } + } + + free(path); + return ret; +} + +char * +sc_file_get_executable_path(void) { +// +#ifdef __linux__ + char buf[PATH_MAX + 1]; // +1 for the null byte + ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); + if (len == -1) { + perror("readlink"); + return NULL; + } + buf[len] = '\0'; + return strdup(buf); +#else + // in practice, we only need this feature for portable builds, only used on + // Windows, so we don't care implementing it for every platform + // (it's useful to have a working version on Linux for debugging though) + return NULL; +#endif +} + +bool +sc_file_is_regular(const char *path) { + struct stat path_stat; + + if (stat(path, &path_stat)) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} + diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 8683a2da..8c4a53c3 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -1,128 +1,204 @@ #include "util/process.h" +#include #include #include -#include #include -#include -#include -#include #include #include #include #include "util/log.h" -bool -search_executable(const char *file) { - char *path = getenv("PATH"); - if (!path) - return false; - path = strdup(path); - if (!path) - return false; - - 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; - } - } +enum sc_process_result +sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, + int *pin, int *pout, int *perr) { + bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT); + bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR); - free(path); - return ret; -} - -enum process_result -process_execute(const char *const argv[], pid_t *pid) { - int fd[2]; + int in[2]; + int out[2]; + int err[2]; + int internal[2]; // communication between parent and children - if (pipe(fd) == -1) { + if (pipe(internal) == -1) { perror("pipe"); - return PROCESS_ERROR_GENERIC; + return SC_PROCESS_ERROR_GENERIC; + } + if (pin) { + if (pipe(in) == -1) { + perror("pipe"); + close(internal[0]); + close(internal[1]); + return SC_PROCESS_ERROR_GENERIC; + } + } + if (pout) { + if (pipe(out) == -1) { + perror("pipe"); + // clean up + if (pin) { + close(in[0]); + close(in[1]); + } + close(internal[0]); + close(internal[1]); + return SC_PROCESS_ERROR_GENERIC; + } + } + if (perr) { + if (pipe(err) == -1) { + perror("pipe"); + // clean up + if (pout) { + close(out[0]); + close(out[1]); + } + if (pin) { + close(in[0]); + close(in[1]); + } + close(internal[0]); + close(internal[1]); + return SC_PROCESS_ERROR_GENERIC; + } } - - enum process_result ret = PROCESS_SUCCESS; *pid = fork(); if (*pid == -1) { perror("fork"); - ret = PROCESS_ERROR_GENERIC; - goto end; + // clean up + if (perr) { + close(err[0]); + close(err[1]); + } + if (pout) { + close(out[0]); + close(out[1]); + } + if (pin) { + close(in[0]); + close(in[1]); + } + close(internal[0]); + close(internal[1]); + return SC_PROCESS_ERROR_GENERIC; } - if (*pid > 0) { - // parent close write side - close(fd[1]); - fd[1] = -1; - // wait for EOF or receive errno from child - if (read(fd[0], &ret, sizeof(ret)) == -1) { - perror("read"); - ret = PROCESS_ERROR_GENERIC; - goto end; + if (*pid == 0) { + if (pin) { + if (in[0] != STDIN_FILENO) { + dup2(in[0], STDIN_FILENO); + close(in[0]); + } + close(in[1]); + } else { + int devnull = open("/dev/null", O_RDONLY | O_CREAT, 0666); + if (devnull != -1) { + dup2(devnull, STDIN_FILENO); + } else { + LOGE("Could not open /dev/null for stdin"); + } + } + + if (pout) { + if (out[1] != STDOUT_FILENO) { + dup2(out[1], STDOUT_FILENO); + close(out[1]); + } + close(out[0]); + } else if (!inherit_stdout) { + int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); + if (devnull != -1) { + dup2(devnull, STDOUT_FILENO); + } else { + LOGE("Could not open /dev/null for stdout"); + } } - } else if (*pid == 0) { - // child close read side - close(fd[0]); - if (fcntl(fd[1], F_SETFD, FD_CLOEXEC) == 0) { - execvp(argv[0], (char *const *)argv); - if (errno == ENOENT) { - ret = PROCESS_ERROR_MISSING_BINARY; + + if (perr) { + if (err[1] != STDERR_FILENO) { + dup2(err[1], STDERR_FILENO); + close(err[1]); + } + close(err[0]); + } else if (!inherit_stderr) { + int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); + if (devnull != -1) { + dup2(devnull, STDERR_FILENO); } else { - ret = PROCESS_ERROR_GENERIC; + LOGE("Could not open /dev/null for stderr"); } + } + + close(internal[0]); + enum sc_process_result err; + + // Somehow SDL masks many signals - undo them for other processes + // https://github.com/libsdl-org/SDL/blob/release-2.0.18/src/thread/pthread/SDL_systhread.c#L167 + sigset_t mask; + sigemptyset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + + if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { + execvp(argv[0], (char *const *) argv); perror("exec"); + err = errno == ENOENT ? SC_PROCESS_ERROR_MISSING_BINARY + : SC_PROCESS_ERROR_GENERIC; } else { perror("fcntl"); - ret = PROCESS_ERROR_GENERIC; + err = SC_PROCESS_ERROR_GENERIC; } - // send ret to the parent - if (write(fd[1], &ret, sizeof(ret)) == -1) { + // send err to the parent + if (write(internal[1], &err, sizeof(err)) == -1) { perror("write"); } - // close write side before exiting - close(fd[1]); + close(internal[1]); _exit(1); } -end: - if (fd[0] != -1) { - close(fd[0]); + // parent + assert(*pid > 0); + + close(internal[1]); + + enum sc_process_result res = SC_PROCESS_SUCCESS; + // wait for EOF or receive err from child + if (read(internal[0], &res, sizeof(res)) == -1) { + perror("read"); + res = SC_PROCESS_ERROR_GENERIC; + } + + close(internal[0]); + + if (pin) { + close(in[0]); + *pin = in[1]; + } + if (pout) { + *pout = out[0]; + close(out[1]); } - if (fd[1] != -1) { - close(fd[1]); + if (perr) { + *perr = err[0]; + close(err[1]); } - return ret; + + return res; } bool -process_terminate(pid_t pid) { +sc_process_terminate(pid_t pid) { if (pid <= 0) { - LOGC("Requested to kill %d, this is an error. Please report the bug.\n", + LOGE("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid); abort(); } return kill(pid, SIGKILL) != -1; } -exit_code_t -process_wait(pid_t pid, bool close) { +sc_exit_code +sc_process_wait(pid_t pid, bool close) { int code; int options = WEXITED; if (!close) { @@ -133,7 +209,7 @@ process_wait(pid_t pid, bool close) { int r = waitid(P_PID, pid, &info, options); if (r == -1 || info.si_code != CLD_EXITED) { // could not wait, or exited unexpectedly, probably by a signal - code = NO_EXIT_CODE; + code = SC_EXIT_CODE_NONE; } else { code = info.si_status; } @@ -141,37 +217,18 @@ process_wait(pid_t pid, bool close) { } void -process_close(pid_t pid) { - process_wait(pid, true); // ignore exit code +sc_process_close(pid_t pid) { + sc_process_wait(pid, true); // ignore exit code } -char * -get_executable_path(void) { -// -#ifdef __linux__ - char buf[PATH_MAX + 1]; // +1 for the null byte - ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX); - if (len == -1) { - perror("readlink"); - return NULL; - } - buf[len] = '\0'; - return strdup(buf); -#else - // in practice, we only need this feature for portable builds, only used on - // Windows, so we don't care implementing it for every platform - // (it's useful to have a working version on Linux for debugging though) - return NULL; -#endif +ssize_t +sc_pipe_read(int pipe, char *data, size_t len) { + return read(pipe, data, len); } -bool -is_regular_file(const char *path) { - struct stat path_stat; - - if (stat(path, &path_stat)) { - perror("stat"); - return false; +void +sc_pipe_close(int pipe) { + if (close(pipe)) { + perror("close pipe"); } - return S_ISREG(path_stat.st_mode); } diff --git a/app/src/sys/win/file.c b/app/src/sys/win/file.c new file mode 100644 index 00000000..d3cf1760 --- /dev/null +++ b/app/src/sys/win/file.c @@ -0,0 +1,43 @@ +#include "util/file.h" + +#include + +#include + +#include "util/log.h" +#include "util/str.h" + +char * +sc_file_get_executable_path(void) { + HMODULE hModule = GetModuleHandleW(NULL); + if (!hModule) { + return NULL; + } + WCHAR buf[MAX_PATH + 1]; // +1 for the null byte + int len = GetModuleFileNameW(hModule, buf, MAX_PATH); + if (!len) { + return NULL; + } + buf[len] = '\0'; + return sc_str_from_wchars(buf); +} + +bool +sc_file_is_regular(const char *path) { + wchar_t *wide_path = sc_str_to_wchars(path); + if (!wide_path) { + LOG_OOM(); + return false; + } + + struct _stat path_stat; + int r = _wstat(wide_path, &path_stat); + free(wide_path); + + if (r) { + perror("stat"); + return false; + } + return S_ISREG(path_stat.st_mode); +} + diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index aafd5d34..6e9da09c 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -1,10 +1,11 @@ #include "util/process.h" +#include + #include -#include #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" #define CMD_MAX_LEN 8192 @@ -14,61 +15,218 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) { // // only make it work for this very specific program // (don't handle escaping nor quotes) - size_t ret = xstrjoin(cmd, argv, ' ', len); + size_t ret = sc_str_join(cmd, argv, ' ', len); if (ret >= len) { - LOGE("Command too long (%" PRIsizet " chars)", len - 1); + LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1); return false; } return true; } -enum process_result -process_execute(const char *const argv[], HANDLE *handle) { - STARTUPINFOW si; +enum sc_process_result +sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, + HANDLE *pin, HANDLE *pout, HANDLE *perr) { + bool inherit_stdout = !pout && !(flags & SC_PROCESS_NO_STDOUT); + bool inherit_stderr = !perr && !(flags & SC_PROCESS_NO_STDERR); + + // Add 1 per non-NULL pointer + unsigned handle_count = !!pin || !!pout || !!perr; + + enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = TRUE; + + HANDLE stdin_read_handle; + HANDLE stdout_write_handle; + HANDLE stderr_write_handle; + if (pin) { + if (!CreatePipe(&stdin_read_handle, pin, &sa, 0)) { + perror("pipe"); + return SC_PROCESS_ERROR_GENERIC; + } + if (!SetHandleInformation(*pin, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation stdin failed"); + goto error_close_stdin; + } + } + if (pout) { + if (!CreatePipe(pout, &stdout_write_handle, &sa, 0)) { + perror("pipe"); + goto error_close_stdin; + } + if (!SetHandleInformation(*pout, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation stdout failed"); + goto error_close_stdout; + } + } + if (perr) { + if (!CreatePipe(perr, &stderr_write_handle, &sa, 0)) { + perror("pipe"); + goto error_close_stdout; + } + if (!SetHandleInformation(*perr, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation stderr failed"); + goto error_close_stderr; + } + } + + STARTUPINFOEXW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); - si.cb = sizeof(si); + si.StartupInfo.cb = sizeof(si); + HANDLE handles[3]; + + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + if (inherit_stdout) { + si.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + } + if (inherit_stderr) { + si.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); + } + + LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; + if (handle_count) { + unsigned i = 0; + if (pin) { + si.StartupInfo.hStdInput = stdin_read_handle; + handles[i++] = si.StartupInfo.hStdInput; + } + if (pout) { + assert(!inherit_stdout); + si.StartupInfo.hStdOutput = stdout_write_handle; + handles[i++] = si.StartupInfo.hStdOutput; + } + if (perr) { + assert(!inherit_stderr); + si.StartupInfo.hStdError = stderr_write_handle; + handles[i++] = si.StartupInfo.hStdError; + } + + SIZE_T size; + // Call it once to know the required buffer size + BOOL ok = + InitializeProcThreadAttributeList(NULL, 1, 0, &size) + || GetLastError() == ERROR_INSUFFICIENT_BUFFER; + if (!ok) { + goto error_close_stderr; + } + + lpAttributeList = malloc(size); + if (!lpAttributeList) { + LOG_OOM(); + goto error_close_stderr; + } + + ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size); + if (!ok) { + free(lpAttributeList); + goto error_close_stderr; + } + + ok = UpdateProcThreadAttribute(lpAttributeList, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + handles, handle_count * sizeof(HANDLE), + NULL, NULL); + if (!ok) { + goto error_free_attribute_list; + } + + si.lpAttributeList = lpAttributeList; + } char *cmd = malloc(CMD_MAX_LEN); if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { - *handle = NULL; - return PROCESS_ERROR_GENERIC; + LOG_OOM(); + goto error_free_attribute_list; } - wchar_t *wide = utf8_to_wide_char(cmd); + wchar_t *wide = sc_str_to_wchars(cmd); free(cmd); if (!wide) { - LOGC("Could not allocate wide char string"); - return PROCESS_ERROR_GENERIC; + LOG_OOM(); + goto error_free_attribute_list; } - if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si, - &pi)) { - free(wide); - *handle = NULL; - if (GetLastError() == ERROR_FILE_NOT_FOUND) { - return PROCESS_ERROR_MISSING_BINARY; + BOOL bInheritHandles = handle_count > 0 || inherit_stdout || inherit_stderr; + DWORD dwCreationFlags = 0; + if (handle_count > 0) { + dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT; + } + if (!inherit_stdout && !inherit_stderr) { + // DETACHED_PROCESS to disable stdin, stdout and stderr + dwCreationFlags |= DETACHED_PROCESS; + } + BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles, + dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); + free(wide); + if (!ok) { + int err = GetLastError(); + LOGE("CreateProcessW() error %d", err); + if (err == ERROR_FILE_NOT_FOUND) { + ret = SC_PROCESS_ERROR_MISSING_BINARY; } - return PROCESS_ERROR_GENERIC; + goto error_free_attribute_list; + } + + if (lpAttributeList) { + DeleteProcThreadAttributeList(lpAttributeList); + free(lpAttributeList); + } + + // These handles are used by the child process, close them for this process + if (pin) { + CloseHandle(stdin_read_handle); + } + if (pout) { + CloseHandle(stdout_write_handle); + } + if (perr) { + CloseHandle(stderr_write_handle); } - free(wide); *handle = pi.hProcess; - return PROCESS_SUCCESS; + + return SC_PROCESS_SUCCESS; + +error_free_attribute_list: + if (lpAttributeList) { + DeleteProcThreadAttributeList(lpAttributeList); + free(lpAttributeList); + } +error_close_stderr: + if (perr) { + CloseHandle(*perr); + CloseHandle(stderr_write_handle); + } +error_close_stdout: + if (pout) { + CloseHandle(*pout); + CloseHandle(stdout_write_handle); + } +error_close_stdin: + if (pin) { + CloseHandle(*pin); + CloseHandle(stdin_read_handle); + } + + return ret; } bool -process_terminate(HANDLE handle) { +sc_process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } -exit_code_t -process_wait(HANDLE handle, bool close) { +sc_exit_code +sc_process_wait(HANDLE handle, bool close) { DWORD code; if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { // could not wait or retrieve the exit code - code = NO_EXIT_CODE; // max value, it's unsigned + code = SC_EXIT_CODE_NONE; } if (close) { CloseHandle(handle); @@ -77,42 +235,24 @@ process_wait(HANDLE handle, bool close) { } void -process_close(HANDLE handle) { +sc_process_close(HANDLE handle) { bool closed = CloseHandle(handle); assert(closed); (void) closed; } -char * -get_executable_path(void) { - HMODULE hModule = GetModuleHandleW(NULL); - if (!hModule) { - return NULL; - } - WCHAR buf[MAX_PATH + 1]; // +1 for the null byte - int len = GetModuleFileNameW(hModule, buf, MAX_PATH); - if (!len) { - return NULL; +ssize_t +sc_pipe_read(HANDLE pipe, char *data, size_t len) { + DWORD r; + if (!ReadFile(pipe, data, len, &r, NULL)) { + return -1; } - buf[len] = '\0'; - return utf8_from_wide_char(buf); + return r; } -bool -is_regular_file(const char *path) { - wchar_t *wide_path = utf8_to_wide_char(path); - if (!wide_path) { - LOGC("Could not allocate wide char string"); - return false; - } - - struct _stat path_stat; - int r = _wstat(wide_path, &path_stat); - free(wide_path); - - if (r) { - perror("stat"); - return false; +void +sc_pipe_close(HANDLE pipe) { + if (!CloseHandle(pipe)) { + LOGW("Cannot close pipe"); } - return S_ISREG(path_stat.st_mode); } diff --git a/app/src/tiny_xpm.c b/app/src/tiny_xpm.c deleted file mode 100644 index df1f9e53..00000000 --- a/app/src/tiny_xpm.c +++ /dev/null @@ -1,119 +0,0 @@ -#include "tiny_xpm.h" - -#include -#include -#include -#include -#include - -#include "util/log.h" - -struct index { - char c; - uint32_t color; -}; - -static bool -find_color(struct index *index, int len, char c, uint32_t *color) { - // there are typically very few color, so it's ok to iterate over the array - for (int i = 0; i < len; ++i) { - if (index[i].c == c) { - *color = index[i].color; - return true; - } - } - *color = 0; - return false; -} - -// We encounter some problems with SDL2_image on MSYS2 (Windows), -// so here is our own XPM parsing not to depend on SDL_image. -// -// We do not hardcode the binary image to keep some flexibility to replace the -// icon easily (just by replacing icon.xpm). -// -// Parameter is not "const char *" because XPM formats are generally stored in a -// (non-const) "char *" -SDL_Surface * -read_xpm(char *xpm[]) { -#ifndef NDEBUG - // patch the XPM to change the icon color in debug mode - xpm[2] = ". c #CC00CC"; -#endif - - char *endptr; - // *** No error handling, assume the XPM source is valid *** - // (it's in our source repo) - // Assertions are only checked in debug - int width = strtol(xpm[0], &endptr, 10); - int height = strtol(endptr + 1, &endptr, 10); - int colors = strtol(endptr + 1, &endptr, 10); - int chars = strtol(endptr + 1, &endptr, 10); - - // sanity checks - assert(0 <= width && width < 256); - assert(0 <= height && height < 256); - assert(0 <= colors && colors < 256); - assert(chars == 1); // this implementation does not support more - - (void) chars; - - // init index - struct index index[colors]; - for (int i = 0; i < colors; ++i) { - const char *line = xpm[1+i]; - index[i].c = line[0]; - assert(line[1] == '\t'); - assert(line[2] == 'c'); - assert(line[3] == ' '); - if (line[4] == '#') { - index[i].color = 0xff000000 | strtol(&line[5], &endptr, 0x10); - assert(*endptr == '\0'); - } else { - assert(!strcmp("None", &line[4])); - index[i].color = 0; - } - } - - // parse image - uint32_t *pixels = SDL_malloc(4 * width * height); - if (!pixels) { - LOGE("Could not allocate icon memory"); - return NULL; - } - for (int y = 0; y < height; ++y) { - const char *line = xpm[1 + colors + y]; - for (int x = 0; x < width; ++x) { - char c = line[x]; - uint32_t color; - bool color_found = find_color(index, colors, c, &color); - assert(color_found); - (void) color_found; - pixels[y * width + x] = color; - } - } - -#if SDL_BYTEORDER == SDL_BIG_ENDIAN - uint32_t amask = 0x000000ff; - uint32_t rmask = 0x0000ff00; - uint32_t gmask = 0x00ff0000; - uint32_t bmask = 0xff000000; -#else // little endian, like x86 - uint32_t amask = 0xff000000; - uint32_t rmask = 0x00ff0000; - uint32_t gmask = 0x0000ff00; - uint32_t bmask = 0x000000ff; -#endif - - SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, - width, height, - 32, 4 * width, - rmask, gmask, bmask, amask); - if (!surface) { - LOGE("Could not create icon surface"); - return NULL; - } - // make the surface own the raw pixels - surface->flags &= ~SDL_PREALLOC; - return surface; -} diff --git a/app/src/tiny_xpm.h b/app/src/tiny_xpm.h deleted file mode 100644 index 29b42d14..00000000 --- a/app/src/tiny_xpm.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef TINYXPM_H -#define TINYXPM_H - -#include "common.h" - -#include - -SDL_Surface * -read_xpm(char *xpm[]); - -#endif diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 64ab0de9..8ef248b6 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -1,12 +1,11 @@ -#ifndef SC_FRAME_SINK -#define SC_FRAME_SINK +#ifndef SC_FRAME_SINK_H +#define SC_FRAME_SINK_H #include "common.h" #include #include - -typedef struct AVFrame AVFrame; +#include /** * Frame sink trait. @@ -18,7 +17,8 @@ struct sc_frame_sink { }; struct sc_frame_sink_ops { - bool (*open)(struct sc_frame_sink *sink); + /* The codec context is valid until the sink is closed */ + bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx); void (*close)(struct sc_frame_sink *sink); bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); }; diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c new file mode 100644 index 00000000..416eccd9 --- /dev/null +++ b/app/src/trait/frame_source.c @@ -0,0 +1,59 @@ +#include "frame_source.h" + +void +sc_frame_source_init(struct sc_frame_source *source) { + source->sink_count = 0; +} + +void +sc_frame_source_add_sink(struct sc_frame_source *source, + struct sc_frame_sink *sink) { + assert(source->sink_count < SC_FRAME_SOURCE_MAX_SINKS); + assert(sink); + assert(sink->ops); + source->sinks[source->sink_count++] = sink; +} + +static void +sc_frame_source_sinks_close_firsts(struct sc_frame_source *source, + unsigned count) { + while (count) { + struct sc_frame_sink *sink = source->sinks[--count]; + sink->ops->close(sink); + } +} + +bool +sc_frame_source_sinks_open(struct sc_frame_source *source, + const AVCodecContext *ctx) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_frame_sink *sink = source->sinks[i]; + if (!sink->ops->open(sink, ctx)) { + sc_frame_source_sinks_close_firsts(source, i); + return false; + } + } + + return true; +} + +void +sc_frame_source_sinks_close(struct sc_frame_source *source) { + assert(source->sink_count); + sc_frame_source_sinks_close_firsts(source, source->sink_count); +} + +bool +sc_frame_source_sinks_push(struct sc_frame_source *source, + const AVFrame *frame) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_frame_sink *sink = source->sinks[i]; + if (!sink->ops->push(sink, frame)) { + return false; + } + } + + return true; +} diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h new file mode 100644 index 00000000..94222af0 --- /dev/null +++ b/app/src/trait/frame_source.h @@ -0,0 +1,38 @@ +#ifndef SC_FRAME_SOURCE_H +#define SC_FRAME_SOURCE_H + +#include "common.h" + +#include "frame_sink.h" + +#define SC_FRAME_SOURCE_MAX_SINKS 2 + +/** + * Frame source trait + * + * Component able to send AVFrames should implement this trait. + */ +struct sc_frame_source { + struct sc_frame_sink *sinks[SC_FRAME_SOURCE_MAX_SINKS]; + unsigned sink_count; +}; + +void +sc_frame_source_init(struct sc_frame_source *source); + +void +sc_frame_source_add_sink(struct sc_frame_source *source, + struct sc_frame_sink *sink); + +bool +sc_frame_source_sinks_open(struct sc_frame_source *source, + const AVCodecContext *ctx); + +void +sc_frame_source_sinks_close(struct sc_frame_source *source); + +bool +sc_frame_source_sinks_push(struct sc_frame_source *source, + const AVFrame *frame); + +#endif diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h new file mode 100644 index 00000000..96374413 --- /dev/null +++ b/app/src/trait/key_processor.h @@ -0,0 +1,62 @@ +#ifndef SC_KEY_PROCESSOR_H +#define SC_KEY_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include "input_events.h" + +/** + * Key processor trait. + * + * Component able to process and inject keys should implement this trait. + */ +struct sc_key_processor { + /** + * Set by the implementation to indicate that it must explicitly wait for + * the clipboard to be set on the device before injecting Ctrl+v to avoid + * race conditions. If it is set, the input_manager will pass a valid + * ack_to_wait to process_key() in case of clipboard synchronization + * resulting of the key event. + */ + bool async_paste; + + /** + * Set by the implementation to indicate that the keyboard is HID. In + * practice, it is used to react on a shortcut to open the hard keyboard + * settings only if the keyboard is HID. + */ + bool hid; + + const struct sc_key_processor_ops *ops; +}; + +struct sc_key_processor_ops { + + /** + * Process a keyboard event + * + * The `sequence` number (if different from `SC_SEQUENCE_INVALID`) indicates + * the acknowledgement number to wait for before injecting this event. + * This allows to ensure that the device clipboard is set before injecting + * Ctrl+v on the device. + * + * This function is mandatory. + */ + void + (*process_key)(struct sc_key_processor *kp, + const struct sc_key_event *event, uint64_t ack_to_wait); + + /** + * Process an input text + * + * This function is optional. + */ + void + (*process_text)(struct sc_key_processor *kp, + const struct sc_text_event *event); +}; + +#endif diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h new file mode 100644 index 00000000..6e0b596e --- /dev/null +++ b/app/src/trait/mouse_processor.h @@ -0,0 +1,66 @@ +#ifndef SC_MOUSE_PROCESSOR_H +#define SC_MOUSE_PROCESSOR_H + +#include "common.h" + +#include +#include + +#include "input_events.h" + +/** + * Mouse processor trait. + * + * Component able to process and inject mouse events should implement this + * trait. + */ +struct sc_mouse_processor { + const struct sc_mouse_processor_ops *ops; + + /** + * If set, the mouse processor works in relative mode (the absolute + * position is irrelevant). In particular, it indicates that the mouse + * pointer must be "captured" by the UI. + */ + bool relative_mode; +}; + +struct sc_mouse_processor_ops { + /** + * Process a mouse motion event + * + * This function is mandatory. + */ + void + (*process_mouse_motion)(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event); + + /** + * Process a mouse click event + * + * This function is mandatory. + */ + void + (*process_mouse_click)(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event); + + /** + * Process a mouse scroll event + * + * This function is optional. + */ + void + (*process_mouse_scroll)(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event); + + /** + * Process a touch event + * + * This function is optional. + */ + void + (*process_touch)(struct sc_mouse_processor *mp, + const struct sc_touch_event *event); +}; + +#endif diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index fe9c137d..84cfe814 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -1,13 +1,11 @@ -#ifndef SC_PACKET_SINK -#define SC_PACKET_SINK +#ifndef SC_PACKET_SINK_H +#define SC_PACKET_SINK_H #include "common.h" #include #include - -typedef struct AVCodec AVCodec; -typedef struct AVPacket AVPacket; +#include /** * Packet sink trait. @@ -19,9 +17,20 @@ struct sc_packet_sink { }; struct sc_packet_sink_ops { - bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); + /* The codec context is valid until the sink is closed */ + bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx); void (*close)(struct sc_packet_sink *sink); bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); + + /*/ + * Called when the input stream has been disabled at runtime. + * + * If it is called, then open(), close() and push() will never be called. + * + * It is useful to notify the recorder that the requested audio stream has + * finally been disabled because the device could not capture it. + */ + void (*disable)(struct sc_packet_sink *sink); }; #endif diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c new file mode 100644 index 00000000..c0836f1d --- /dev/null +++ b/app/src/trait/packet_source.c @@ -0,0 +1,70 @@ +#include "packet_source.h" + +void +sc_packet_source_init(struct sc_packet_source *source) { + source->sink_count = 0; +} + +void +sc_packet_source_add_sink(struct sc_packet_source *source, + struct sc_packet_sink *sink) { + assert(source->sink_count < SC_PACKET_SOURCE_MAX_SINKS); + assert(sink); + assert(sink->ops); + source->sinks[source->sink_count++] = sink; +} + +static void +sc_packet_source_sinks_close_firsts(struct sc_packet_source *source, + unsigned count) { + while (count) { + struct sc_packet_sink *sink = source->sinks[--count]; + sink->ops->close(sink); + } +} + +bool +sc_packet_source_sinks_open(struct sc_packet_source *source, + AVCodecContext *ctx) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_packet_sink *sink = source->sinks[i]; + if (!sink->ops->open(sink, ctx)) { + sc_packet_source_sinks_close_firsts(source, i); + return false; + } + } + + return true; +} + +void +sc_packet_source_sinks_close(struct sc_packet_source *source) { + assert(source->sink_count); + sc_packet_source_sinks_close_firsts(source, source->sink_count); +} + +bool +sc_packet_source_sinks_push(struct sc_packet_source *source, + const AVPacket *packet) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_packet_sink *sink = source->sinks[i]; + if (!sink->ops->push(sink, packet)) { + return false; + } + } + + return true; +} + +void +sc_packet_source_sinks_disable(struct sc_packet_source *source) { + assert(source->sink_count); + for (unsigned i = 0; i < source->sink_count; ++i) { + struct sc_packet_sink *sink = source->sinks[i]; + if (sink->ops->disable) { + sink->ops->disable(sink); + } + } +} diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h new file mode 100644 index 00000000..16d56e86 --- /dev/null +++ b/app/src/trait/packet_source.h @@ -0,0 +1,41 @@ +#ifndef SC_PACKET_SOURCE_H +#define SC_PACKET_SOURCE_H + +#include "common.h" + +#include "packet_sink.h" + +#define SC_PACKET_SOURCE_MAX_SINKS 2 + +/** + * Packet source trait + * + * Component able to send AVPackets should implement this trait. + */ +struct sc_packet_source { + struct sc_packet_sink *sinks[SC_PACKET_SOURCE_MAX_SINKS]; + unsigned sink_count; +}; + +void +sc_packet_source_init(struct sc_packet_source *source); + +void +sc_packet_source_add_sink(struct sc_packet_source *source, + struct sc_packet_sink *sink); + +bool +sc_packet_source_sinks_open(struct sc_packet_source *source, + AVCodecContext *ctx); + +void +sc_packet_source_sinks_close(struct sc_packet_source *source); + +bool +sc_packet_source_sinks_push(struct sc_packet_source *source, + const AVPacket *packet); + +void +sc_packet_source_sinks_disable(struct sc_packet_source *source); + +#endif diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c new file mode 100644 index 00000000..515a3fd9 --- /dev/null +++ b/app/src/uhid/keyboard_uhid.c @@ -0,0 +1,162 @@ +#include "keyboard_uhid.h" + +#include "util/log.h" + +/** Downcast key processor to keyboard_uhid */ +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) + +/** Downcast uhid_receiver to keyboard_uhid */ +#define DOWNCAST_RECEIVER(UR) \ + container_of(UR, struct sc_keyboard_uhid, uhid_receiver) + +#define UHID_KEYBOARD_ID 1 + +static void +sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb, + const struct sc_hid_event *event) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = UHID_KEYBOARD_ID; + + assert(event->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, event->data, event->size); + msg.uhid_input.size = event->size; + + if (!sc_controller_push_msg(kb->controller, &msg)) { + LOGE("Could not send UHID_INPUT message (key)"); + } +} + +static void +sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) { + SDL_Keymod sdl_mod = SDL_GetModState(); + uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM); + + uint16_t device_mod = + atomic_load_explicit(&kb->device_mod, memory_order_relaxed); + uint16_t diff = mod ^ device_mod; + + if (diff) { + // Inherently racy (the HID output reports arrive asynchronously in + // response to key presses), but will re-synchronize on next key press + // or HID output anyway + atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed); + + struct sc_hid_event hid_event; + sc_hid_keyboard_event_from_mods(&hid_event, diff); + + LOGV("HID keyboard state synchronized"); + + sc_keyboard_uhid_send_input(kb, &hid_event); + } +} + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const struct sc_key_event *event, + uint64_t ack_to_wait) { + (void) ack_to_wait; + + if (event->repeat) { + // In USB HID protocol, key repeat is handled by the host (Android), so + // just ignore key repeat here. + return; + } + + struct sc_keyboard_uhid *kb = DOWNCAST(kp); + + struct sc_hid_event hid_event; + + // Not all keys are supported, just ignore unsupported keys + if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (event->scancode == SC_SCANCODE_CAPSLOCK) { + atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS, + memory_order_relaxed); + } else if (event->scancode == SC_SCANCODE_NUMLOCK) { + atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM, + memory_order_relaxed); + } else { + // Synchronize modifiers (only if the scancode itself does not + // change the modifiers) + sc_keyboard_uhid_synchronize_mod(kb); + } + sc_keyboard_uhid_send_input(kb, &hid_event); + } +} + +static unsigned +sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) { + // + // (chapter 11: LED page) + unsigned mod = 0; + if (hid_led & 0x01) { + mod |= SC_MOD_NUM; + } + if (hid_led & 0x02) { + mod |= SC_MOD_CAPS; + } + return mod; +} + +static void +sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver, + const uint8_t *data, size_t len) { + // Called from the thread receiving device messages + + assert(len); + + // Also check at runtime (do not trust the server) + if (!len) { + LOGE("Unexpected empty HID output message"); + return; + } + + struct sc_keyboard_uhid *kb = DOWNCAST_RECEIVER(receiver); + + uint8_t hid_led = data[0]; + uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led); + atomic_store_explicit(&kb->device_mod, device_mod, memory_order_relaxed); +} + +bool +sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, + struct sc_controller *controller, + struct sc_uhid_devices *uhid_devices) { + sc_hid_keyboard_init(&kb->hid); + + kb->controller = controller; + atomic_init(&kb->device_mod, 0); + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + // Never forward text input via HID (all the keys are injected + // separately) + .process_text = NULL, + }; + + // Clipboard synchronization is requested over the same control socket, so + // there is no need for a specific synchronization mechanism + kb->key_processor.async_paste = false; + kb->key_processor.hid = true; + kb->key_processor.ops = &ops; + + static const struct sc_uhid_receiver_ops uhid_receiver_ops = { + .process_output = sc_uhid_receiver_process_output, + }; + + kb->uhid_receiver.id = UHID_KEYBOARD_ID; + kb->uhid_receiver.ops = &uhid_receiver_ops; + sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = UHID_KEYBOARD_ID; + msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC; + msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN; + if (!sc_controller_push_msg(controller, &msg)) { + LOGE("Could not send UHID_CREATE message (keyboard)"); + return false; + } + + return true; +} diff --git a/app/src/uhid/keyboard_uhid.h b/app/src/uhid/keyboard_uhid.h new file mode 100644 index 00000000..5e1be70c --- /dev/null +++ b/app/src/uhid/keyboard_uhid.h @@ -0,0 +1,27 @@ +#ifndef SC_KEYBOARD_UHID_H +#define SC_KEYBOARD_UHID_H + +#include "common.h" + +#include + +#include "controller.h" +#include "hid/hid_keyboard.h" +#include "uhid/uhid_output.h" +#include "trait/key_processor.h" + +struct sc_keyboard_uhid { + struct sc_key_processor key_processor; // key processor trait + struct sc_uhid_receiver uhid_receiver; + + struct sc_hid_keyboard hid; + struct sc_controller *controller; + atomic_uint_least16_t device_mod; +}; + +bool +sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, + struct sc_controller *controller, + struct sc_uhid_devices *uhid_devices); + +#endif diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c new file mode 100644 index 00000000..77446f9e --- /dev/null +++ b/app/src/uhid/mouse_uhid.c @@ -0,0 +1,89 @@ +#include "mouse_uhid.h" + +#include "hid/hid_mouse.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast mouse processor to mouse_uhid */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor) + +#define UHID_MOUSE_ID 2 + +static void +sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse, + const struct sc_hid_event *event, const char *name) { + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT; + msg.uhid_input.id = UHID_MOUSE_ID; + + assert(event->size <= SC_HID_MAX_SIZE); + memcpy(msg.uhid_input.data, event->data, event->size); + msg.uhid_input.size = event->size; + + if (!sc_controller_push_msg(mouse->controller, &msg)) { + LOGE("Could not send UHID_INPUT message (%s)", name); + } +} + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event) { + struct sc_mouse_uhid *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_motion(&hid_event, event); + + sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion"); +} + +static void +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { + struct sc_mouse_uhid *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_click(&hid_event, event); + + sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click"); +} + +static void +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { + struct sc_mouse_uhid *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_scroll(&hid_event, event); + + sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll"); +} + +bool +sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, + struct sc_controller *controller) { + mouse->controller = controller; + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + // Touch events not supported (coordinates are not relative) + .process_touch = NULL, + }; + + mouse->mouse_processor.ops = &ops; + + mouse->mouse_processor.relative_mode = true; + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; + msg.uhid_create.id = UHID_MOUSE_ID; + msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC; + msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN; + if (!sc_controller_push_msg(controller, &msg)) { + LOGE("Could not send UHID_CREATE message (mouse)"); + return false; + } + + return true; +} diff --git a/app/src/uhid/mouse_uhid.h b/app/src/uhid/mouse_uhid.h new file mode 100644 index 00000000..f117ba97 --- /dev/null +++ b/app/src/uhid/mouse_uhid.h @@ -0,0 +1,19 @@ +#ifndef SC_MOUSE_UHID_H +#define SC_MOUSE_UHID_H + +#include + +#include "controller.h" +#include "trait/mouse_processor.h" + +struct sc_mouse_uhid { + struct sc_mouse_processor mouse_processor; // mouse processor trait + + struct sc_controller *controller; +}; + +bool +sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, + struct sc_controller *controller); + +#endif diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c new file mode 100644 index 00000000..3b095faf --- /dev/null +++ b/app/src/uhid/uhid_output.c @@ -0,0 +1,25 @@ +#include "uhid_output.h" + +#include + +void +sc_uhid_devices_init(struct sc_uhid_devices *devices) { + devices->count = 0; +} + +void +sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, + struct sc_uhid_receiver *receiver) { + assert(devices->count < SC_UHID_MAX_RECEIVERS); + devices->receivers[devices->count++] = receiver; +} + +struct sc_uhid_receiver * +sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id) { + for (size_t i = 0; i < devices->count; ++i) { + if (devices->receivers[i]->id == id) { + return devices->receivers[i]; + } + } + return NULL; +} diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h new file mode 100644 index 00000000..e13eed87 --- /dev/null +++ b/app/src/uhid/uhid_output.h @@ -0,0 +1,45 @@ +#ifndef SC_UHID_OUTPUT_H +#define SC_UHID_OUTPUT_H + +#include "common.h" + +#include +#include + +/** + * The communication with UHID devices is bidirectional. + * + * This component manages the registration of receivers to handle UHID output + * messages (sent from the device to the computer). + */ + +struct sc_uhid_receiver { + uint16_t id; + + const struct sc_uhid_receiver_ops *ops; +}; + +struct sc_uhid_receiver_ops { + void + (*process_output)(struct sc_uhid_receiver *receiver, + const uint8_t *data, size_t len); +}; + +#define SC_UHID_MAX_RECEIVERS 1 + +struct sc_uhid_devices { + struct sc_uhid_receiver *receivers[SC_UHID_MAX_RECEIVERS]; + unsigned count; +}; + +void +sc_uhid_devices_init(struct sc_uhid_devices *devices); + +void +sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices, + struct sc_uhid_receiver *receiver); + +struct sc_uhid_receiver * +sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id); + +#endif diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c new file mode 100644 index 00000000..50bc33fe --- /dev/null +++ b/app/src/usb/aoa_hid.c @@ -0,0 +1,304 @@ +#include "util/log.h" + +#include +#include + +#include "aoa_hid.h" +#include "util/log.h" +#include "util/str.h" + +// See . +#define ACCESSORY_REGISTER_HID 54 +#define ACCESSORY_SET_HID_REPORT_DESC 56 +#define ACCESSORY_SEND_HID_EVENT 57 +#define ACCESSORY_UNREGISTER_HID 55 + +#define DEFAULT_TIMEOUT 1000 + +#define SC_AOA_EVENT_QUEUE_MAX 64 + +static void +sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) { + // HID Event: [00] FF FF FF FF... + assert(event->size); + char *hex = sc_str_to_hex_string(event->data, event->size); + if (!hex) { + return; + } + LOGV("HID Event: [%d] %s", accessory_id, hex); + free(hex); +} + +bool +sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, + struct sc_acksync *acksync) { + sc_vecdeque_init(&aoa->queue); + + if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) { + return false; + } + + if (!sc_mutex_init(&aoa->mutex)) { + sc_vecdeque_destroy(&aoa->queue); + return false; + } + + if (!sc_cond_init(&aoa->event_cond)) { + sc_mutex_destroy(&aoa->mutex); + sc_vecdeque_destroy(&aoa->queue); + return false; + } + + aoa->stopped = false; + aoa->acksync = acksync; + aoa->usb = usb; + + return true; +} + +void +sc_aoa_destroy(struct sc_aoa *aoa) { + sc_vecdeque_destroy(&aoa->queue); + + sc_cond_destroy(&aoa->event_cond); + sc_mutex_destroy(&aoa->mutex); +} + +static bool +sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, + uint16_t report_desc_size) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_REGISTER_HID; + // + // value (arg0): accessory assigned ID for the HID device + // index (arg1): total length of the HID report descriptor + uint16_t value = accessory_id; + uint16_t index = report_desc_size; + unsigned char *data = NULL; + uint16_t length = 0; + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, data, length, + DEFAULT_TIMEOUT); + if (result < 0) { + LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); + return false; + } + + return true; +} + +static bool +sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, + uint16_t report_desc_size) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; + /** + * If the HID descriptor is longer than the endpoint zero max packet size, + * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC + * commands. The data for the descriptor must be sent sequentially + * if multiple packets are needed. + * + * + * libusb handles packet abstraction internally, so we don't need to care + * about bMaxPacketSize0 here. + * + * See + */ + // value (arg0): accessory assigned ID for the HID device + // index (arg1): offset of data in descriptor + uint16_t value = accessory_id; + uint16_t index = 0; + // libusb_control_transfer expects a pointer to non-const + unsigned char *data = (unsigned char *) report_desc; + uint16_t length = report_desc_size; + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, data, length, + DEFAULT_TIMEOUT); + if (result < 0) { + LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); + return false; + } + + return true; +} + +bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size) { + bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); + if (!ok) { + return false; + } + + ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, + report_desc_size); + if (!ok) { + if (!sc_aoa_unregister_hid(aoa, accessory_id)) { + LOGW("Could not unregister HID"); + } + return false; + } + + return true; +} + +static bool +sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, + const struct sc_hid_event *event) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_SEND_HID_EVENT; + // + // value (arg0): accessory assigned ID for the HID device + // index (arg1): 0 (unused) + uint16_t value = accessory_id; + uint16_t index = 0; + unsigned char *data = (uint8_t *) event->data; // discard const + uint16_t length = event->size; + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, data, length, + DEFAULT_TIMEOUT); + if (result < 0) { + LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); + return false; + } + + return true; +} + +bool +sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) { + uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; + uint8_t request = ACCESSORY_UNREGISTER_HID; + // + // value (arg0): accessory assigned ID for the HID device + // index (arg1): 0 + uint16_t value = accessory_id; + uint16_t index = 0; + unsigned char *data = NULL; + uint16_t length = 0; + int result = libusb_control_transfer(aoa->usb->handle, request_type, + request, value, index, data, length, + DEFAULT_TIMEOUT); + if (result < 0) { + LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); + sc_usb_check_disconnected(aoa->usb, result); + return false; + } + + return true; +} + +bool +sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, + uint16_t accessory_id, + const struct sc_hid_event *event, + uint64_t ack_to_wait) { + if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { + sc_hid_event_log(accessory_id, event); + } + + sc_mutex_lock(&aoa->mutex); + bool full = sc_vecdeque_is_full(&aoa->queue); + if (!full) { + bool was_empty = sc_vecdeque_is_empty(&aoa->queue); + + struct sc_aoa_event *aoa_event = + sc_vecdeque_push_hole_noresize(&aoa->queue); + aoa_event->hid = *event; + aoa_event->accessory_id = accessory_id; + aoa_event->ack_to_wait = ack_to_wait; + + if (was_empty) { + sc_cond_signal(&aoa->event_cond); + } + } + // Otherwise (if the queue is full), the event is discarded + + sc_mutex_unlock(&aoa->mutex); + + return !full; +} + +static int +run_aoa_thread(void *data) { + struct sc_aoa *aoa = data; + + for (;;) { + sc_mutex_lock(&aoa->mutex); + while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) { + sc_cond_wait(&aoa->event_cond, &aoa->mutex); + } + if (aoa->stopped) { + // Stop immediately, do not process further events + sc_mutex_unlock(&aoa->mutex); + break; + } + + assert(!sc_vecdeque_is_empty(&aoa->queue)); + struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue); + uint64_t ack_to_wait = event.ack_to_wait; + sc_mutex_unlock(&aoa->mutex); + + if (ack_to_wait != SC_SEQUENCE_INVALID) { + LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait); + + // If some events have ack_to_wait set, then sc_aoa must have been + // initialized with a non NULL acksync + assert(aoa->acksync); + + // Do not block the loop indefinitely if the ack never comes (it + // should never happen) + sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); + enum sc_acksync_wait_result result = + sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); + + if (result == SC_ACKSYNC_WAIT_TIMEOUT) { + LOGW("Ack not received after 500ms, discarding HID event"); + continue; + } else if (result == SC_ACKSYNC_WAIT_INTR) { + // stopped + break; + } + } + + bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid); + if (!ok) { + LOGW("Could not send HID event to USB device"); + } + } + return 0; +} + +bool +sc_aoa_start(struct sc_aoa *aoa) { + LOGD("Starting AOA thread"); + + bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "scrcpy-aoa", aoa); + if (!ok) { + LOGE("Could not start AOA thread"); + return false; + } + + return true; +} + +void +sc_aoa_stop(struct sc_aoa *aoa) { + sc_mutex_lock(&aoa->mutex); + aoa->stopped = true; + sc_cond_signal(&aoa->event_cond); + sc_mutex_unlock(&aoa->mutex); + + if (aoa->acksync) { + sc_acksync_interrupt(aoa->acksync); + } +} + +void +sc_aoa_join(struct sc_aoa *aoa) { + sc_thread_join(&aoa->thread, NULL); +} diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h new file mode 100644 index 00000000..33a1f136 --- /dev/null +++ b/app/src/usb/aoa_hid.h @@ -0,0 +1,72 @@ +#ifndef SC_AOA_HID_H +#define SC_AOA_HID_H + +#include +#include + +#include + +#include "hid/hid_event.h" +#include "usb.h" +#include "util/acksync.h" +#include "util/thread.h" +#include "util/tick.h" +#include "util/vecdeque.h" + +#define SC_HID_MAX_SIZE 8 + +struct sc_aoa_event { + struct sc_hid_event hid; + uint16_t accessory_id; + uint64_t ack_to_wait; +}; + +struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event); + +struct sc_aoa { + struct sc_usb *usb; + sc_thread thread; + sc_mutex mutex; + sc_cond event_cond; + bool stopped; + struct sc_aoa_event_queue queue; + + struct sc_acksync *acksync; +}; + +bool +sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *acksync); + +void +sc_aoa_destroy(struct sc_aoa *aoa); + +bool +sc_aoa_start(struct sc_aoa *aoa); + +void +sc_aoa_stop(struct sc_aoa *aoa); + +void +sc_aoa_join(struct sc_aoa *aoa); + +bool +sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, + const uint8_t *report_desc, uint16_t report_desc_size); + +bool +sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); + +bool +sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa, + uint16_t accessory_id, + const struct sc_hid_event *event, + uint64_t ack_to_wait); + +static inline bool +sc_aoa_push_hid_event(struct sc_aoa *aoa, uint16_t accessory_id, + const struct sc_hid_event *event) { + return sc_aoa_push_hid_event_with_ack_to_wait(aoa, accessory_id, event, + SC_SEQUENCE_INVALID); +} + +#endif diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c new file mode 100644 index 00000000..736c97b0 --- /dev/null +++ b/app/src/usb/keyboard_aoa.c @@ -0,0 +1,110 @@ +#include "keyboard_aoa.h" + +#include + +#include "input_events.h" +#include "util/log.h" + +/** Downcast key processor to keyboard_aoa */ +#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor) + +#define HID_KEYBOARD_ACCESSORY_ID 1 + +static bool +push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) { + struct sc_hid_event hid_event; + if (!sc_hid_keyboard_event_from_mods(&hid_event, mods_state)) { + // Nothing to do + return true; + } + + if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mod lock state)"); + return false; + } + + LOGD("HID keyboard state synchronized"); + + return true; +} + +static void +sc_key_processor_process_key(struct sc_key_processor *kp, + const struct sc_key_event *event, + uint64_t ack_to_wait) { + if (event->repeat) { + // In USB HID protocol, key repeat is handled by the host (Android), so + // just ignore key repeat here. + return; + } + + struct sc_keyboard_aoa *kb = DOWNCAST(kp); + + struct sc_hid_event hid_event; + + // Not all keys are supported, just ignore unsupported keys + if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) { + if (!kb->mod_lock_synchronized) { + // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize + // keyboard state + if (push_mod_lock_state(kb, event->mods_state)) { + kb->mod_lock_synchronized = true; + } + } + + // If ack_to_wait is != SC_SEQUENCE_INVALID, then Ctrl+v is pressed, so + // clipboard synchronization has been requested. Wait until clipboard + // synchronization is acknowledged by the server, otherwise it could + // paste the old clipboard content. + + if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa, + HID_KEYBOARD_ACCESSORY_ID, + &hid_event, + ack_to_wait)) { + LOGW("Could not request HID event (key)"); + } + } +} + +bool +sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { + kb->aoa = aoa; + + bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, + SC_HID_KEYBOARD_REPORT_DESC, + SC_HID_KEYBOARD_REPORT_DESC_LEN); + if (!ok) { + LOGW("Register HID keyboard failed"); + return false; + } + + sc_hid_keyboard_init(&kb->hid); + + kb->mod_lock_synchronized = false; + + static const struct sc_key_processor_ops ops = { + .process_key = sc_key_processor_process_key, + // Never forward text input via HID (all the keys are injected + // separately) + .process_text = NULL, + }; + + // Clipboard synchronization is requested over the control socket, while HID + // events are sent over AOA, so it must wait for clipboard synchronization + // to be acknowledged by the device before injecting Ctrl+v. + kb->key_processor.async_paste = true; + kb->key_processor.hid = true; + kb->key_processor.ops = &ops; + + return true; +} + +void +sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) { + // Unregister HID keyboard so the soft keyboard shows again on Android + bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); + if (!ok) { + LOGW("Could not unregister HID keyboard"); + } +} diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h new file mode 100644 index 00000000..565b9177 --- /dev/null +++ b/app/src/usb/keyboard_aoa.h @@ -0,0 +1,27 @@ +#ifndef SC_KEYBOARD_AOA_H +#define SC_KEYBOARD_AOA_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "hid/hid_keyboard.h" +#include "trait/key_processor.h" + +struct sc_keyboard_aoa { + struct sc_key_processor key_processor; // key processor trait + + struct sc_hid_keyboard hid; + struct sc_aoa *aoa; + + bool mod_lock_synchronized; +}; + +bool +sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa); + +void +sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb); + +#endif diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c new file mode 100644 index 00000000..93b32328 --- /dev/null +++ b/app/src/usb/mouse_aoa.c @@ -0,0 +1,89 @@ +#include "mouse_aoa.h" + +#include + +#include "hid/hid_mouse.h" +#include "input_events.h" +#include "util/log.h" + +/** Downcast mouse processor to mouse_aoa */ +#define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor) + +#define HID_MOUSE_ACCESSORY_ID 2 + +static void +sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, + const struct sc_mouse_motion_event *event) { + struct sc_mouse_aoa *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_motion(&hid_event, event); + + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mouse motion)"); + } +} + +static void +sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, + const struct sc_mouse_click_event *event) { + struct sc_mouse_aoa *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_click(&hid_event, event); + + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mouse click)"); + } +} + +static void +sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, + const struct sc_mouse_scroll_event *event) { + struct sc_mouse_aoa *mouse = DOWNCAST(mp); + + struct sc_hid_event hid_event; + sc_hid_mouse_event_from_scroll(&hid_event, event); + + if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID, + &hid_event)) { + LOGW("Could not request HID event (mouse scroll)"); + } +} + +bool +sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) { + mouse->aoa = aoa; + + bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, + SC_HID_MOUSE_REPORT_DESC, + SC_HID_MOUSE_REPORT_DESC_LEN); + if (!ok) { + LOGW("Register HID mouse failed"); + return false; + } + + static const struct sc_mouse_processor_ops ops = { + .process_mouse_motion = sc_mouse_processor_process_mouse_motion, + .process_mouse_click = sc_mouse_processor_process_mouse_click, + .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, + // Touch events not supported (coordinates are not relative) + .process_touch = NULL, + }; + + mouse->mouse_processor.ops = &ops; + + mouse->mouse_processor.relative_mode = true; + + return true; +} + +void +sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) { + bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); + if (!ok) { + LOGW("Could not unregister HID mouse"); + } +} diff --git a/app/src/usb/mouse_aoa.h b/app/src/usb/mouse_aoa.h new file mode 100644 index 00000000..afaed761 --- /dev/null +++ b/app/src/usb/mouse_aoa.h @@ -0,0 +1,23 @@ +#ifndef SC_MOUSE_AOA_H +#define SC_MOUSE_AOA_H + +#include "common.h" + +#include + +#include "aoa_hid.h" +#include "trait/mouse_processor.h" + +struct sc_mouse_aoa { + struct sc_mouse_processor mouse_processor; // mouse processor trait + + struct sc_aoa *aoa; +}; + +bool +sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa); + +void +sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse); + +#endif diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c new file mode 100644 index 00000000..c1d38da3 --- /dev/null +++ b/app/src/usb/scrcpy_otg.c @@ -0,0 +1,212 @@ +#include "scrcpy_otg.h" + +#include + +#include "adb/adb.h" +#include "events.h" +#include "screen_otg.h" +#include "util/log.h" + +struct scrcpy_otg { + struct sc_usb usb; + struct sc_aoa aoa; + struct sc_keyboard_aoa keyboard; + struct sc_mouse_aoa mouse; + + struct sc_screen_otg screen_otg; +}; + +static void +sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { + (void) usb; + (void) userdata; + + SDL_Event event; + event.type = SC_EVENT_USB_DEVICE_DISCONNECTED; + int ret = SDL_PushEvent(&event); + if (ret < 0) { + LOGE("Could not post USB disconnection event: %s", SDL_GetError()); + } +} + +static enum scrcpy_exit_code +event_loop(struct scrcpy_otg *s) { + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case SC_EVENT_USB_DEVICE_DISCONNECTED: + LOGW("Device disconnected"); + return SCRCPY_EXIT_DISCONNECTED; + case SDL_QUIT: + LOGD("User requested to quit"); + return SCRCPY_EXIT_SUCCESS; + default: + sc_screen_otg_handle_event(&s->screen_otg, &event); + break; + } + } + return SCRCPY_EXIT_FAILURE; +} + +enum scrcpy_exit_code +scrcpy_otg(struct scrcpy_options *options) { + static struct scrcpy_otg scrcpy_otg; + struct scrcpy_otg *s = &scrcpy_otg; + + const char *serial = options->serial; + + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + LOGW("Could not enable linear filtering"); + } + + // Minimal SDL initialization + if (SDL_Init(SDL_INIT_EVENTS)) { + LOGE("Could not initialize SDL: %s", SDL_GetError()); + return SCRCPY_EXIT_FAILURE; + } + + atexit(SDL_Quit); + + if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { + LOGW("Could not enable mouse focus clickthrough"); + } + + enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; + + struct sc_keyboard_aoa *keyboard = NULL; + struct sc_mouse_aoa *mouse = NULL; + bool usb_device_initialized = false; + bool usb_connected = false; + bool aoa_started = false; + bool aoa_initialized = false; + +#ifdef _WIN32 + // On Windows, only one process could open a USB device + // + LOGI("Killing adb server (if any)..."); + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + // uninterruptible (intr == NULL), but in practice it's very quick + sc_adb_kill_server(NULL, flags); +#endif + + static const struct sc_usb_callbacks cbs = { + .on_disconnected = sc_usb_on_disconnected, + }; + bool ok = sc_usb_init(&s->usb); + if (!ok) { + return SCRCPY_EXIT_FAILURE; + } + + struct sc_usb_device usb_device; + ok = sc_usb_select_device(&s->usb, serial, &usb_device); + if (!ok) { + goto end; + } + + usb_device_initialized = true; + + ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); + if (!ok) { + goto end; + } + usb_connected = true; + + ok = sc_aoa_init(&s->aoa, &s->usb, NULL); + if (!ok) { + goto end; + } + aoa_initialized = true; + + assert(options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA + || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED); + assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA + || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED); + + bool enable_keyboard = + options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA; + bool enable_mouse = + options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA; + + if (enable_keyboard) { + ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); + if (!ok) { + goto end; + } + keyboard = &s->keyboard; + } + + if (enable_mouse) { + ok = sc_mouse_aoa_init(&s->mouse, &s->aoa); + if (!ok) { + goto end; + } + mouse = &s->mouse; + } + + ok = sc_aoa_start(&s->aoa); + if (!ok) { + goto end; + } + aoa_started = true; + + const char *window_title = options->window_title; + if (!window_title) { + window_title = usb_device.product ? usb_device.product : "scrcpy"; + } + + struct sc_screen_otg_params params = { + .keyboard = keyboard, + .mouse = mouse, + .window_title = window_title, + .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, + }; + + ok = sc_screen_otg_init(&s->screen_otg, ¶ms); + if (!ok) { + goto end; + } + + // usb_device not needed anymore + sc_usb_device_destroy(&usb_device); + usb_device_initialized = false; + + ret = event_loop(s); + LOGD("quit..."); + +end: + if (aoa_started) { + sc_aoa_stop(&s->aoa); + } + sc_usb_stop(&s->usb); + + if (mouse) { + sc_mouse_aoa_destroy(&s->mouse); + } + if (keyboard) { + sc_keyboard_aoa_destroy(&s->keyboard); + } + + if (aoa_initialized) { + sc_aoa_join(&s->aoa); + sc_aoa_destroy(&s->aoa); + } + + sc_usb_join(&s->usb); + + if (usb_connected) { + sc_usb_disconnect(&s->usb); + } + + if (usb_device_initialized) { + sc_usb_device_destroy(&usb_device); + } + + sc_usb_destroy(&s->usb); + + return ret; +} diff --git a/app/src/usb/scrcpy_otg.h b/app/src/usb/scrcpy_otg.h new file mode 100644 index 00000000..e477660b --- /dev/null +++ b/app/src/usb/scrcpy_otg.h @@ -0,0 +1,12 @@ +#ifndef SCRCPY_OTG_H +#define SCRCPY_OTG_H + +#include "common.h" + +#include "options.h" +#include "scrcpy.h" + +enum scrcpy_exit_code +scrcpy_otg(struct scrcpy_options *options); + +#endif diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c new file mode 100644 index 00000000..e1d5cb01 --- /dev/null +++ b/app/src/usb/screen_otg.c @@ -0,0 +1,299 @@ +#include "screen_otg.h" + +#include "icon.h" +#include "options.h" +#include "util/log.h" + +static void +sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(screen->window, &x, &y); + SDL_GetWindowSize(screen->window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); + } + } +#else + (void) screen; +#endif + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + } +} + +static inline bool +sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) { + (void) screen; + return SDL_GetRelativeMouseMode(); +} + +static inline void +sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) { + (void) screen; + bool new_value = !sc_screen_otg_get_mouse_capture(screen); + sc_screen_otg_set_mouse_capture(screen, new_value); +} + +static void +sc_screen_otg_render(struct sc_screen_otg *screen) { + SDL_RenderClear(screen->renderer); + if (screen->texture) { + SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); + } + SDL_RenderPresent(screen->renderer); +} + +bool +sc_screen_otg_init(struct sc_screen_otg *screen, + const struct sc_screen_otg_params *params) { + screen->keyboard = params->keyboard; + screen->mouse = params->mouse; + + screen->mouse_capture_key_pressed = 0; + + const char *title = params->window_title; + assert(title); + + 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; + int width = params->window_width ? params->window_width : 256; + int height = params->window_height ? params->window_height : 256; + + uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; + if (params->always_on_top) { + window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; + } + if (params->window_borderless) { + window_flags |= SDL_WINDOW_BORDERLESS; + } + + screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags); + if (!screen->window) { + LOGE("Could not create window: %s", SDL_GetError()); + return false; + } + + screen->renderer = SDL_CreateRenderer(screen->window, -1, 0); + if (!screen->renderer) { + LOGE("Could not create renderer: %s", SDL_GetError()); + goto error_destroy_window; + } + + SDL_Surface *icon = scrcpy_icon_load(); + + if (icon) { + SDL_SetWindowIcon(screen->window, icon); + + if (SDL_RenderSetLogicalSize(screen->renderer, icon->w, icon->h)) { + LOGW("Could not set renderer logical size: %s", SDL_GetError()); + // don't fail + } + + screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon); + scrcpy_icon_destroy(icon); + if (!screen->texture) { + goto error_destroy_renderer; + } + } else { + screen->texture = NULL; + LOGW("Could not load icon"); + } + + if (screen->mouse) { + // Capture mouse on start + sc_screen_otg_set_mouse_capture(screen, true); + } + + return true; + +error_destroy_window: + SDL_DestroyWindow(screen->window); +error_destroy_renderer: + SDL_DestroyRenderer(screen->renderer); + + return false; +} + +void +sc_screen_otg_destroy(struct sc_screen_otg *screen) { + if (screen->texture) { + SDL_DestroyTexture(screen->texture); + } + SDL_DestroyRenderer(screen->renderer); + SDL_DestroyWindow(screen->window); +} + +static inline bool +sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + +static void +sc_screen_otg_process_key(struct sc_screen_otg *screen, + const SDL_KeyboardEvent *event) { + assert(screen->keyboard); + struct sc_key_processor *kp = &screen->keyboard->key_processor; + + struct sc_key_event evt = { + .action = sc_action_from_sdl_keyboard_type(event->type), + .keycode = sc_keycode_from_sdl(event->keysym.sym), + .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .repeat = event->repeat, + .mods_state = sc_mods_state_from_sdl(event->keysym.mod), + }; + + assert(kp->ops->process_key); + kp->ops->process_key(kp, &evt, SC_SEQUENCE_INVALID); +} + +static void +sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, + const SDL_MouseMotionEvent *event) { + assert(screen->mouse); + struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; + + struct sc_mouse_motion_event evt = { + // .position not used for HID events + .xrel = event->xrel, + .yrel = event->yrel, + .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true), + }; + + assert(mp->ops->process_mouse_motion); + mp->ops->process_mouse_motion(mp, &evt); +} + +static void +sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, + const SDL_MouseButtonEvent *event) { + assert(screen->mouse); + struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; + + uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + + struct sc_mouse_click_event evt = { + // .position not used for HID events + .action = sc_action_from_sdl_mousebutton_type(event->type), + .button = sc_mouse_button_from_sdl(event->button), + .buttons_state = + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + }; + + assert(mp->ops->process_mouse_click); + mp->ops->process_mouse_click(mp, &evt); +} + +static void +sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, + const SDL_MouseWheelEvent *event) { + assert(screen->mouse); + struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; + + uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); + + struct sc_mouse_scroll_event evt = { + // .position not used for HID events + .hscroll = event->x, + .vscroll = event->y, + .buttons_state = + sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), + }; + + assert(mp->ops->process_mouse_scroll); + mp->ops->process_mouse_scroll(mp, &evt); +} + +void +sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { + switch (event->type) { + case SDL_WINDOWEVENT: + switch (event->window.event) { + case SDL_WINDOWEVENT_EXPOSED: + sc_screen_otg_render(screen); + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if (screen->mouse) { + sc_screen_otg_set_mouse_capture(screen, false); + } + break; + } + return; + case SDL_KEYDOWN: + if (screen->mouse) { + SDL_Keycode key = event->key.keysym.sym; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (!screen->mouse_capture_key_pressed) { + screen->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + screen->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return; + } + } + + if (screen->keyboard) { + sc_screen_otg_process_key(screen, &event->key); + } + break; + case SDL_KEYUP: + if (screen->mouse) { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = screen->mouse_capture_key_pressed; + screen->mouse_capture_key_pressed = 0; + if (sc_screen_otg_is_mouse_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_screen_otg_toggle_mouse_capture(screen); + } + // Mouse capture keys are never forwarded to the device + return; + } + } + + if (screen->keyboard) { + sc_screen_otg_process_key(screen, &event->key); + } + break; + case SDL_MOUSEMOTION: + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + sc_screen_otg_process_mouse_motion(screen, &event->motion); + } + break; + case SDL_MOUSEBUTTONDOWN: + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + sc_screen_otg_process_mouse_button(screen, &event->button); + } + break; + case SDL_MOUSEBUTTONUP: + if (screen->mouse) { + if (sc_screen_otg_get_mouse_capture(screen)) { + sc_screen_otg_process_mouse_button(screen, &event->button); + } else { + sc_screen_otg_set_mouse_capture(screen, true); + } + } + break; + case SDL_MOUSEWHEEL: + if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + sc_screen_otg_process_mouse_wheel(screen, &event->wheel); + } + break; + } +} diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h new file mode 100644 index 00000000..c4e03b87 --- /dev/null +++ b/app/src/usb/screen_otg.h @@ -0,0 +1,47 @@ +#ifndef SC_SCREEN_OTG_H +#define SC_SCREEN_OTG_H + +#include "common.h" + +#include +#include + +#include "keyboard_aoa.h" +#include "mouse_aoa.h" + +struct sc_screen_otg { + struct sc_keyboard_aoa *keyboard; + struct sc_mouse_aoa *mouse; + + SDL_Window *window; + SDL_Renderer *renderer; + SDL_Texture *texture; + + // See equivalent mechanism in screen.h + SDL_Keycode mouse_capture_key_pressed; +}; + +struct sc_screen_otg_params { + struct sc_keyboard_aoa *keyboard; + struct sc_mouse_aoa *mouse; + + const char *window_title; + bool always_on_top; + int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_width; + uint16_t window_height; + bool window_borderless; +}; + +bool +sc_screen_otg_init(struct sc_screen_otg *screen, + const struct sc_screen_otg_params *params); + +void +sc_screen_otg_destroy(struct sc_screen_otg *screen); + +void +sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event); + +#endif diff --git a/app/src/usb/usb.c b/app/src/usb/usb.c new file mode 100644 index 00000000..4f750581 --- /dev/null +++ b/app/src/usb/usb.c @@ -0,0 +1,379 @@ +#include "usb.h" + +#include + +#include "util/log.h" +#include "util/vector.h" + +struct sc_vec_usb_devices SC_VECTOR(struct sc_usb_device); + +static char * +read_string(libusb_device_handle *handle, uint8_t desc_index) { + char buffer[128]; + int result = + libusb_get_string_descriptor_ascii(handle, desc_index, + (unsigned char *) buffer, + sizeof(buffer)); + if (result < 0) { + LOGD("Read string: libusb error: %s", libusb_strerror(result)); + return NULL; + } + + assert((size_t) result <= sizeof(buffer)); + + // When non-negative, 'result' contains the number of bytes written + char *s = malloc(result + 1); + if (!s) { + LOG_OOM(); + return NULL; + } + + memcpy(s, buffer, result); + s[result] = '\0'; + return s; +} + +static bool +sc_usb_read_device(libusb_device *device, struct sc_usb_device *out) { + // Do not log any USB error in this function, it is expected that many USB + // devices available on the computer have permission restrictions + + struct libusb_device_descriptor desc; + int result = libusb_get_device_descriptor(device, &desc); + if (result < 0 || !desc.iSerialNumber) { + return false; + } + + libusb_device_handle *handle; + result = libusb_open(device, &handle); + if (result < 0) { + // Log at debug level because it is expected that some non-Android USB + // devices present on the computer require special permissions + LOGD("Open USB device %04x:%04x: libusb error: %s", + (unsigned) desc.idVendor, (unsigned) desc.idProduct, + libusb_strerror(result)); + return false; + } + + char *device_serial = read_string(handle, desc.iSerialNumber); + if (!device_serial) { + libusb_close(handle); + return false; + } + + out->device = libusb_ref_device(device); + out->serial = device_serial; + out->vid = desc.idVendor; + out->pid = desc.idProduct; + out->manufacturer = read_string(handle, desc.iManufacturer); + out->product = read_string(handle, desc.iProduct); + out->selected = false; + + libusb_close(handle); + + return true; +} + +void +sc_usb_device_destroy(struct sc_usb_device *usb_device) { + if (usb_device->device) { + libusb_unref_device(usb_device->device); + } + free(usb_device->serial); + free(usb_device->manufacturer); + free(usb_device->product); +} + +void +sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) { + *dst = *src; + src->device = NULL; + src->serial = NULL; + src->manufacturer = NULL; + src->product = NULL; +} + +static void +sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) { + for (size_t i = 0; i < usb_devices->size; ++i) { + sc_usb_device_destroy(&usb_devices->data[i]); + } + sc_vector_destroy(usb_devices); +} + +static bool +sc_usb_list_devices(struct sc_usb *usb, struct sc_vec_usb_devices *out_vec) { + libusb_device **list; + ssize_t count = libusb_get_device_list(usb->context, &list); + if (count < 0) { + LOGE("List USB devices: libusb error: %s", libusb_strerror(count)); + return false; + } + + for (size_t i = 0; i < (size_t) count; ++i) { + libusb_device *device = list[i]; + + struct sc_usb_device usb_device; + if (sc_usb_read_device(device, &usb_device)) { + bool ok = sc_vector_push(out_vec, usb_device); + if (!ok) { + LOG_OOM(); + LOGE("Could not push usb_device to vector"); + sc_usb_device_destroy(&usb_device); + // continue anyway + } + } + } + + libusb_free_device_list(list, 1); + return true; +} + +static bool +sc_usb_accept_device(const struct sc_usb_device *device, const char *serial) { + if (!serial) { + return true; + } + + return !strcmp(serial, device->serial); +} + +static size_t +sc_usb_devices_select(struct sc_usb_device *devices, size_t len, + const char *serial, size_t *idx_out) { + size_t count = 0; + for (size_t i = 0; i < len; ++i) { + struct sc_usb_device *device = &devices[i]; + device->selected = sc_usb_accept_device(device, serial); + if (device->selected) { + if (idx_out && !count) { + *idx_out = i; + } + ++count; + } + } + + return count; +} + +static void +sc_usb_devices_log(enum sc_log_level level, struct sc_usb_device *devices, + size_t count) { + for (size_t i = 0; i < count; ++i) { + struct sc_usb_device *d = &devices[i]; + const char *selection = d->selected ? "-->" : " "; + // Convert uint16_t to unsigned because PRIx16 may not exist on Windows + LOG(level, " %s %-18s (%04x:%04x) %s %s", + selection, d->serial, (unsigned) d->vid, (unsigned) d->pid, + d->manufacturer, d->product); + } +} + +bool +sc_usb_select_device(struct sc_usb *usb, const char *serial, + struct sc_usb_device *out_device) { + struct sc_vec_usb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_usb_list_devices(usb, &vec); + if (!ok) { + LOGE("Could not list USB devices"); + return false; + } + + if (vec.size == 0) { + LOGE("Could not find any USB device"); + return false; + } + + size_t sel_idx; // index of the single matching device if sel_count == 1 + size_t sel_count = + sc_usb_devices_select(vec.data, vec.size, serial, &sel_idx); + + if (sel_count == 0) { + // if count > 0 && sel_count == 0, then necessarily a serial is provided + assert(serial); + LOGE("Could not find USB device %s", serial); + sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); + sc_usb_devices_destroy(&vec); + return false; + } + + if (sel_count > 1) { + if (serial) { + LOGE("Multiple (%" SC_PRIsizet ") USB devices with serial %s:", + sel_count, serial); + } else { + LOGE("Multiple (%" SC_PRIsizet ") USB devices:", sel_count); + } + sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); + LOGE("Select a device via -s (--serial)"); + sc_usb_devices_destroy(&vec); + return false; + } + + assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 + struct sc_usb_device *device = &vec.data[sel_idx]; + + LOGI("USB device found:"); + sc_usb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size); + + // Move device into out_device (do not destroy device) + sc_usb_device_move(out_device, device); + sc_usb_devices_destroy(&vec); + return true; +} + +bool +sc_usb_init(struct sc_usb *usb) { + usb->handle = NULL; + return libusb_init(&usb->context) == LIBUSB_SUCCESS; +} + +void +sc_usb_destroy(struct sc_usb *usb) { + libusb_exit(usb->context); +} + +static void +sc_usb_report_disconnected(struct sc_usb *usb) { + if (usb->cbs && !atomic_flag_test_and_set(&usb->disconnection_notified)) { + assert(usb->cbs && usb->cbs->on_disconnected); + usb->cbs->on_disconnected(usb, usb->cbs_userdata); + } +} + +bool +sc_usb_check_disconnected(struct sc_usb *usb, int result) { + if (result == LIBUSB_ERROR_NO_DEVICE || result == LIBUSB_ERROR_NOT_FOUND) { + sc_usb_report_disconnected(usb); + return false; + } + + return true; +} + +static LIBUSB_CALL int +sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, + libusb_hotplug_event event, void *userdata) { + (void) ctx; + (void) device; + (void) event; + + struct sc_usb *usb = userdata; + + libusb_device *dev = libusb_get_device(usb->handle); + assert(dev); + if (dev != device) { + // Not the connected device + return 0; + } + + sc_usb_report_disconnected(usb); + + // Do not automatically deregister the callback by returning 1. Instead, + // manually deregister to interrupt libusb_handle_events() from the libusb + // event thread: + return 0; +} + +static int +run_libusb_event_handler(void *data) { + struct sc_usb *usb = data; + while (!atomic_load(&usb->stopped)) { + // Interrupted by events or by libusb_hotplug_deregister_callback() + libusb_handle_events(usb->context); + } + return 0; +} + +static bool +sc_usb_register_callback(struct sc_usb *usb) { + if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { + LOGW("On this platform, libusb does not have hotplug capability; " + "device disconnection will not be detected properly"); + return false; + } + + libusb_device *device = libusb_get_device(usb->handle); + assert(device); + + struct libusb_device_descriptor desc; + int result = libusb_get_device_descriptor(device, &desc); + if (result < 0) { + LOGE("Device descriptor: libusb error: %s", libusb_strerror(result)); + return false; + } + + int events = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; + int flags = LIBUSB_HOTPLUG_NO_FLAGS; + int vendor_id = desc.idVendor; + int product_id = desc.idProduct; + int dev_class = LIBUSB_HOTPLUG_MATCH_ANY; + result = libusb_hotplug_register_callback(usb->context, events, flags, + vendor_id, product_id, dev_class, + sc_usb_libusb_callback, usb, + &usb->callback_handle); + if (result < 0) { + LOGE("Register hotplog callback: libusb error: %s", + libusb_strerror(result)); + return false; + } + + usb->has_callback_handle = true; + return true; +} + +bool +sc_usb_connect(struct sc_usb *usb, libusb_device *device, + const struct sc_usb_callbacks *cbs, void *cbs_userdata) { + int result = libusb_open(device, &usb->handle); + if (result < 0) { + LOGE("Open USB device: libusb error: %s", libusb_strerror(result)); + return false; + } + + usb->has_callback_handle = false; + usb->has_libusb_event_thread = false; + + // If cbs is set, then cbs->on_disconnected must be set + assert(!cbs || cbs->on_disconnected); + usb->cbs = cbs; + usb->cbs_userdata = cbs_userdata; + + if (cbs) { + atomic_init(&usb->stopped, false); + usb->disconnection_notified = (atomic_flag) ATOMIC_FLAG_INIT; + if (sc_usb_register_callback(usb)) { + // Create a thread to process libusb events, so that device + // disconnection could be detected immediately + usb->has_libusb_event_thread = + sc_thread_create(&usb->libusb_event_thread, + run_libusb_event_handler, "scrcpy-usbev", usb); + if (!usb->has_libusb_event_thread) { + LOGW("Libusb event thread handler could not be created, USB " + "device disconnection might not be detected immediately"); + } + } + } + + return true; +} + +void +sc_usb_disconnect(struct sc_usb *usb) { + libusb_close(usb->handle); +} + +void +sc_usb_stop(struct sc_usb *usb) { + if (usb->has_callback_handle) { + atomic_store(&usb->stopped, true); + libusb_hotplug_deregister_callback(usb->context, usb->callback_handle); + } +} + +void +sc_usb_join(struct sc_usb *usb) { + if (usb->has_libusb_event_thread) { + sc_thread_join(&usb->libusb_event_thread, NULL); + } +} diff --git a/app/src/usb/usb.h b/app/src/usb/usb.h new file mode 100644 index 00000000..f0ebbd96 --- /dev/null +++ b/app/src/usb/usb.h @@ -0,0 +1,88 @@ +#ifndef SC_USB_H +#define SC_USB_H + +#include "common.h" + +#include +#include + +#include "util/thread.h" + +struct sc_usb { + libusb_context *context; + libusb_device_handle *handle; + + const struct sc_usb_callbacks *cbs; + void *cbs_userdata; + + bool has_callback_handle; + libusb_hotplug_callback_handle callback_handle; + + bool has_libusb_event_thread; + sc_thread libusb_event_thread; + + atomic_bool stopped; // only used if cbs != NULL + atomic_flag disconnection_notified; +}; + +struct sc_usb_callbacks { + void (*on_disconnected)(struct sc_usb *usb, void *userdata); +}; + +struct sc_usb_device { + libusb_device *device; + char *serial; + char *manufacturer; + char *product; + uint16_t vid; + uint16_t pid; + bool selected; +}; + +void +sc_usb_device_destroy(struct sc_usb_device *usb_device); + +/** + * Move src to dst + * + * After this call, the content of src is undefined, except that + * sc_usb_device_destroy() can be called. + * + * This is useful to take a device from a list that will be destroyed, without + * making unnecessary copies. + */ +void +sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src); + +void +sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count); + +bool +sc_usb_init(struct sc_usb *usb); + +void +sc_usb_destroy(struct sc_usb *usb); + +bool +sc_usb_select_device(struct sc_usb *usb, const char *serial, + struct sc_usb_device *out_device); + +bool +sc_usb_connect(struct sc_usb *usb, libusb_device *device, + const struct sc_usb_callbacks *cbs, void *cbs_userdata); + +void +sc_usb_disconnect(struct sc_usb *usb); + +// A client should call this function with the return value of a libusb call +// to detect disconnection immediately +bool +sc_usb_check_disconnected(struct sc_usb *usb, int result); + +void +sc_usb_stop(struct sc_usb *usb); + +void +sc_usb_join(struct sc_usb *usb); + +#endif diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c new file mode 100644 index 00000000..2899cdcb --- /dev/null +++ b/app/src/util/acksync.c @@ -0,0 +1,76 @@ +#include "acksync.h" + +#include +#include "util/log.h" + +bool +sc_acksync_init(struct sc_acksync *as) { + bool ok = sc_mutex_init(&as->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&as->cond); + if (!ok) { + sc_mutex_destroy(&as->mutex); + return false; + } + + as->stopped = false; + as->ack = SC_SEQUENCE_INVALID; + + return true; +} + +void +sc_acksync_destroy(struct sc_acksync *as) { + sc_cond_destroy(&as->cond); + sc_mutex_destroy(&as->mutex); +} + +void +sc_acksync_ack(struct sc_acksync *as, uint64_t sequence) { + sc_mutex_lock(&as->mutex); + + // Acknowledgements must be monotonic + assert(sequence >= as->ack); + + as->ack = sequence; + sc_cond_signal(&as->cond); + + sc_mutex_unlock(&as->mutex); +} + +enum sc_acksync_wait_result +sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline) { + sc_mutex_lock(&as->mutex); + + bool timed_out = false; + while (!as->stopped && as->ack < ack && !timed_out) { + timed_out = !sc_cond_timedwait(&as->cond, &as->mutex, deadline); + } + + enum sc_acksync_wait_result ret; + if (as->stopped) { + ret = SC_ACKSYNC_WAIT_INTR; + } else if (as->ack >= ack) { + ret = SC_ACKSYNC_WAIT_OK; + } else { + assert(timed_out); + ret = SC_ACKSYNC_WAIT_TIMEOUT; + } + sc_mutex_unlock(&as->mutex); + + return ret; +} + +/** + * Interrupt any `sc_acksync_wait()` + */ +void +sc_acksync_interrupt(struct sc_acksync *as) { + sc_mutex_lock(&as->mutex); + as->stopped = true; + sc_cond_signal(&as->cond); + sc_mutex_unlock(&as->mutex); +} diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h new file mode 100644 index 00000000..58ab1b35 --- /dev/null +++ b/app/src/util/acksync.h @@ -0,0 +1,66 @@ +#ifndef SC_ACK_SYNC_H +#define SC_ACK_SYNC_H + +#include "common.h" + +#include "thread.h" + +#define SC_SEQUENCE_INVALID 0 + +/** + * Helper to wait for acknowledgments + * + * In practice, it is used to wait for device clipboard acknowledgement from the + * server before injecting Ctrl+v via AOA HID, in order to avoid pasting the + * content of the old device clipboard (if Ctrl+v was injected before the + * clipboard content was actually set). + */ +struct sc_acksync { + sc_mutex mutex; + sc_cond cond; + + bool stopped; + + // Last acked value, initially SC_SEQUENCE_INVALID + uint64_t ack; +}; + +enum sc_acksync_wait_result { + // Acknowledgment received + SC_ACKSYNC_WAIT_OK, + + // Timeout expired + SC_ACKSYNC_WAIT_TIMEOUT, + + // Interrupted from another thread by sc_acksync_interrupt() + SC_ACKSYNC_WAIT_INTR, +}; + +bool +sc_acksync_init(struct sc_acksync *as); + +void +sc_acksync_destroy(struct sc_acksync *as); + +/** + * Acknowledge `sequence` + * + * The `sequence` must be greater than (or equal to) any previous acknowledged + * sequence. + */ +void +sc_acksync_ack(struct sc_acksync *as, uint64_t sequence); + +/** + * Wait for acknowledgment of sequence `ack` (or higher) + */ +enum sc_acksync_wait_result +sc_acksync_wait(struct sc_acksync *as, uint64_t ack, sc_tick deadline); + +/** + * Interrupt any `sc_acksync_wait()` + */ +void +sc_acksync_interrupt(struct sc_acksync *as); + +#endif diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c new file mode 100644 index 00000000..3597f7ee --- /dev/null +++ b/app/src/util/audiobuf.c @@ -0,0 +1,112 @@ +#include "audiobuf.h" + +#include +#include +#include +#include + +bool +sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, + uint32_t capacity) { + assert(sample_size); + assert(capacity); + + // The actual capacity is (alloc_size - 1) so that head == tail is + // non-ambiguous + buf->alloc_size = capacity + 1; + buf->data = sc_allocarray(buf->alloc_size, sample_size); + if (!buf->data) { + LOG_OOM(); + return false; + } + + buf->sample_size = sample_size; + atomic_init(&buf->head, 0); + atomic_init(&buf->tail, 0); + + return true; +} + +void +sc_audiobuf_destroy(struct sc_audiobuf *buf) { + free(buf->data); +} + +uint32_t +sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) { + assert(samples_count); + + uint8_t *to = to_; + + // Only the reader thread can write tail without synchronization, so + // memory_order_relaxed is sufficient + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed); + + // The head cursor is updated after the data is written to the array + uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); + + uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size; + if (samples_count > can_read) { + samples_count = can_read; + } + + if (to) { + uint32_t right_count = buf->alloc_size - tail; + if (right_count > samples_count) { + right_count = samples_count; + } + memcpy(to, + buf->data + (tail * buf->sample_size), + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memcpy(to + (right_count * buf->sample_size), + buf->data, + left_count * buf->sample_size); + } + } + + uint32_t new_tail = (tail + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->tail, new_tail, memory_order_release); + + return samples_count; +} + +uint32_t +sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, + uint32_t samples_count) { + const uint8_t *from = from_; + + // Only the writer thread can write head, so memory_order_relaxed is + // sufficient + uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); + + // The tail cursor is updated after the data is consumed by the reader + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + + uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (samples_count > can_write) { + samples_count = can_write; + } + + uint32_t right_count = buf->alloc_size - head; + if (right_count > samples_count) { + right_count = samples_count; + } + memcpy(buf->data + (head * buf->sample_size), + from, + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memcpy(buf->data, + from + (right_count * buf->sample_size), + left_count * buf->sample_size); + } + + uint32_t new_head = (head + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->head, new_head, memory_order_release); + + return samples_count; +} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h new file mode 100644 index 00000000..5e7dd4a0 --- /dev/null +++ b/app/src/util/audiobuf.h @@ -0,0 +1,65 @@ +#ifndef SC_AUDIOBUF_H +#define SC_AUDIOBUF_H + +#include "common.h" + +#include +#include +#include +#include + +/** + * Wrapper around bytebuf to read and write samples + * + * Each sample takes sample_size bytes. + */ +struct sc_audiobuf { + uint8_t *data; + uint32_t alloc_size; // in samples + size_t sample_size; + + atomic_uint_least32_t head; // writer cursor, in samples + atomic_uint_least32_t tail; // reader cursor, in samples + // empty: tail == head + // full: ((tail + 1) % alloc_size) == head +}; + +static inline uint32_t +sc_audiobuf_to_samples(struct sc_audiobuf *buf, size_t bytes) { + assert(bytes % buf->sample_size == 0); + return bytes / buf->sample_size; +} + +static inline size_t +sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) { + return samples * buf->sample_size; +} + +bool +sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, + uint32_t capacity); + +void +sc_audiobuf_destroy(struct sc_audiobuf *buf); + +uint32_t +sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count); + +uint32_t +sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, + uint32_t samples_count); + +static inline uint32_t +sc_audiobuf_capacity(struct sc_audiobuf *buf) { + assert(buf->alloc_size); + return buf->alloc_size - 1; +} + +static inline uint32_t +sc_audiobuf_can_read(struct sc_audiobuf *buf) { + uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire); + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + return (buf->alloc_size + head - tail) % buf->alloc_size; +} + +#endif diff --git a/app/src/util/average.c b/app/src/util/average.c new file mode 100644 index 00000000..ace23d45 --- /dev/null +++ b/app/src/util/average.c @@ -0,0 +1,26 @@ +#include "average.h" + +#include + +void +sc_average_init(struct sc_average *avg, unsigned range) { + avg->range = range; + avg->avg = 0; + avg->count = 0; +} + +void +sc_average_push(struct sc_average *avg, float value) { + if (avg->count < avg->range) { + ++avg->count; + } + + assert(avg->count); + avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count; +} + +float +sc_average_get(struct sc_average *avg) { + assert(avg->count); + return avg->avg; +} diff --git a/app/src/util/average.h b/app/src/util/average.h new file mode 100644 index 00000000..59fae7d1 --- /dev/null +++ b/app/src/util/average.h @@ -0,0 +1,40 @@ +#ifndef SC_AVERAGE +#define SC_AVERAGE + +#include "common.h" + +#include +#include + +struct sc_average { + // Current average value + float avg; + + // Target range, to update the average as follow: + // avg = ((range - 1) * avg + new_value) / range + unsigned range; + + // Number of values pushed when less than range (count <= range). + // The purpose is to handle the first (range - 1) values properly. + unsigned count; +}; + +void +sc_average_init(struct sc_average *avg, unsigned range); + +/** + * Push a new value to update the "rolling" average + */ +void +sc_average_push(struct sc_average *avg, float value); + +/** + * Get the current average value + * + * It is an error to call this function if sc_average_push() has not been + * called at least once. + */ +float +sc_average_get(struct sc_average *avg); + +#endif diff --git a/app/src/util/binary.h b/app/src/util/binary.h new file mode 100644 index 00000000..6dc1b58e --- /dev/null +++ b/app/src/util/binary.h @@ -0,0 +1,76 @@ +#ifndef SC_BINARY_H +#define SC_BINARY_H + +#include "common.h" + +#include +#include +#include + +static inline void +sc_write16be(uint8_t *buf, uint16_t value) { + buf[0] = value >> 8; + buf[1] = value; +} + +static inline void +sc_write32be(uint8_t *buf, uint32_t value) { + buf[0] = value >> 24; + buf[1] = value >> 16; + buf[2] = value >> 8; + buf[3] = value; +} + +static inline void +sc_write64be(uint8_t *buf, uint64_t value) { + sc_write32be(buf, value >> 32); + sc_write32be(&buf[4], (uint32_t) value); +} + +static inline uint16_t +sc_read16be(const uint8_t *buf) { + return (buf[0] << 8) | buf[1]; +} + +static inline uint32_t +sc_read32be(const uint8_t *buf) { + return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; +} + +static inline uint64_t +sc_read64be(const uint8_t *buf) { + uint32_t msb = sc_read32be(buf); + uint32_t lsb = sc_read32be(&buf[4]); + return ((uint64_t) msb << 32) | lsb; +} + +/** + * Convert a float between 0 and 1 to an unsigned 16-bit fixed-point value + */ +static inline uint16_t +sc_float_to_u16fp(float f) { + assert(f >= 0.0f && f <= 1.0f); + uint32_t u = f * 0x1p16f; // 2^16 + if (u >= 0xffff) { + assert(u == 0x10000); // for f == 1.0f + u = 0xffff; + } + return (uint16_t) u; +} + +/** + * Convert a float between -1 and 1 to a signed 16-bit fixed-point value + */ +static inline int16_t +sc_float_to_i16fp(float f) { + assert(f >= -1.0f && f <= 1.0f); + int32_t i = f * 0x1p15f; // 2^15 + assert(i >= -0x8000); + if (i >= 0x7fff) { + assert(i == 0x8000); // for f == 1.0f + i = 0x7fff; + } + return (int16_t) i; +} + +#endif diff --git a/app/src/util/buffer_util.h b/app/src/util/buffer_util.h deleted file mode 100644 index 337bb262..00000000 --- a/app/src/util/buffer_util.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef BUFFER_UTIL_H -#define BUFFER_UTIL_H - -#include "common.h" - -#include -#include - -static inline void -buffer_write16be(uint8_t *buf, uint16_t value) { - buf[0] = value >> 8; - buf[1] = value; -} - -static inline void -buffer_write32be(uint8_t *buf, uint32_t value) { - buf[0] = value >> 24; - buf[1] = value >> 16; - buf[2] = value >> 8; - buf[3] = value; -} - -static inline void -buffer_write64be(uint8_t *buf, uint64_t value) { - buffer_write32be(buf, value >> 32); - buffer_write32be(&buf[4], (uint32_t) value); -} - -static inline uint16_t -buffer_read16be(const uint8_t *buf) { - return (buf[0] << 8) | buf[1]; -} - -static inline uint32_t -buffer_read32be(const uint8_t *buf) { - return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; -} - -static inline uint64_t -buffer_read64be(const uint8_t *buf) { - uint32_t msb = buffer_read32be(buf); - uint32_t lsb = buffer_read32be(&buf[4]); - return ((uint64_t) msb << 32) | lsb; -} - -#endif diff --git a/app/src/util/cbuf.h b/app/src/util/cbuf.h deleted file mode 100644 index 01e41044..00000000 --- a/app/src/util/cbuf.h +++ /dev/null @@ -1,52 +0,0 @@ -// generic circular buffer (bounded queue) implementation -#ifndef CBUF_H -#define CBUF_H - -#include "common.h" - -#include -#include - -// To define a circular buffer type of 20 ints: -// struct cbuf_int CBUF(int, 20); -// -// data has length CAP + 1 to distinguish empty vs full. -#define CBUF(TYPE, CAP) { \ - TYPE data[(CAP) + 1]; \ - size_t head; \ - size_t tail; \ -} - -#define cbuf_size_(PCBUF) \ - (sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data)) - -#define cbuf_is_empty(PCBUF) \ - ((PCBUF)->head == (PCBUF)->tail) - -#define cbuf_is_full(PCBUF) \ - (((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail) - -#define cbuf_init(PCBUF) \ - (void) ((PCBUF)->head = (PCBUF)->tail = 0) - -#define cbuf_push(PCBUF, ITEM) \ - ({ \ - bool ok = !cbuf_is_full(PCBUF); \ - if (ok) { \ - (PCBUF)->data[(PCBUF)->head] = (ITEM); \ - (PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \ - } \ - ok; \ - }) - -#define cbuf_take(PCBUF, PITEM) \ - ({ \ - bool ok = !cbuf_is_empty(PCBUF); \ - if (ok) { \ - *(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \ - (PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \ - } \ - ok; \ - }) - -#endif diff --git a/app/src/util/file.c b/app/src/util/file.c new file mode 100644 index 00000000..174e5efd --- /dev/null +++ b/app/src/util/file.c @@ -0,0 +1,48 @@ +#include "file.h" + +#include +#include + +#include "util/log.h" + +char * +sc_file_get_local_path(const char *name) { + char *executable_path = sc_file_get_executable_path(); + if (!executable_path) { + return NULL; + } + + // dirname() does not work correctly everywhere, so get the parent + // directory manually. + // See + char *p = strrchr(executable_path, SC_PATH_SEPARATOR); + if (!p) { + LOGE("Unexpected executable path: \"%s\" (it should contain a '%c')", + executable_path, SC_PATH_SEPARATOR); + free(executable_path); + return NULL; + } + + *p = '\0'; // modify executable_path in place + char *dir = executable_path; + size_t dirlen = strlen(dir); + size_t namelen = strlen(name); + + size_t len = dirlen + namelen + 2; // +2: '/' and '\0' + char *file_path = malloc(len); + if (!file_path) { + LOG_OOM(); + free(executable_path); + return NULL; + } + + memcpy(file_path, dir, dirlen); + file_path[dirlen] = SC_PATH_SEPARATOR; + // namelen + 1 to copy the final '\0' + memcpy(&file_path[dirlen + 1], name, namelen + 1); + + free(executable_path); + + return file_path; +} + diff --git a/app/src/util/file.h b/app/src/util/file.h new file mode 100644 index 00000000..089f6f75 --- /dev/null +++ b/app/src/util/file.h @@ -0,0 +1,49 @@ +#ifndef SC_FILE_H +#define SC_FILE_H + +#include "common.h" + +#include + +#ifdef _WIN32 +# define SC_PATH_SEPARATOR '\\' +#else +# define SC_PATH_SEPARATOR '/' +#endif + +#ifndef _WIN32 +/** + * Indicate if an executable exists using $PATH + * + * In practice, it is only used to know if a package manager is available on + * the system. It is only implemented on Linux. + */ +bool +sc_file_executable_exists(const char *file); +#endif + +/** + * Return the absolute path of the executable (the scrcpy binary) + * + * The result must be freed by the caller using free(). It may return NULL on + * error. + */ +char * +sc_file_get_executable_path(void); + +/** + * Return the absolute path of a file in the same directory as the executable + * + * The result must be freed by the caller using free(). It may return NULL on + * error. + */ +char * +sc_file_get_local_path(const char *name); + +/** + * Indicate if the file exists and is not a directory + */ +bool +sc_file_is_regular(const char *path); + +#endif diff --git a/app/src/util/intmap.c b/app/src/util/intmap.c new file mode 100644 index 00000000..fa11acef --- /dev/null +++ b/app/src/util/intmap.c @@ -0,0 +1,13 @@ +#include "intmap.h" + +const struct sc_intmap_entry * +sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len, + int32_t key) { + for (size_t i = 0; i < len; ++i) { + const struct sc_intmap_entry *entry = &entries[i]; + if (entry->key == key) { + return entry; + } + } + return NULL; +} diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h new file mode 100644 index 00000000..2898c461 --- /dev/null +++ b/app/src/util/intmap.h @@ -0,0 +1,24 @@ +#ifndef SC_ARRAYMAP_H +#define SC_ARRAYMAP_H + +#include "common.h" + +#include + +struct sc_intmap_entry { + int32_t key; + int32_t value; +}; + +const struct sc_intmap_entry * +sc_intmap_find_entry(const struct sc_intmap_entry entries[], size_t len, + int32_t key); + +/** + * MAP is expected to be a static array of sc_intmap_entry, so that + * ARRAY_LEN(MAP) can be computed statically. + */ +#define SC_INTMAP_FIND_ENTRY(MAP, KEY) \ + sc_intmap_find_entry(MAP, ARRAY_LEN(MAP), KEY) + +#endif diff --git a/app/src/util/intr.c b/app/src/util/intr.c new file mode 100644 index 00000000..22bd121a --- /dev/null +++ b/app/src/util/intr.c @@ -0,0 +1,83 @@ +#include "intr.h" + +#include "util/log.h" + +#include + +bool +sc_intr_init(struct sc_intr *intr) { + bool ok = sc_mutex_init(&intr->mutex); + if (!ok) { + LOG_OOM(); + return false; + } + + intr->socket = SC_SOCKET_NONE; + intr->process = SC_PROCESS_NONE; + + atomic_store_explicit(&intr->interrupted, false, memory_order_relaxed); + + return true; +} + +bool +sc_intr_set_socket(struct sc_intr *intr, sc_socket socket) { + assert(intr->process == SC_PROCESS_NONE); + + sc_mutex_lock(&intr->mutex); + bool interrupted = + atomic_load_explicit(&intr->interrupted, memory_order_relaxed); + if (!interrupted) { + intr->socket = socket; + } + sc_mutex_unlock(&intr->mutex); + + return !interrupted; +} + +bool +sc_intr_set_process(struct sc_intr *intr, sc_pid pid) { + assert(intr->socket == SC_SOCKET_NONE); + + sc_mutex_lock(&intr->mutex); + bool interrupted = + atomic_load_explicit(&intr->interrupted, memory_order_relaxed); + if (!interrupted) { + intr->process = pid; + } + sc_mutex_unlock(&intr->mutex); + + return !interrupted; +} + +void +sc_intr_interrupt(struct sc_intr *intr) { + sc_mutex_lock(&intr->mutex); + + atomic_store_explicit(&intr->interrupted, true, memory_order_relaxed); + + // No more than one component to interrupt + assert(intr->socket == SC_SOCKET_NONE || + intr->process == SC_PROCESS_NONE); + + if (intr->socket != SC_SOCKET_NONE) { + LOGD("Interrupting socket"); + net_interrupt(intr->socket); + intr->socket = SC_SOCKET_NONE; + } + if (intr->process != SC_PROCESS_NONE) { + LOGD("Interrupting process"); + sc_process_terminate(intr->process); + intr->process = SC_PROCESS_NONE; + } + + sc_mutex_unlock(&intr->mutex); +} + +void +sc_intr_destroy(struct sc_intr *intr) { + assert(intr->socket == SC_SOCKET_NONE); + assert(intr->process == SC_PROCESS_NONE); + + sc_mutex_destroy(&intr->mutex); +} diff --git a/app/src/util/intr.h b/app/src/util/intr.h new file mode 100644 index 00000000..1c20f6df --- /dev/null +++ b/app/src/util/intr.h @@ -0,0 +1,78 @@ +#ifndef SC_INTR_H +#define SC_INTR_H + +#include "common.h" + +#include +#include + +#include "net.h" +#include "process.h" +#include "thread.h" + +/** + * Interruptor to wake up a blocking call from another thread + * + * It allows to register a socket or a process before a blocking call, and + * interrupt/close from another thread to wake up the blocking call. + */ +struct sc_intr { + sc_mutex mutex; + + sc_socket socket; + sc_pid process; + + // Written protected by the mutex to avoid race conditions against + // sc_intr_set_socket() and sc_intr_set_process(), but can be read + // (atomically) without mutex + atomic_bool interrupted; +}; + +/** + * Initialize an interruptor + */ +bool +sc_intr_init(struct sc_intr *intr); + +/** + * Set a socket as the interruptible component + * + * Call with SC_SOCKET_NONE to unset. + */ +bool +sc_intr_set_socket(struct sc_intr *intr, sc_socket socket); + +/** + * Set a process as the interruptible component + * + * Call with SC_PROCESS_NONE to unset. + */ +bool +sc_intr_set_process(struct sc_intr *intr, sc_pid socket); + +/** + * Interrupt the current interruptible component + * + * Must be called from a different thread. + */ +void +sc_intr_interrupt(struct sc_intr *intr); + +/** + * Read the interrupted state + * + * It is exposed as a static inline function because it just loads from an + * atomic. + */ +static inline bool +sc_intr_is_interrupted(struct sc_intr *intr) { + return atomic_load_explicit(&intr->interrupted, memory_order_relaxed); +} + +/** + * Destroy the interruptor + */ +void +sc_intr_destroy(struct sc_intr *intr); + +#endif diff --git a/app/src/util/log.c b/app/src/util/log.c index a285fffb..8a347c84 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -1,6 +1,10 @@ #include "log.h" +#if _WIN32 +# include +#endif #include +#include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { @@ -44,6 +48,7 @@ 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); + SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log); } enum sc_log_level @@ -51,3 +56,99 @@ sc_get_log_level(void) { SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION); return log_level_sdl_to_sc(sdl_log); } + +void +sc_log(enum sc_log_level level, const char *fmt, ...) { + SDL_LogPriority sdl_level = log_level_sc_to_sdl(level); + + va_list ap; + va_start(ap, fmt); + SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, sdl_level, fmt, ap); + va_end(ap); +} + +#ifdef _WIN32 +bool +sc_log_windows_error(const char *prefix, int error) { + assert(prefix); + + char *message; + DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM; + DWORD lang_id = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + int ret = + FormatMessage(flags, NULL, error, lang_id, (char *) &message, 0, NULL); + if (ret <= 0) { + return false; + } + + // Note: message already contains a trailing '\n' + LOGE("%s: [%d] %s", prefix, error, message); + LocalFree(message); + return true; +} +#endif + +static SDL_LogPriority +sdl_priority_from_av_level(int level) { + switch (level) { + case AV_LOG_PANIC: + case AV_LOG_FATAL: + return SDL_LOG_PRIORITY_CRITICAL; + case AV_LOG_ERROR: + return SDL_LOG_PRIORITY_ERROR; + case AV_LOG_WARNING: + return SDL_LOG_PRIORITY_WARN; + case AV_LOG_INFO: + return SDL_LOG_PRIORITY_INFO; + } + // do not forward others, which are too verbose + return 0; +} + +static void +sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) { + (void) avcl; + SDL_LogPriority priority = sdl_priority_from_av_level(level); + if (priority == 0) { + return; + } + + size_t fmt_len = strlen(fmt); + char *local_fmt = malloc(fmt_len + 10); + if (!local_fmt) { + LOG_OOM(); + return; + } + 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_CUSTOM, priority, local_fmt, vl); + free(local_fmt); +} + +static const char *const sc_sdl_log_priority_names[SDL_NUM_LOG_PRIORITIES] = { + [SDL_LOG_PRIORITY_VERBOSE] = "VERBOSE", + [SDL_LOG_PRIORITY_DEBUG] = "DEBUG", + [SDL_LOG_PRIORITY_INFO] = "INFO", + [SDL_LOG_PRIORITY_WARN] = "WARN", + [SDL_LOG_PRIORITY_ERROR] = "ERROR", + [SDL_LOG_PRIORITY_CRITICAL] = "CRITICAL", +}; + +static void SDLCALL +sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority, + const char *message) { + (void) userdata; + (void) category; + + FILE *out = priority < SDL_LOG_PRIORITY_WARN ? stdout : stderr; + assert(priority < SDL_NUM_LOG_PRIORITIES); + const char *prio_name = sc_sdl_log_priority_names[priority]; + fprintf(out, "%s: %s\n", prio_name, message); +} + +void +sc_log_configure(void) { + SDL_LogSetOutputFunction(sc_sdl_log_print, NULL); + // Redirect FFmpeg logs to SDL logs + av_log_set_callback(sc_av_log_callback); +} diff --git a/app/src/util/log.h b/app/src/util/log.h index 30934b5c..0d79c9a4 100644 --- a/app/src/util/log.h +++ b/app/src/util/log.h @@ -5,14 +5,19 @@ #include -#include "scrcpy.h" +#include "options.h" + +#define LOG_STR_IMPL_(x) # x +#define LOG_STR(x) LOG_STR_IMPL_(x) #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__) #define LOGW(...) SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) #define LOGE(...) SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) -#define LOGC(...) SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__) + +#define LOG_OOM() \ + LOGE("OOM: %s:%d %s()", __FILE__, __LINE__, __func__) void sc_set_log_level(enum sc_log_level level); @@ -20,4 +25,17 @@ sc_set_log_level(enum sc_log_level level); enum sc_log_level sc_get_log_level(void); +void +sc_log(enum sc_log_level level, const char *fmt, ...); +#define LOG(LEVEL, ...) sc_log((LEVEL), __VA_ARGS__) + +#ifdef _WIN32 +// Log system error (typically returned by GetLastError() or similar) +bool +sc_log_windows_error(const char *prefix, int error); +#endif + +void +sc_log_configure(void); + #endif diff --git a/app/src/util/memory.c b/app/src/util/memory.c new file mode 100644 index 00000000..64ee616e --- /dev/null +++ b/app/src/util/memory.c @@ -0,0 +1,14 @@ +#include "memory.h" + +#include +#include + +void * +sc_allocarray(size_t nmemb, size_t size) { + size_t bytes; + if (__builtin_mul_overflow(nmemb, size, &bytes)) { + errno = ENOMEM; + return NULL; + } + return malloc(bytes); +} diff --git a/app/src/util/memory.h b/app/src/util/memory.h new file mode 100644 index 00000000..0fb6bc64 --- /dev/null +++ b/app/src/util/memory.h @@ -0,0 +1,15 @@ +#ifndef SC_MEMORY_H +#define SC_MEMORY_H + +#include + +/** + * Allocate an array of `nmemb` items of `size` bytes each + * + * Like calloc(), but without initialization. + * Like reallocarray(), but without reallocation. + */ +void * +sc_allocarray(size_t nmemb, size_t size); + +#endif diff --git a/app/src/util/net.c b/app/src/util/net.c index 17299424..67317ead 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,72 +1,169 @@ #include "net.h" +#include +#include #include -#include #include "log.h" -#ifdef __WINDOWS__ +#ifdef _WIN32 +# include typedef int socklen_t; + typedef SOCKET sc_raw_socket; +# define SC_RAW_SOCKET_NONE INVALID_SOCKET #else # include # include # include # include # include +# include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; + typedef int sc_raw_socket; +# define SC_RAW_SOCKET_NONE -1 +#endif + +bool +net_init(void) { +#ifdef _WIN32 + WSADATA wsa; + int res = WSAStartup(MAKEWORD(1, 1), &wsa); + if (res) { + LOGE("WSAStartup failed with error %d", res); + return false; + } +#endif + return true; +} + +void +net_cleanup(void) { +#ifdef _WIN32 + WSACleanup(); +#endif +} + +static inline sc_socket +wrap(sc_raw_socket sock) { +#ifdef _WIN32 + if (sock == INVALID_SOCKET) { + return SC_SOCKET_NONE; + } + + struct sc_socket_windows *socket = malloc(sizeof(*socket)); + if (!socket) { + LOG_OOM(); + closesocket(sock); + return SC_SOCKET_NONE; + } + + socket->socket = sock; + socket->closed = (atomic_flag) ATOMIC_FLAG_INIT; + + return socket; +#else + return sock; +#endif +} + +static inline sc_raw_socket +unwrap(sc_socket socket) { +#ifdef _WIN32 + if (socket == SC_SOCKET_NONE) { + return INVALID_SOCKET; + } + + return socket->socket; +#else + return socket; +#endif +} + +#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning +static inline bool +sc_raw_socket_close(sc_raw_socket raw_sock) { +#ifndef _WIN32 + return !close(raw_sock); +#else + return !closesocket(raw_sock); +#endif +} +#endif + +#ifndef HAVE_SOCK_CLOEXEC +// If SOCK_CLOEXEC does not exist, the flag must be set manually once the +// socket is created +static bool +set_cloexec_flag(sc_raw_socket raw_sock) { +#ifndef _WIN32 + if (fcntl(raw_sock, F_SETFD, FD_CLOEXEC) == -1) { + perror("fcntl F_SETFD"); + return false; + } +#else + if (!SetHandleInformation((HANDLE) raw_sock, HANDLE_FLAG_INHERIT, 0)) { + LOGE("SetHandleInformation socket failed"); + return false; + } +#endif + return true; +} #endif static void net_perror(const char *s) { #ifdef _WIN32 - int error = WSAGetLastError(); - char *wsa_message; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, - NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (char *) &wsa_message, 0, NULL); - // no explicit '\n', wsa_message already contains a trailing '\n' - fprintf(stderr, "%s: [%d] %s", s, error, wsa_message); - LocalFree(wsa_message); + sc_log_windows_error(s, WSAGetLastError()); #else perror(s); #endif } -socket_t -net_connect(uint32_t addr, uint16_t port) { - socket_t sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock == INVALID_SOCKET) { +sc_socket +net_socket(void) { +#ifdef HAVE_SOCK_CLOEXEC + sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); +#else + sc_raw_socket raw_sock = socket(AF_INET, SOCK_STREAM, 0); + if (raw_sock != SC_RAW_SOCKET_NONE && !set_cloexec_flag(raw_sock)) { + sc_raw_socket_close(raw_sock); + return SC_SOCKET_NONE; + } +#endif + + sc_socket sock = wrap(raw_sock); + if (sock == SC_SOCKET_NONE) { net_perror("socket"); - return INVALID_SOCKET; } + return sock; +} + +bool +net_connect(sc_socket socket, uint32_t addr, uint16_t port) { + sc_raw_socket raw_sock = unwrap(socket); SOCKADDR_IN sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(addr); sin.sin_port = htons(port); - if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + if (connect(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("connect"); - net_close(sock); - return INVALID_SOCKET; + return false; } - return sock; + return true; } -socket_t -net_listen(uint32_t addr, uint16_t port, int backlog) { - socket_t sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock == INVALID_SOCKET) { - net_perror("socket"); - return INVALID_SOCKET; - } +bool +net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog) { + sc_raw_socket raw_sock = unwrap(server_socket); int reuse = 1; - if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, + if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) { net_perror("setsockopt(SO_REUSEADDR)"); } @@ -76,49 +173,64 @@ net_listen(uint32_t addr, uint16_t port, int backlog) { sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY sin.sin_port = htons(port); - if (bind(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { + if (bind(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("bind"); - net_close(sock); - return INVALID_SOCKET; + return false; } - if (listen(sock, backlog) == SOCKET_ERROR) { + if (listen(raw_sock, backlog) == SOCKET_ERROR) { net_perror("listen"); - net_close(sock); - return INVALID_SOCKET; + return false; } - return sock; + return true; } -socket_t -net_accept(socket_t server_socket) { +sc_socket +net_accept(sc_socket server_socket) { + sc_raw_socket raw_server_socket = unwrap(server_socket); + SOCKADDR_IN csin; socklen_t sinsize = sizeof(csin); - return accept(server_socket, (SOCKADDR *) &csin, &sinsize); + +#ifdef HAVE_SOCK_CLOEXEC + sc_raw_socket raw_sock = + accept4(raw_server_socket, (SOCKADDR *) &csin, &sinsize, SOCK_CLOEXEC); +#else + sc_raw_socket raw_sock = + accept(raw_server_socket, (SOCKADDR *) &csin, &sinsize); + if (raw_sock != SC_RAW_SOCKET_NONE && !set_cloexec_flag(raw_sock)) { + sc_raw_socket_close(raw_sock); + return SC_SOCKET_NONE; + } +#endif + + return wrap(raw_sock); } ssize_t -net_recv(socket_t socket, void *buf, size_t len) { - return recv(socket, buf, len, 0); +net_recv(sc_socket socket, void *buf, size_t len) { + sc_raw_socket raw_sock = unwrap(socket); + return recv(raw_sock, buf, len, 0); } ssize_t -net_recv_all(socket_t socket, void *buf, size_t len) { - return recv(socket, buf, len, MSG_WAITALL); +net_recv_all(sc_socket socket, void *buf, size_t len) { + sc_raw_socket raw_sock = unwrap(socket); + return recv(raw_sock, buf, len, MSG_WAITALL); } ssize_t -net_send(socket_t socket, const void *buf, size_t len) { - return send(socket, buf, len, 0); +net_send(sc_socket socket, const void *buf, size_t len) { + sc_raw_socket raw_sock = unwrap(socket); + return send(raw_sock, buf, len, 0); } ssize_t -net_send_all(socket_t socket, const void *buf, size_t len) { +net_send_all(sc_socket socket, const void *buf, size_t len) { size_t copied = 0; - ssize_t w = 0; while (len > 0) { - w = send(socket, buf, len, 0); + ssize_t w = net_send(socket, buf, len); if (w == -1) { return copied ? (ssize_t) copied : -1; } @@ -130,35 +242,45 @@ net_send_all(socket_t socket, const void *buf, size_t len) { } bool -net_shutdown(socket_t socket, int how) { - return !shutdown(socket, how); -} +net_interrupt(sc_socket socket) { + assert(socket != SC_SOCKET_NONE); -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; + sc_raw_socket raw_sock = unwrap(socket); + +#ifdef _WIN32 + if (!atomic_flag_test_and_set(&socket->closed)) { + return !closesocket(raw_sock); } -#endif return true; -} - -void -net_cleanup(void) { -#ifdef __WINDOWS__ - WSACleanup(); +#else + return !shutdown(raw_sock, SHUT_RDWR); #endif } bool -net_close(socket_t socket) { -#ifdef __WINDOWS__ - return !closesocket(socket); +net_close(sc_socket socket) { + sc_raw_socket raw_sock = unwrap(socket); + +#ifdef _WIN32 + bool ret = true; + if (!atomic_flag_test_and_set(&socket->closed)) { + ret = !closesocket(raw_sock); + } + free(socket); + return ret; #else - return !close(socket); + return !close(raw_sock); #endif } + +bool +net_parse_ipv4(const char *s, uint32_t *ipv4) { + struct in_addr addr; + if (!inet_pton(AF_INET, s, &addr)) { + LOGE("Invalid IPv4 address: %s", s); + return false; + } + + *ipv4 = ntohl(addr.s_addr); + return true; +} diff --git a/app/src/util/net.h b/app/src/util/net.h index d3b1f941..21396882 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -1,57 +1,76 @@ -#ifndef NET_H -#define NET_H +#ifndef SC_NET_H +#define SC_NET_H #include "common.h" #include #include -#include -#ifdef __WINDOWS__ +#ifdef _WIN32 + # include - #define SHUT_RD SD_RECEIVE - #define SHUT_WR SD_SEND - #define SHUT_RDWR SD_BOTH - typedef SOCKET socket_t; -#else +# include +# define SC_SOCKET_NONE NULL + typedef struct sc_socket_windows { + SOCKET socket; + atomic_flag closed; + } *sc_socket; + +#else // not _WIN32 + # include -# define INVALID_SOCKET -1 - typedef int socket_t; +# define SC_SOCKET_NONE -1 + typedef int sc_socket; + #endif +#define IPV4_LOCALHOST 0x7F000001 + bool net_init(void); void net_cleanup(void); -socket_t -net_connect(uint32_t addr, uint16_t port); +sc_socket +net_socket(void); -socket_t -net_listen(uint32_t addr, uint16_t port, int backlog); +bool +net_connect(sc_socket socket, uint32_t addr, uint16_t port); + +bool +net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog); -socket_t -net_accept(socket_t server_socket); +sc_socket +net_accept(sc_socket server_socket); // the _all versions wait/retry until len bytes have been written/read ssize_t -net_recv(socket_t socket, void *buf, size_t len); +net_recv(sc_socket socket, void *buf, size_t len); ssize_t -net_recv_all(socket_t socket, void *buf, size_t len); +net_recv_all(sc_socket socket, void *buf, size_t len); ssize_t -net_send(socket_t socket, const void *buf, size_t len); +net_send(sc_socket socket, const void *buf, size_t len); ssize_t -net_send_all(socket_t socket, const void *buf, size_t len); +net_send_all(sc_socket socket, const void *buf, size_t len); + +// Shutdown the socket (or close on Windows) so that any blocking send() or +// recv() are interrupted. +bool +net_interrupt(sc_socket socket); -// how is SHUT_RD (read), SHUT_WR (write) or SHUT_RDWR (both) +// Close the socket. +// A socket must always be closed, even if net_interrupt() has been called. bool -net_shutdown(socket_t socket, int how); +net_close(sc_socket socket); +/** + * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation + */ bool -net_close(socket_t socket); +net_parse_ipv4(const char *ip, uint32_t *ipv4); #endif diff --git a/app/src/util/net_intr.c b/app/src/util/net_intr.c new file mode 100644 index 00000000..55286af6 --- /dev/null +++ b/app/src/util/net_intr.c @@ -0,0 +1,97 @@ +#include "net_intr.h" + +bool +net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, + uint16_t port) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return false; + } + + bool ret = net_connect(socket, addr, port); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return ret; +} + +bool +net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, + uint16_t port, int backlog) { + if (!sc_intr_set_socket(intr, server_socket)) { + // Already interrupted + return false; + } + + bool ret = net_listen(server_socket, addr, port, backlog); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return ret; +} + +sc_socket +net_accept_intr(struct sc_intr *intr, sc_socket server_socket) { + if (!sc_intr_set_socket(intr, server_socket)) { + // Already interrupted + return SC_SOCKET_NONE; + } + + sc_socket socket = net_accept(server_socket); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return socket; +} + +ssize_t +net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t r = net_recv(socket, buf, len); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return r; +} + +ssize_t +net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, + size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t r = net_recv_all(socket, buf, len); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return r; +} + +ssize_t +net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t w = net_send(socket, buf, len); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return w; +} + +ssize_t +net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len) { + if (!sc_intr_set_socket(intr, socket)) { + // Already interrupted + return -1; + } + + ssize_t w = net_send_all(socket, buf, len); + + sc_intr_set_socket(intr, SC_SOCKET_NONE); + return w; +} diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h new file mode 100644 index 00000000..dbef528d --- /dev/null +++ b/app/src/util/net_intr.h @@ -0,0 +1,35 @@ +#ifndef SC_NET_INTR_H +#define SC_NET_INTR_H + +#include "common.h" + +#include "intr.h" +#include "net.h" + +bool +net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, + uint16_t port); + +bool +net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, + uint16_t port, int backlog); + +sc_socket +net_accept_intr(struct sc_intr *intr, sc_socket server_socket); + +ssize_t +net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); + +ssize_t +net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, + size_t len); + +ssize_t +net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len); + +ssize_t +net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, + size_t len); + +#endif diff --git a/app/src/util/process.c b/app/src/util/process.c index 5edeeee6..9c4dcd9f 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,21 +1,102 @@ #include "process.h" +#include +#include #include "log.h" +enum sc_process_result +sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) { + return sc_process_execute_p(argv, pid, flags, NULL, NULL, NULL); +} + +ssize_t +sc_pipe_read_all(sc_pipe pipe, char *data, size_t len) { + size_t copied = 0; + while (len > 0) { + ssize_t r = sc_pipe_read(pipe, data, len); + if (r <= 0) { + return copied ? (ssize_t) copied : r; + } + len -= r; + data += r; + copied += r; + } + return copied; +} + +static int +run_observer(void *data) { + struct sc_process_observer *observer = data; + sc_process_wait(observer->pid, false); // ignore exit code + + sc_mutex_lock(&observer->mutex); + observer->terminated = true; + sc_cond_signal(&observer->cond_terminated); + sc_mutex_unlock(&observer->mutex); + + if (observer->listener) { + observer->listener->on_terminated(observer->listener_userdata); + } + + return 0; +} + bool -process_check_success(process_t proc, const char *name, bool close) { - if (proc == PROCESS_NONE) { - LOGE("Could not execute \"%s\"", name); +sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid, + const struct sc_process_listener *listener, + void *listener_userdata) { + // Either no listener, or on_terminated() is defined + assert(!listener || listener->on_terminated); + + bool ok = sc_mutex_init(&observer->mutex); + if (!ok) { return false; } - 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); - } + + ok = sc_cond_init(&observer->cond_terminated); + if (!ok) { + sc_mutex_destroy(&observer->mutex); return false; } + + observer->pid = pid; + observer->listener = listener; + observer->listener_userdata = listener_userdata; + observer->terminated = false; + + ok = sc_thread_create(&observer->thread, run_observer, "scrcpy-proc", + observer); + if (!ok) { + sc_cond_destroy(&observer->cond_terminated); + sc_mutex_destroy(&observer->mutex); + return false; + } + return true; } + +bool +sc_process_observer_timedwait(struct sc_process_observer *observer, + sc_tick deadline) { + sc_mutex_lock(&observer->mutex); + bool timed_out = false; + while (!observer->terminated && !timed_out) { + timed_out = !sc_cond_timedwait(&observer->cond_terminated, + &observer->mutex, deadline); + } + bool terminated = observer->terminated; + sc_mutex_unlock(&observer->mutex); + + return terminated; +} + +void +sc_process_observer_join(struct sc_process_observer *observer) { + sc_thread_join(&observer->thread, NULL); +} + +void +sc_process_observer_destroy(struct sc_process_observer *observer) { + sc_cond_destroy(&observer->cond_terminated); + sc_mutex_destroy(&observer->mutex); +} diff --git a/app/src/util/process.h b/app/src/util/process.h index 7838a848..4d9d1684 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -4,78 +4,174 @@ #include "common.h" #include +#include "util/thread.h" #ifdef _WIN32 // not needed here, but winsock2.h must never be included AFTER windows.h # include # include -# define PATH_SEPARATOR '\\' -# define PRIexitcode "lu" -// -# define PRIsizet "Iu" -# define PROCESS_NONE NULL -# define NO_EXIT_CODE -1u // max value as unsigned - typedef HANDLE process_t; - typedef DWORD exit_code_t; +# define SC_PRIexitcode "lu" +# define SC_PROCESS_NONE NULL +# define SC_EXIT_CODE_NONE -1UL // max value as unsigned long + typedef HANDLE sc_pid; + typedef DWORD sc_exit_code; + typedef HANDLE sc_pipe; #else # include -# define PATH_SEPARATOR '/' -# define PRIsizet "zu" -# define PRIexitcode "d" -# define PROCESS_NONE -1 -# define NO_EXIT_CODE -1 - typedef pid_t process_t; - typedef int exit_code_t; +# define SC_PRIexitcode "d" +# define SC_PROCESS_NONE -1 +# define SC_EXIT_CODE_NONE -1 + typedef pid_t sc_pid; + typedef int sc_exit_code; + typedef int sc_pipe; #endif -enum process_result { - PROCESS_SUCCESS, - PROCESS_ERROR_GENERIC, - PROCESS_ERROR_MISSING_BINARY, +struct sc_process_listener { + void (*on_terminated)(void *userdata); }; -// execute the command and write the result to the output parameter "process" -enum process_result -process_execute(const char *const argv[], process_t *process); - -// kill the process -bool -process_terminate(process_t pid); +/** + * Tool to observe process termination + * + * To keep things simple and multiplatform, it runs a separate thread to wait + * for process termination (without closing the process to avoid race + * conditions). + * + * It allows a caller to block until the process is terminated (with a + * timeout), and to be notified asynchronously from the observer thread. + * + * The process is not owned by the observer (the observer will never close it). + */ +struct sc_process_observer { + sc_pid pid; + + sc_mutex mutex; + sc_cond cond_terminated; + bool terminated; + + sc_thread thread; + const struct sc_process_listener *listener; + void *listener_userdata; +}; -// 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); +enum sc_process_result { + SC_PROCESS_SUCCESS, + SC_PROCESS_ERROR_GENERIC, + SC_PROCESS_ERROR_MISSING_BINARY, +}; -// close the process -// -// Semantically, process_wait(close) = process_wait(noclose) + process_close +#define SC_PROCESS_NO_STDOUT (1 << 0) +#define SC_PROCESS_NO_STDERR (1 << 1) + +/** + * Execute the command and write the process id to `pid` + * + * The `flags` argument is a bitwise OR of the following values: + * - SC_PROCESS_NO_STDOUT + * - SC_PROCESS_NO_STDERR + * + * It indicates if stdout and stderr must be inherited from the scrcpy process + * (i.e. if the process must output to the scrcpy console). + */ +enum sc_process_result +sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags); + +/** + * Execute the command and write the process id to `pid` + * + * If not NULL, provide a pipe for stdin (`pin`), stdout (`pout`) and stderr + * (`perr`). + * + * The `flags` argument has the same semantics as in `sc_process_execute()`. + */ +enum sc_process_result +sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags, + sc_pipe *pin, sc_pipe *pout, sc_pipe *perr); + +/** + * Kill the process + */ +bool +sc_process_terminate(sc_pid pid); + +/** + * Wait and close the process (similar to waitpid()) + * + * The `close` flag indicates if the process must be _closed_ (reaped) (passing + * false is equivalent to enable WNOWAIT in waitid()). + */ +sc_exit_code +sc_process_wait(sc_pid pid, bool close); + +/** + * Close (reap) the process + * + * Semantically: + * sc_process_wait(close) = sc_process_wait(noclose) + sc_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 +sc_process_close(sc_pid pid); + +/** + * Read from the pipe + * + * Same semantic as read(). + */ +ssize_t +sc_pipe_read(sc_pipe pipe, char *data, size_t len); + +/** + * Read exactly `len` chars from a pipe (unless EOF) + */ +ssize_t +sc_pipe_read_all(sc_pipe pipe, char *data, size_t len); + +/** + * Close the pipe + */ +void +sc_pipe_close(sc_pipe pipe); + +/** + * Start observing process + * + * The listener is optional. If set, its callback will be called from the + * observer thread once the process is terminated. + */ bool -process_check_success(process_t proc, const char *name, bool close); - -#ifndef _WIN32 -// only used to find package manager, not implemented for Windows +sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid, + const struct sc_process_listener *listener, + void *listener_userdata); + +/** + * Wait for process termination until a deadline + * + * Return true if the process is already terminated. Return false if the + * process terminatation has not been detected yet (however, it may have + * terminated in the meantime). + * + * To wait without timeout/deadline, just use sc_process_wait() instead. + */ bool -search_executable(const char *file); -#endif +sc_process_observer_timedwait(struct sc_process_observer *observer, + sc_tick deadline); -// return the absolute path of the executable (the scrcpy binary) -// may be NULL on error; to be freed by free() -char * -get_executable_path(void); +/** + * Join the observer thread + */ +void +sc_process_observer_join(struct sc_process_observer *observer); -// returns true if the file exists and is not a directory -bool -is_regular_file(const char *path); +/** + * Destroy the observer + * + * This does not close the associated process. + */ +void +sc_process_observer_destroy(struct sc_process_observer *observer); #endif diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c new file mode 100644 index 00000000..d37bd5a5 --- /dev/null +++ b/app/src/util/process_intr.c @@ -0,0 +1,35 @@ +#include "process_intr.h" + +ssize_t +sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, + size_t len) { + if (intr && !sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + ssize_t ret = sc_pipe_read(pipe, data, len); + + if (intr) { + sc_intr_set_process(intr, SC_PROCESS_NONE); + } + + return ret; +} + +ssize_t +sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, + char *data, size_t len) { + if (intr && !sc_intr_set_process(intr, pid)) { + // Already interrupted + return false; + } + + ssize_t ret = sc_pipe_read_all(pipe, data, len); + + if (intr) { + sc_intr_set_process(intr, SC_PROCESS_NONE); + } + + return ret; +} diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h new file mode 100644 index 00000000..530a9046 --- /dev/null +++ b/app/src/util/process_intr.h @@ -0,0 +1,17 @@ +#ifndef SC_PROCESS_INTR_H +#define SC_PROCESS_INTR_H + +#include "common.h" + +#include "intr.h" +#include "process.h" + +ssize_t +sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, + size_t len); + +ssize_t +sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, + char *data, size_t len); + +#endif diff --git a/app/src/util/queue.h b/app/src/util/queue.h deleted file mode 100644 index 2233eca0..00000000 --- a/app/src/util/queue.h +++ /dev/null @@ -1,77 +0,0 @@ -// generic intrusive FIFO queue -#ifndef SC_QUEUE_H -#define SC_QUEUE_H - -#include "common.h" - -#include -#include -#include - -// To define a queue type of "struct foo": -// struct queue_foo QUEUE(struct foo); -#define SC_QUEUE(TYPE) { \ - TYPE *first; \ - TYPE *last; \ -} - -#define sc_queue_init(PQ) \ - (void) ((PQ)->first = (PQ)->last = NULL) - -#define sc_queue_is_empty(PQ) \ - !(PQ)->first - -// NEXTFIELD is the field in the ITEM type used for intrusive linked-list -// -// For example: -// struct foo { -// int value; -// struct foo *next; -// }; -// -// // define the type "struct my_queue" -// struct my_queue SC_QUEUE(struct foo); -// -// struct my_queue queue; -// sc_queue_init(&queue); -// -// struct foo v1 = { .value = 42 }; -// struct foo v2 = { .value = 27 }; -// -// sc_queue_push(&queue, next, v1); -// sc_queue_push(&queue, next, v2); -// -// struct foo *foo; -// sc_queue_take(&queue, next, &foo); -// assert(foo->value == 42); -// sc_queue_take(&queue, next, &foo); -// assert(foo->value == 27); -// assert(sc_queue_is_empty(&queue)); -// - -// push a new item into the queue -#define sc_queue_push(PQ, NEXTFIELD, ITEM) \ - (void) ({ \ - (ITEM)->NEXTFIELD = NULL; \ - if (sc_queue_is_empty(PQ)) { \ - (PQ)->first = (PQ)->last = (ITEM); \ - } else { \ - (PQ)->last->NEXTFIELD = (ITEM); \ - (PQ)->last = (ITEM); \ - } \ - }) - -// take the next item and remove it from the queue (the queue must not be empty) -// the result is stored in *(PITEM) -// (without typeof(), we could not store a local variable having the correct -// type so that we can "return" it) -#define sc_queue_take(PQ, NEXTFIELD, PITEM) \ - (void) ({ \ - assert(!sc_queue_is_empty(PQ)); \ - *(PITEM) = (PQ)->first; \ - (PQ)->first = (PQ)->first->NEXTFIELD; \ - }) - // no need to update (PQ)->last if the queue is left empty: - // (PQ)->last is undefined if !(PQ)->first anyway - -#endif diff --git a/app/src/util/rand.c b/app/src/util/rand.c new file mode 100644 index 00000000..590e4ca4 --- /dev/null +++ b/app/src/util/rand.c @@ -0,0 +1,24 @@ +#include "rand.h" + +#include + +#include "tick.h" + +void sc_rand_init(struct sc_rand *rand) { + sc_tick seed = sc_tick_now(); // microsecond precision + rand->xsubi[0] = (seed >> 32) & 0xFFFF; + rand->xsubi[1] = (seed >> 16) & 0xFFFF; + rand->xsubi[2] = seed & 0xFFFF; +} + +uint32_t sc_rand_u32(struct sc_rand *rand) { + // jrand returns a value in range [-2^31, 2^31] + // conversion from signed to unsigned is well-defined to wrap-around + return jrand48(rand->xsubi); +} + +uint64_t sc_rand_u64(struct sc_rand *rand) { + uint32_t msb = sc_rand_u32(rand); + uint32_t lsb = sc_rand_u32(rand); + return ((uint64_t) msb << 32) | lsb; +} diff --git a/app/src/util/rand.h b/app/src/util/rand.h new file mode 100644 index 00000000..262b0b9b --- /dev/null +++ b/app/src/util/rand.h @@ -0,0 +1,16 @@ +#ifndef SC_RAND_H +#define SC_RAND_H + +#include "common.h" + +#include + +struct sc_rand { + unsigned short xsubi[3]; +}; + +void sc_rand_init(struct sc_rand *rand); +uint32_t sc_rand_u32(struct sc_rand *rand); +uint64_t sc_rand_u64(struct sc_rand *rand); + +#endif diff --git a/app/src/util/str_util.c b/app/src/util/str.c similarity index 50% rename from app/src/util/str_util.c rename to app/src/util/str.c index 287c08de..755369d8 100644 --- a/app/src/util/str_util.c +++ b/app/src/util/str.c @@ -1,7 +1,9 @@ -#include "str_util.h" +#include "str.h" +#include #include #include +#include #include #include @@ -10,8 +12,11 @@ # include #endif +#include "log.h" +#include "strbuf.h" + size_t -xstrncpy(char *dest, const char *src, size_t n) { +sc_strncpy(char *dest, const char *src, size_t n) { size_t i; for (i = 0; i < n - 1 && src[i] != '\0'; ++i) dest[i] = src[i]; @@ -21,7 +26,7 @@ xstrncpy(char *dest, const char *src, size_t n) { } size_t -xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) { +sc_str_join(char *dst, const char *const tokens[], char sep, size_t n) { const char *const *remaining = tokens; const char *token = *remaining++; size_t i = 0; @@ -31,7 +36,7 @@ xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) { if (i == n) goto truncated; } - size_t w = xstrncpy(dst + i, token, n - i); + size_t w = sc_strncpy(dst + i, token, n - i); if (w >= n - i) goto truncated; i += w; @@ -45,10 +50,11 @@ truncated: } char * -strquote(const char *src) { +sc_str_quote(const char *src) { size_t len = strlen(src); char *quoted = malloc(len + 3); if (!quoted) { + LOG_OOM(); return NULL; } memcpy("ed[1], src, len); @@ -59,7 +65,7 @@ strquote(const char *src) { } bool -parse_integer(const char *s, long *out) { +sc_str_parse_integer(const char *s, long *out) { char *endptr; if (*s == '\0') { return false; @@ -78,7 +84,8 @@ parse_integer(const char *s, long *out) { } size_t -parse_integers(const char *s, const char sep, size_t max_items, long *out) { +sc_str_parse_integers(const char *s, const char sep, size_t max_items, + long *out) { size_t count = 0; char *endptr; do { @@ -107,7 +114,7 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out) { } bool -parse_integer_with_suffix(const char *s, long *out) { +sc_str_parse_integer_with_suffix(const char *s, long *out) { char *endptr; if (*s == '\0') { return false; @@ -141,7 +148,7 @@ parse_integer_with_suffix(const char *s, long *out) { } bool -strlist_contains(const char *list, char sep, const char *s) { +sc_str_list_contains(const char *list, char sep, const char *s) { char *p; do { p = strchr(list, sep); @@ -159,7 +166,7 @@ strlist_contains(const char *list, char sep, const char *s) { } size_t -utf8_truncation_index(const char *utf8, size_t max_len) { +sc_str_utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); if (len <= max_len) { return len; @@ -177,7 +184,7 @@ utf8_truncation_index(const char *utf8, size_t max_len) { #ifdef _WIN32 wchar_t * -utf8_to_wide_char(const char *utf8) { +sc_str_to_wchars(const char *utf8) { int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); if (!len) { return NULL; @@ -185,6 +192,7 @@ utf8_to_wide_char(const char *utf8) { wchar_t *wide = malloc(len * sizeof(wchar_t)); if (!wide) { + LOG_OOM(); return NULL; } @@ -193,7 +201,7 @@ utf8_to_wide_char(const char *utf8) { } char * -utf8_from_wide_char(const wchar_t *ws) { +sc_str_from_wchars(const wchar_t *ws) { int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); if (!len) { return NULL; @@ -201,6 +209,7 @@ utf8_from_wide_char(const wchar_t *ws) { char *utf8 = malloc(len); if (!utf8) { + LOG_OOM(); return NULL; } @@ -209,3 +218,138 @@ utf8_from_wide_char(const wchar_t *ws) { } #endif + +char * +sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { + assert(indent < columns); + + struct sc_strbuf buf; + + // The output string should not be much longer than the input string (just + // a few '\n' added), so this initial capacity should hopefully almost + // always avoid internal realloc() in string buffer + size_t cap = strlen(input) * 3 / 2; + + if (!sc_strbuf_init(&buf, cap)) { + return false; + } + +#define APPEND(S,N) if (!sc_strbuf_append(&buf, S, N)) goto error +#define APPEND_CHAR(C) if (!sc_strbuf_append_char(&buf, C)) goto error +#define APPEND_N(C,N) if (!sc_strbuf_append_n(&buf, C, N)) goto error +#define APPEND_INDENT() if (indent) APPEND_N(' ', indent) + + APPEND_INDENT(); + + // The last separator encountered, it must be inserted only conditionally, + // depending on the next token + char pending = 0; + + // col tracks the current column in the current line + size_t col = indent; + while (*input) { + size_t sep_idx = strcspn(input, "\n "); + size_t new_col = col + sep_idx; + if (pending == ' ') { + // The pending space counts + ++new_col; + } + bool wrap = new_col > columns; + + char sep = input[sep_idx]; + if (sep == ' ') + sep = ' '; + + if (wrap) { + APPEND_CHAR('\n'); + APPEND_INDENT(); + col = indent; + } else if (pending) { + APPEND_CHAR(pending); + ++col; + if (pending == '\n') + { + APPEND_INDENT(); + col = indent; + } + } + + if (sep_idx) { + APPEND(input, sep_idx); + col += sep_idx; + } + + pending = sep; + + input += sep_idx; + if (*input != '\0') { + // Skip the separator + ++input; + } + } + + if (pending) + APPEND_CHAR(pending); + + return buf.s; + +error: + free(buf.s); + return NULL; +} + +ssize_t +sc_str_index_of_column(const char *s, unsigned col, const char *seps) { + size_t colidx = 0; + + size_t idx = 0; + while (s[idx] != '\0' && colidx != col) { + size_t r = strcspn(&s[idx], seps); + idx += r; + + if (s[idx] == '\0') { + // Not found + return -1; + } + + size_t consecutive_seps = strspn(&s[idx], seps); + assert(consecutive_seps); // At least one + idx += consecutive_seps; + + if (s[idx] != '\0') { + ++colidx; + } + } + + return col == colidx ? (ssize_t) idx : -1; +} + +size_t +sc_str_remove_trailing_cr(char *s, size_t len) { + while (len) { + if (s[len - 1] != '\r') { + break; + } + s[--len] = '\0'; + } + return len; +} + +char * +sc_str_to_hex_string(const uint8_t *data, size_t size) { + size_t buffer_size = size * 3 + 1; + char *buffer = malloc(buffer_size); + if (!buffer) { + LOG_OOM(); + return NULL; + } + + for (size_t i = 0; i < size; ++i) { + snprintf(buffer + i * 3, 4, "%02X ", data[i]); + } + + // Remove the final space + buffer[size * 3] = '\0'; + + return buffer; +} diff --git a/app/src/util/str.h b/app/src/util/str.h new file mode 100644 index 00000000..20da26f0 --- /dev/null +++ b/app/src/util/str.h @@ -0,0 +1,147 @@ +#ifndef SC_STR_H +#define SC_STR_H + +#include "common.h" + +#include +#include + +/* Stringify a numeric value */ +#define SC_STR(s) SC_XSTR(s) +#define SC_XSTR(s) #s + +/** + * Like strncpy(), except: + * - it copies at most n-1 chars + * - the dest string is nul-terminated + * - it does not write useless bytes if strlen(src) < n + * - it returns the number of chars actually written (max n-1) if src has + * been copied completely, or n if src has been truncated + */ +size_t +sc_strncpy(char *dest, const char *src, size_t n); + +/** + * Join tokens by separator `sep` into `dst` + * + * Return the number of chars actually written (max n-1) if no truncation + * occurred, or n if truncated. + */ +size_t +sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); + +/** + * Quote a string + * + * Return a new allocated string, surrounded with quotes (`"`). + */ +char * +sc_str_quote(const char *src); + +/** + * Parse `s` as an integer into `out` + * + * Return true if the conversion succeeded, false otherwise. + */ +bool +sc_str_parse_integer(const char *s, long *out); + +/** + * Parse `s` as integers separated by `sep` (for example `1234:2000`) into `out` + * + * Returns the number of integers on success, 0 on failure. + */ +size_t +sc_str_parse_integers(const char *s, const char sep, size_t max_items, + long *out); + +/** + * Parse `s` as an integer into `out` + * + * Like `sc_str_parse_integer()`, but accept 'k'/'K' (x1000) and 'm'/'M' + * (x1000000) as suffixes. + * + * Return true if the conversion succeeded, false otherwise. + */ +bool +sc_str_parse_integer_with_suffix(const char *s, long *out); + +/** + * Search `s` in the list separated by `sep` + * + * For example, sc_str_list_contains("a,bc,def", ',', "bc") returns true. + */ +bool +sc_str_list_contains(const char *list, char sep, const char *s); + +/** + * Return the index to truncate a UTF-8 string at a valid position + */ +size_t +sc_str_utf8_truncation_index(const char *utf8, size_t max_len); + +#ifdef _WIN32 +/** + * Convert a UTF-8 string to a wchar_t string + * + * Return the new allocated string, to be freed by the caller. + */ +wchar_t * +sc_str_to_wchars(const char *utf8); + +/** + * Convert a wchar_t string to a UTF-8 string + * + * Return the new allocated string, to be freed by the caller. + */ +char * +sc_str_from_wchars(const wchar_t *s); +#endif + +/** + * Wrap input lines to fit in `columns` columns + * + * Break input lines at word boundaries (spaces) so that they fit in `columns` + * columns, left-indented by `indent` spaces. + */ +char * +sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); + +/** + * Find the start of a column in a string + * + * A string may represent several columns, separated by some "spaces" + * (separators). This function aims to find the start of the column number + * `col`. + * + * For example, to find the 4th column (column number 3): + * + * // here + * // v + * const char *s = "abc def ghi jk"; + * ssize_t index = sc_str_index_of_column(s, 3, " "); + * assert(index == 16); // points to "jk" + * + * Return -1 if no such column exists. + */ +ssize_t +sc_str_index_of_column(const char *s, unsigned col, const char *seps); + +/** + * Remove all `\r` at the end of the line + * + * The line length is provided by `len` (this avoids a call to `strlen()` when + * the caller already knows the length). + * + * Return the new length. + */ +size_t +sc_str_remove_trailing_cr(char *s, size_t len); + +/** + * Convert binary data to hexadecimal string + */ +char * +sc_str_to_hex_string(const uint8_t *data, size_t len); + +#endif diff --git a/app/src/util/str_util.h b/app/src/util/str_util.h deleted file mode 100644 index 361d2bdd..00000000 --- a/app/src/util/str_util.h +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef STRUTIL_H -#define STRUTIL_H - -#include "common.h" - -#include -#include - -// like strncpy, except: -// - it copies at most n-1 chars -// - the dest string is nul-terminated -// - it does not write useless bytes if strlen(src) < n -// - it returns the number of chars actually written (max n-1) if src has -// been copied completely, or n if src has been truncated -size_t -xstrncpy(char *dest, const char *src, size_t n); - -// join tokens by sep into dst -// returns the number of chars actually written (max n-1) if no truncation -// occurred, or n if truncated -size_t -xstrjoin(char *dst, const char *const tokens[], char sep, size_t n); - -// quote a string -// returns the new allocated string, to be freed by the caller -char * -strquote(const char *src); - -// parse s as an integer into value -// returns true if the conversion succeeded, false otherwise -bool -parse_integer(const char *s, long *out); - -// parse s as integers separated by sep (for example '1234:2000') -// returns the number of integers on success, 0 on failure -size_t -parse_integers(const char *s, const char sep, size_t max_items, long *out); - -// parse s as an integer into value -// like parse_integer(), but accept 'k'/'K' (x1000) and 'm'/'M' (x1000000) as -// suffix -// returns true if the conversion succeeded, false otherwise -bool -parse_integer_with_suffix(const char *s, long *out); - -// search s in the list separated by sep -// for example, strlist_contains("a,bc,def", ',', "bc") returns true -bool -strlist_contains(const char *list, char sep, const char *s); - -// return the index to truncate a UTF-8 string at a valid position -size_t -utf8_truncation_index(const char *utf8, size_t max_len); - -#ifdef _WIN32 -// convert a UTF-8 string to a wchar_t string -// returns the new allocated string, to be freed by the caller -wchar_t * -utf8_to_wide_char(const char *utf8); - -char * -utf8_from_wide_char(const wchar_t *s); -#endif - -#endif diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c new file mode 100644 index 00000000..1892b46b --- /dev/null +++ b/app/src/util/strbuf.c @@ -0,0 +1,90 @@ +#include "strbuf.h" + +#include +#include +#include +#include + +#include "log.h" + +bool +sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { + buf->s = malloc(init_cap + 1); // +1 for '\0' + if (!buf->s) { + LOG_OOM(); + return false; + } + + buf->len = 0; + buf->cap = init_cap; + return true; +} + +static bool +sc_strbuf_reserve(struct sc_strbuf *buf, size_t len) { + if (buf->len + len > buf->cap) { + size_t new_cap = buf->cap * 3 / 2 + len; + char *s = realloc(buf->s, new_cap + 1); // +1 for '\0' + if (!s) { + // Leave the old buf->s + LOG_OOM(); + return false; + } + buf->s = s; + buf->cap = new_cap; + } + return true; +} + +bool +sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len) { + assert(s); + assert(*s); + assert(strlen(s) >= len); + if (!sc_strbuf_reserve(buf, len)) { + return false; + } + + memcpy(&buf->s[buf->len], s, len); + buf->len += len; + buf->s[buf->len] = '\0'; + + return true; +} + +bool +sc_strbuf_append_char(struct sc_strbuf *buf, const char c) { + if (!sc_strbuf_reserve(buf, 1)) { + return false; + } + + buf->s[buf->len] = c; + buf->len ++; + buf->s[buf->len] = '\0'; + + return true; +} + +bool +sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n) { + if (!sc_strbuf_reserve(buf, n)) { + return false; + } + + memset(&buf->s[buf->len], c, n); + buf->len += n; + buf->s[buf->len] = '\0'; + + return true; +} + +void +sc_strbuf_shrink(struct sc_strbuf *buf) { + assert(buf->len <= buf->cap); + if (buf->len != buf->cap) { + char *s = realloc(buf->s, buf->len + 1); // +1 for '\0' + assert(s); // decreasing the size may not fail + buf->s = s; + buf->cap = buf->len; + } +} diff --git a/app/src/util/strbuf.h b/app/src/util/strbuf.h new file mode 100644 index 00000000..1878df2f --- /dev/null +++ b/app/src/util/strbuf.h @@ -0,0 +1,73 @@ +#ifndef SC_STRBUF_H +#define SC_STRBUF_H + +#include "common.h" + +#include +#include +#include + +struct sc_strbuf { + char *s; + size_t len; + size_t cap; +}; + +/** + * Initialize the string buffer + * + * `buf->s` must be manually freed by the caller. + */ +bool +sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap); + +/** + * Append a string + * + * Append `len` characters from `s` to the buffer. + */ +bool +sc_strbuf_append(struct sc_strbuf *buf, const char *s, size_t len); + +/** + * Append a char + * + * Append a single character to the buffer. + */ +bool +sc_strbuf_append_char(struct sc_strbuf *buf, const char c); + +/** + * Append a char `n` times + * + * Append the same characters `n` times to the buffer. + */ +bool +sc_strbuf_append_n(struct sc_strbuf *buf, const char c, size_t n); + +/** + * Append a NUL-terminated string + */ +static inline bool +sc_strbuf_append_str(struct sc_strbuf *buf, const char *s) { + return sc_strbuf_append(buf, s, strlen(s)); +} + +/** + * Append a static string + * + * Append a string whose size is known at compile time (for + * example a string literal). + */ +#define sc_strbuf_append_staticstr(BUF, S) \ + sc_strbuf_append(BUF, S, sizeof(S) - 1) + +/** + * Shrink the buffer capacity to its current length + * + * This resizes `buf->s` to fit the content. + */ +void +sc_strbuf_shrink(struct sc_strbuf *buf); + +#endif diff --git a/app/src/util/term.c b/app/src/util/term.c new file mode 100644 index 00000000..ff6bc4b1 --- /dev/null +++ b/app/src/util/term.c @@ -0,0 +1,51 @@ +#include "term.h" + +#include + +#ifdef _WIN32 +# include +#else +# include +# include +#endif + +bool +sc_term_get_size(unsigned *rows, unsigned *cols) { +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO csbi; + + bool ok = + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + if (!ok) { + return false; + } + + if (rows) { + assert(csbi.srWindow.Bottom >= csbi.srWindow.Top); + *rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; + } + + if (cols) { + assert(csbi.srWindow.Right >= csbi.srWindow.Left); + *cols = csbi.srWindow.Right - csbi.srWindow.Left + 1; + } + + return true; +#else + struct winsize ws; + int r = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + if (r == -1) { + return false; + } + + if (rows) { + *rows = ws.ws_row; + } + + if (cols) { + *cols = ws.ws_col; + } + + return true; +#endif +} diff --git a/app/src/util/term.h b/app/src/util/term.h new file mode 100644 index 00000000..0211bcb4 --- /dev/null +++ b/app/src/util/term.h @@ -0,0 +1,21 @@ +#ifndef SC_TERM_H +#define SC_TERM_H + +#include "common.h" + +#include + +/** + * Return the terminal dimensions + * + * Return false if the dimensions could not be retrieved. + * + * Otherwise, return true, and: + * - if `rows` is not NULL, then the number of rows is written to `*rows`. + * - if `columns` is not NULL, then the number of columns is written to + * `*columns`. + */ +bool +sc_term_get_size(unsigned *rows, unsigned *cols); + +#endif diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 2c376e97..94921fb7 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -1,6 +1,7 @@ #include "thread.h" #include +#include #include #include "log.h" @@ -8,8 +9,13 @@ bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { + // The thread name length is limited on some systems. Never use a name + // longer than 16 bytes (including the final '\0') + assert(strlen(name) <= 15); + SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); if (!sdl_thread) { + LOG_OOM(); return false; } @@ -17,6 +23,39 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, return true; } +static SDL_ThreadPriority +to_sdl_thread_priority(enum sc_thread_priority priority) { + switch (priority) { + case SC_THREAD_PRIORITY_TIME_CRITICAL: +#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL + return SDL_THREAD_PRIORITY_TIME_CRITICAL; +#else + // fall through +#endif + case SC_THREAD_PRIORITY_HIGH: + return SDL_THREAD_PRIORITY_HIGH; + case SC_THREAD_PRIORITY_NORMAL: + return SDL_THREAD_PRIORITY_NORMAL; + case SC_THREAD_PRIORITY_LOW: + return SDL_THREAD_PRIORITY_LOW; + default: + assert(!"Unknown thread priority"); + return 0; + } +} + +bool +sc_thread_set_priority(enum sc_thread_priority priority) { + SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority); + int r = SDL_SetThreadPriority(sdl_priority); + if (r) { + LOGD("Could not set thread priority: %s", SDL_GetError()); + return false; + } + + return true; +} + void sc_thread_join(sc_thread *thread, int *status) { SDL_WaitThread(thread->thread, status); @@ -26,6 +65,7 @@ bool sc_mutex_init(sc_mutex *mutex) { SDL_mutex *sdl_mutex = SDL_CreateMutex(); if (!sdl_mutex) { + LOG_OOM(); return false; } @@ -48,7 +88,7 @@ sc_mutex_lock(sc_mutex *mutex) { int r = SDL_LockMutex(mutex->mutex); #ifndef NDEBUG if (r) { - LOGC("Could not lock mutex: %s", SDL_GetError()); + LOGE("Could not lock mutex: %s", SDL_GetError()); abort(); } @@ -68,7 +108,7 @@ sc_mutex_unlock(sc_mutex *mutex) { int r = SDL_UnlockMutex(mutex->mutex); #ifndef NDEBUG if (r) { - LOGC("Could not lock mutex: %s", SDL_GetError()); + LOGE("Could not lock mutex: %s", SDL_GetError()); abort(); } #else @@ -94,6 +134,7 @@ bool sc_cond_init(sc_cond *cond) { SDL_cond *sdl_cond = SDL_CreateCond(); if (!sdl_cond) { + LOG_OOM(); return false; } @@ -111,7 +152,7 @@ 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()); + LOGE("Could not wait on condition: %s", SDL_GetError()); abort(); } @@ -129,11 +170,13 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { return false; // timeout } - uint32_t ms = SC_TICK_TO_MS(deadline - now); + // Round up to the next millisecond to guarantee that the deadline is + // reached when returning due to timeout + uint32_t ms = SC_TICK_TO_MS(deadline - now + SC_TICK_FROM_MS(1) - 1); 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()); + LOGE("Could not wait on condition with timeout: %s", SDL_GetError()); abort(); } @@ -141,6 +184,8 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { memory_order_relaxed); #endif assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); + // The deadline is reached on timeout + assert(r != SDL_MUTEX_TIMEDOUT || sc_tick_now() >= deadline); return r == 0; } @@ -149,7 +194,7 @@ 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()); + LOGE("Could not signal a condition: %s", SDL_GetError()); abort(); } #else @@ -162,7 +207,7 @@ 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()); + LOGE("Could not broadcast a condition: %s", SDL_GetError()); abort(); } #else diff --git a/app/src/util/thread.h b/app/src/util/thread.h index 7add6f1c..4183adac 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -21,6 +21,13 @@ typedef struct sc_thread { SDL_Thread *thread; } sc_thread; +enum sc_thread_priority { + SC_THREAD_PRIORITY_LOW, + SC_THREAD_PRIORITY_NORMAL, + SC_THREAD_PRIORITY_HIGH, + SC_THREAD_PRIORITY_TIME_CRITICAL, +}; + typedef struct sc_mutex { SDL_mutex *mutex; #ifndef NDEBUG @@ -39,6 +46,9 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void sc_thread_join(sc_thread *thread, int *status); +bool +sc_thread_set_priority(enum sc_thread_priority priority); + bool sc_mutex_init(sc_mutex *mutex); diff --git a/app/src/util/tick.c b/app/src/util/tick.c index b85ce971..cc0bab5e 100644 --- a/app/src/util/tick.c +++ b/app/src/util/tick.c @@ -1,16 +1,55 @@ #include "tick.h" -#include +#include +#include +#ifdef _WIN32 +# include +#endif sc_tick sc_tick_now(void) { - // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed - // in microseconds to store PTS without precision loss. - // - // As an alternative, SDL_GetPerformanceCounter() and - // SDL_GetPerformanceFrequency() could be used, but: - // - the conversions (avoiding overflow) are expansive, since the - // frequency is not known at compile time; - // - in practice, we don't need more precision for now. - return (sc_tick) SDL_GetTicks() * 1000; +#ifndef _WIN32 + // Maximum sc_tick precision (microsecond) + struct timespec ts; + int ret = clock_gettime(CLOCK_MONOTONIC, &ts); + if (ret) { + abort(); + } + + return SC_TICK_FROM_SEC(ts.tv_sec) + SC_TICK_FROM_NS(ts.tv_nsec); +#else + LARGE_INTEGER c; + + // On systems that run Windows XP or later, the function will always + // succeed and will thus never return zero. + // + // + + BOOL ok = QueryPerformanceCounter(&c); + assert(ok); + (void) ok; + + LONGLONG counter = c.QuadPart; + + static LONGLONG frequency; + if (!frequency) { + // Initialize on first call + LARGE_INTEGER f; + ok = QueryPerformanceFrequency(&f); + assert(ok); + frequency = f.QuadPart; + assert(frequency); + } + + if (frequency % SC_TICK_FREQ == 0) { + // Expected case (typically frequency = 10000000, i.e. 100ns precision) + sc_tick div = frequency / SC_TICK_FREQ; + return SC_TICK_FROM_US(counter / div); + } + + // Split the division to avoid overflow + sc_tick secs = SC_TICK_FROM_SEC(counter / frequency); + sc_tick subsec = SC_TICK_FREQ * (counter % frequency) / frequency; + return secs + subsec; +#endif } diff --git a/app/src/util/tick.h b/app/src/util/tick.h index 472a18a7..2d941f23 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -1,6 +1,8 @@ #ifndef SC_TICK_H #define SC_TICK_H +#include "common.h" + #include typedef int64_t sc_tick; @@ -8,9 +10,11 @@ typedef int64_t sc_tick; #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes +#define SC_TICK_TO_NS(tick) ((tick) * 1000) #define SC_TICK_TO_US(tick) (tick) #define SC_TICK_TO_MS(tick) ((tick) / 1000) #define SC_TICK_TO_SEC(tick) ((tick) / 1000000) +#define SC_TICK_FROM_NS(ns) ((ns) / 1000) #define SC_TICK_FROM_US(us) (us) #define SC_TICK_FROM_MS(ms) ((ms) * 1000) #define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c new file mode 100644 index 00000000..a1665373 --- /dev/null +++ b/app/src/util/timeout.c @@ -0,0 +1,77 @@ +#include "timeout.h" + +#include + +#include "log.h" + +bool +sc_timeout_init(struct sc_timeout *timeout) { + bool ok = sc_mutex_init(&timeout->mutex); + if (!ok) { + return false; + } + + ok = sc_cond_init(&timeout->cond); + if (!ok) { + return false; + } + + timeout->stopped = false; + + return true; +} + +static int +run_timeout(void *data) { + struct sc_timeout *timeout = data; + sc_tick deadline = timeout->deadline; + + sc_mutex_lock(&timeout->mutex); + bool timed_out = false; + while (!timeout->stopped && !timed_out) { + timed_out = !sc_cond_timedwait(&timeout->cond, &timeout->mutex, + deadline); + } + sc_mutex_unlock(&timeout->mutex); + + timeout->cbs->on_timeout(timeout, timeout->cbs_userdata); + + return 0; +} + +bool +sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline, + const struct sc_timeout_callbacks *cbs, void *cbs_userdata) { + bool ok = sc_thread_create(&timeout->thread, run_timeout, "scrcpy-timeout", + timeout); + if (!ok) { + LOGE("Timeout: could not start thread"); + return false; + } + + timeout->deadline = deadline; + + assert(cbs && cbs->on_timeout); + timeout->cbs = cbs; + timeout->cbs_userdata = cbs_userdata; + + return true; +} + +void +sc_timeout_stop(struct sc_timeout *timeout) { + sc_mutex_lock(&timeout->mutex); + timeout->stopped = true; + sc_mutex_unlock(&timeout->mutex); +} + +void +sc_timeout_join(struct sc_timeout *timeout) { + sc_thread_join(&timeout->thread, NULL); +} + +void +sc_timeout_destroy(struct sc_timeout *timeout) { + sc_mutex_destroy(&timeout->mutex); + sc_cond_destroy(&timeout->cond); +} diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h new file mode 100644 index 00000000..ae171b86 --- /dev/null +++ b/app/src/util/timeout.h @@ -0,0 +1,43 @@ +#ifndef SC_TIMEOUT_H +#define SC_TIMEOUT_H + +#include "common.h" + +#include + +#include "thread.h" +#include "tick.h" + +struct sc_timeout { + sc_thread thread; + sc_tick deadline; + + sc_mutex mutex; + sc_cond cond; + bool stopped; + + const struct sc_timeout_callbacks *cbs; + void *cbs_userdata; +}; + +struct sc_timeout_callbacks { + void (*on_timeout)(struct sc_timeout *timeout, void *userdata); +}; + +bool +sc_timeout_init(struct sc_timeout *timeout); + +void +sc_timeout_destroy(struct sc_timeout *timeout); + +bool +sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline, + const struct sc_timeout_callbacks *cbs, void *cbs_userdata); + +void +sc_timeout_stop(struct sc_timeout *timeout); + +void +sc_timeout_join(struct sc_timeout *timeout); + +#endif diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h new file mode 100644 index 00000000..ce559ee9 --- /dev/null +++ b/app/src/util/vecdeque.h @@ -0,0 +1,379 @@ +#ifndef SC_VECDEQUE_H +#define SC_VECDEQUE_H + +#include "common.h" + +#include +#include +#include +#include +#include + +#include "util/memory.h" + +/** + * A double-ended queue implemented with a growable ring buffer. + * + * Inspired from the Rust VecDeque type: + * + */ + +/** + * VecDeque struct body + * + * A VecDeque is a dynamic ring-buffer, managed by the sc_vecdeque_* helpers. + * + * It is generic over the type of its items, so it is implemented via macros. + * + * To use a VecDeque, a new type must be defined: + * + * struct vecdeque_int SC_VECDEQUE(int); + * + * The struct may be anonymous: + * + * struct SC_VECDEQUE(const char *) names; + * + * Functions and macros having name ending with '_' are private. + */ +#define SC_VECDEQUE(type) { \ + size_t cap; \ + size_t origin; \ + size_t size; \ + type *data; \ +} + +/** + * Static initializer for a VecDeque + */ +#define SC_VECDEQUE_INITIALIZER { 0, 0, 0, NULL } + +/** + * Initialize an empty VecDeque + */ +#define sc_vecdeque_init(pv) \ +({ \ + (pv)->cap = 0; \ + (pv)->origin = 0; \ + (pv)->size = 0; \ + (pv)->data = NULL; \ +}) + +/** + * Destroy a VecDeque + */ +#define sc_vecdeque_destroy(pv) \ + free((pv)->data) + +/** + * Clear a VecDeque + * + * Remove all items. + */ +#define sc_vecdeque_clear(pv) \ +(void) ({ \ + sc_vecdeque_destroy(pv); \ + sc_vecdeque_init(pv); \ +}) + +/** + * Returns the content size + */ +#define sc_vecdeque_size(pv) \ + (pv)->size + +/** + * Return whether the VecDeque is empty (i.e. its size is 0) + */ +#define sc_vecdeque_is_empty(pv) \ + ((pv)->size == 0) + +/** + * Return whether the VecDeque is full + * + * A VecDeque is full when its size equals its current capacity. However, it + * does not prevent to push a new item (with sc_vecdeque_push()), since this + * will increase its capacity. + */ +#define sc_vecdeque_is_full(pv) \ + ((pv)->size == (pv)->cap) + +/** + * The minimal allocation size, in number of items + * + * Private. + */ +#define SC_VECDEQUE_MINCAP_ ((size_t) 10) + +/** + * The maximal allocation size, in number of items + * + * Use SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. + * + * Private. + */ +#define sc_vecdeque_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) + +/** + * Realloc the internal array to a specific capacity + * + * On reallocation success, update the VecDeque capacity (`*pcap`) and origin + * (`*porigin`), and return the reallocated data. + * + * On reallocation failure, return NULL without any change. + * + * Private. + * + * \param ptr the current `data` field of the SC_VECDEQUE to realloc + * \param newcap the requested capacity, in number of items + * \param item_size the size of one item (the generic type is unknown from this + * function) + * \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT] + * \param porigin a pointer to pv->origin [IN/OUT] + * \param size the `size` field of the SC_VECDEQUE + * \return the new array to assign to the `data` field of the SC_VECDEQUE (if + * not NULL) + */ +static inline void * +sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size, + size_t *pcap, size_t *porigin, size_t size) { + + size_t oldcap = *pcap; + size_t oldorigin = *porigin; + + assert(newcap > oldcap); // Could only grow + + if (oldorigin + size <= oldcap) { + // The current content will stay in place, just realloc + // + // As an example, here is the content of a ring-buffer (oldcap=10) + // before the realloc: + // + // _ _ 2 3 4 5 6 7 _ _ + // ^ + // origin + // + // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): + // + // _ _ 2 3 4 5 6 7 _ _ _ _ _ _ _ + // ^ + // origin + + void *newptr = reallocarray(ptr, newcap, item_size); + if (!newptr) { + return NULL; + } + + *pcap = newcap; + return newptr; + } + + // Copy the current content to the new array + // + // As an example, here is the content of a ring-buffer (oldcap=10) before + // the realloc: + // + // 5 6 7 _ _ 0 1 2 3 4 + // ^ + // origin + // + // It is resized (newcap=15), e.g. with sc_vecdeque_reserve(): + // + // 0 1 2 3 4 5 6 7 _ _ _ _ _ _ _ + // ^ + // origin + + assert(size); + void *newptr = sc_allocarray(newcap, item_size); + if (!newptr) { + return NULL; + } + + size_t right_len = MIN(size, oldcap - oldorigin); + assert(right_len); + memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size); + + if (size > right_len) { + memcpy((char *) newptr + (right_len * item_size), ptr, + (size - right_len) * item_size); + } + + free(ptr); + + *pcap = newcap; + *porigin = 0; + return newptr; +} + +/** + * Macro to realloc the internal data to a new capacity + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_realloc_(pv, newcap) \ +({ \ + void *p = sc_vecdeque_reallocdata_((pv)->data, newcap, \ + sizeof(*(pv)->data), &(pv)->cap, \ + &(pv)->origin, (pv)->size); \ + if (p) { \ + (pv)->data = p; \ + } \ + (bool) p; \ +}); + +static inline size_t +sc_vecdeque_growsize_(size_t value) +{ + /* integer multiplication by 1.5 */ + return value + (value >> 1); +} + +/** + * Increase the capacity of the VecDeque to at least `mincap` + * + * \param pv a pointer to the VecDeque + * \param mincap (`size_t`) the requested capacity + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_reserve(pv, mincap) \ +({ \ + assert(mincap <= sc_vecdeque_max_cap_(pv)); \ + bool ok; \ + /* avoid to allocate tiny arrays (< SC_VECDEQUE_MINCAP_) */ \ + size_t mincap_ = MAX(mincap, SC_VECDEQUE_MINCAP_); \ + if (mincap_ <= (pv)->cap) { \ + /* nothing to do */ \ + ok = true; \ + } else if (mincap_ <= sc_vecdeque_max_cap_(pv)) { \ + /* not too big */ \ + size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ + newsize = CLAMP(newsize, mincap_, sc_vecdeque_max_cap_(pv)); \ + ok = sc_vecdeque_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +/** + * Automatically grow the VecDeque capacity + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_grow_(pv) \ +({ \ + bool ok; \ + if ((pv)->cap < sc_vecdeque_max_cap_(pv)) { \ + size_t newsize = sc_vecdeque_growsize_((pv)->cap); \ + newsize = CLAMP(newsize, SC_VECDEQUE_MINCAP_, \ + sc_vecdeque_max_cap_(pv)); \ + ok = sc_vecdeque_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +/** + * Grow the VecDeque capacity if it is full + * + * Private. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_grow_if_needed_(pv) \ + (!sc_vecdeque_is_full(pv) || sc_vecdeque_grow_(pv)) + +/** + * Push an uninitialized item, and return a pointer to it + * + * It does not attempt to resize the VecDeque. It is an error to this function + * if the VecDeque is full. + * + * This function may not fail. It returns a valid non-NULL pointer to the + * uninitialized item just pushed. + */ +#define sc_vecdeque_push_hole_noresize(pv) \ +({ \ + assert(!sc_vecdeque_is_full(pv)); \ + ++(pv)->size; \ + &(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap]; \ +}) + +/** + * Push an uninitialized item, and return a pointer to it + * + * If the VecDeque is full, it is resized. + * + * This function returns either a valid non-NULL pointer to the uninitialized + * item just pushed, or NULL on reallocation failure. + */ +#define sc_vecdeque_push_hole(pv) \ + (sc_vecdeque_grow_if_needed_(pv) ? \ + sc_vecdeque_push_hole_noresize(pv) : NULL) + +/** + * Push an item + * + * It does not attempt to resize the VecDeque. It is an error to this function + * if the VecDeque is full. + * + * This function may not fail. + */ +#define sc_vecdeque_push_noresize(pv, item) \ +(void) ({ \ + assert(!sc_vecdeque_is_full(pv)); \ + ++(pv)->size; \ + (pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap] = item; \ +}) + +/** + * Push an item + * + * If the VecDeque is full, it is resized. + * + * \retval true on success + * \retval false on allocation failure (the VecDeque is left untouched) + */ +#define sc_vecdeque_push(pv, item) \ +({ \ + bool ok = sc_vecdeque_grow_if_needed_(pv); \ + if (ok) { \ + sc_vecdeque_push_noresize(pv, item); \ + } \ + ok; \ +}) + +/** + * Pop an item and return a pointer to it (still in the VecDeque) + * + * Returning a pointer allows the caller to destroy it in place without copy + * (especially if the item type is big). + * + * It is an error to call this function if the VecDeque is empty. + */ +#define sc_vecdeque_popref(pv) \ +({ \ + assert(!sc_vecdeque_is_empty(pv)); \ + size_t pos = (pv)->origin; \ + (pv)->origin = ((pv)->origin + 1) % (pv)->cap; \ + --(pv)->size; \ + &(pv)->data[pos]; \ +}) + +/** + * Pop an item and return it + * + * It is an error to call this function if the VecDeque is empty. + */ +#define sc_vecdeque_pop(pv) \ + (*sc_vecdeque_popref(pv)) + +#endif diff --git a/app/src/util/vector.h b/app/src/util/vector.h new file mode 100644 index 00000000..97d7c389 --- /dev/null +++ b/app/src/util/vector.h @@ -0,0 +1,541 @@ +#ifndef SC_VECTOR_H +#define SC_VECTOR_H + +#include "common.h" + +#include +#include +#include +#include + +// Adapted from vlc_vector: +// + +/** + * Vector struct body + * + * A vector is a dynamic array, managed by the sc_vector_* helpers. + * + * It is generic over the type of its items, so it is implemented as macros. + * + * To use a vector, a new type must be defined: + * + * struct vec_int SC_VECTOR(int); + * + * The struct may be anonymous: + * + * struct SC_VECTOR(const char *) names; + * + * Vector size is accessible via `vec.size`, and items are intended to be + * accessed directly, via `vec.data[i]`. + * + * Functions and macros having name ending with '_' are private. + */ +#define SC_VECTOR(type) \ +{ \ + size_t cap; \ + size_t size; \ + type *data; \ +} + +/** + * Static initializer for a vector + */ +#define SC_VECTOR_INITIALIZER { 0, 0, NULL } + +/** + * Initialize an empty vector + */ +#define sc_vector_init(pv) \ +({ \ + (pv)->cap = 0; \ + (pv)->size = 0; \ + (pv)->data = NULL; \ +}) + +/** + * Destroy a vector + * + * The vector may not be used anymore unless sc_vector_init() is called. + */ +#define sc_vector_destroy(pv) \ + free((pv)->data) + +/** + * Clear a vector + * + * Remove all items from the vector. + */ +#define sc_vector_clear(pv) \ +({ \ + sc_vector_destroy(pv); \ + sc_vector_init(pv);\ +}) + +/** + * The minimal allocation size, in number of items + * + * Private. + */ +#define SC_VECTOR_MINCAP_ ((size_t) 10) + +static inline size_t +sc_vector_min_(size_t a, size_t b) +{ + return a < b ? a : b; +} + +static inline size_t +sc_vector_max_(size_t a, size_t b) +{ + return a > b ? a : b; +} + +static inline size_t +sc_vector_clamp_(size_t x, size_t min, size_t max) +{ + return sc_vector_max_(min, sc_vector_min_(max, x)); +} + +/** + * Realloc data and update vector fields + * + * On reallocation success, update the vector capacity (*pcap) and size + * (*psize), and return the reallocated data. + * + * On reallocation failure, return NULL without any change. + * + * Private. + * + * \param ptr the current `data` field of the vector to realloc + * \param count the requested capacity, in number of items + * \param size the size of one item + * \param pcap a pointer to the `cap` field of the vector [IN/OUT] + * \param psize a pointer to the `size` field of the vector [IN/OUT] + * \return the new ptr on success, NULL on error + */ +static inline void * +sc_vector_reallocdata_(void *ptr, size_t count, size_t size, + size_t *restrict pcap, size_t *restrict psize) +{ + void *p = reallocarray(ptr, count, size); + if (!p) { + return NULL; + } + + *pcap = count; + *psize = sc_vector_min_(*psize, count); + return p; +} + +#define sc_vector_realloc_(pv, newcap) \ +({ \ + void *p = sc_vector_reallocdata_((pv)->data, newcap, sizeof(*(pv)->data), \ + &(pv)->cap, &(pv)->size); \ + if (p) { \ + (pv)->data = p; \ + } \ + (bool) p; \ +}); + +#define sc_vector_resize_(pv, newcap) \ +({ \ + bool ok; \ + if ((pv)->cap == (newcap)) { \ + ok = true; \ + } else if ((newcap) > 0) { \ + ok = sc_vector_realloc_(pv, (newcap)); \ + } else { \ + sc_vector_clear(pv); \ + ok = true; \ + } \ + ok; \ +}) + +static inline size_t +sc_vector_growsize_(size_t value) +{ + /* integer multiplication by 1.5 */ + return value + (value >> 1); +} + +/* SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. */ +#define sc_vector_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) + +/** + * Increase the capacity of the vector to at least `mincap` + * + * \param pv a pointer to the vector + * \param mincap (size_t) the requested capacity + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_reserve(pv, mincap) \ +({ \ + bool ok; \ + /* avoid to allocate tiny arrays (< SC_VECTOR_MINCAP_) */ \ + size_t mincap_ = sc_vector_max_(mincap, SC_VECTOR_MINCAP_); \ + if (mincap_ <= (pv)->cap) { \ + /* nothing to do */ \ + ok = true; \ + } else if (mincap_ <= sc_vector_max_cap_(pv)) { \ + /* not too big */ \ + size_t newsize = sc_vector_growsize_((pv)->cap); \ + newsize = sc_vector_clamp_(newsize, mincap_, sc_vector_max_cap_(pv)); \ + ok = sc_vector_realloc_(pv, newsize); \ + } else { \ + ok = false; \ + } \ + ok; \ +}) + +#define sc_vector_shrink_to_fit(pv) \ + /* decreasing the size may not fail */ \ + (void) sc_vector_resize_(pv, (pv)->size) + +/** + * Resize the vector down automatically + * + * Shrink only when necessary (in practice when cap > (size+5)*1.5) + * + * \param pv a pointer to the vector + */ +#define sc_vector_autoshrink(pv) \ +({ \ + bool must_shrink = \ + /* do not shrink to tiny size */ \ + (pv)->cap > SC_VECTOR_MINCAP_ && \ + /* no need to shrink */ \ + (pv)->cap >= sc_vector_growsize_((pv)->size + 5); \ + if (must_shrink) { \ + size_t newsize = sc_vector_max_((pv)->size + 5, SC_VECTOR_MINCAP_); \ + sc_vector_resize_(pv, newsize); \ + } \ +}) + +#define sc_vector_check_same_ptr_type_(a, b) \ + (void) ((a) == (b)) /* warn on type mismatch */ + +/** + * Push an item at the end of the vector + * + * The amortized complexity is O(1). + * + * \param pv a pointer to the vector + * \param item the item to append + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_push(pv, item) \ +({ \ + bool ok = sc_vector_reserve(pv, (pv)->size + 1); \ + if (ok) { \ + (pv)->data[(pv)->size++] = (item); \ + } \ + ok; \ +}) + +/** + * Append `count` items at the end of the vector + * + * \param pv a pointer to the vector + * \param items the items array to append + * \param count the number of items in the array + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_push_all(pv, items, count) \ + sc_vector_push_all_(pv, items, (size_t) count) + +#define sc_vector_push_all_(pv, items, count) \ +({ \ + sc_vector_check_same_ptr_type_((pv)->data, items); \ + bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ + if (ok) { \ + memcpy(&(pv)->data[(pv)->size], items, (count) * sizeof(*(pv)->data)); \ + (pv)->size += count; \ + } \ + ok; \ +}) + +/** + * Insert an hole of size `count` to the given index + * + * The items in range [index; size-1] will be moved. The items in the hole are + * left uninitialized. + * + * \param pv a pointer to the vector + * \param index the index where the hole is to be inserted + * \param count the number of items in the hole + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_insert_hole(pv, index, count) \ + sc_vector_insert_hole_(pv, (size_t) index, (size_t) count); + +#define sc_vector_insert_hole_(pv, index, count) \ +({ \ + bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ + if (ok) { \ + if ((index) < (pv)->size) { \ + memmove(&(pv)->data[(index) + (count)], \ + &(pv)->data[(index)], \ + ((pv)->size - (index)) * sizeof(*(pv)->data)); \ + } \ + (pv)->size += count; \ + } \ + ok; \ +}) + +/** + * Insert an item at the given index + * + * The items in range [index; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index where the item is to be inserted + * \param item the item to append + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_insert(pv, index, item) \ + sc_vector_insert_(pv, (size_t) index, (size_t) item); + +#define sc_vector_insert_(pv, index, item) \ +({ \ + bool ok = sc_vector_insert_hole_(pv, index, 1); \ + if (ok) { \ + (pv)->data[index] = (item); \ + } \ + ok; \ +}) + +/** + * Insert `count` items at the given index + * + * The items in range [index; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index where the items are to be inserted + * \param items the items array to append + * \param count the number of items in the array + * \retval true if no allocation failed + * \retval false on allocation failure (the vector is left untouched) + */ +#define sc_vector_insert_all(pv, index, items, count) \ + sc_vector_insert_all_(pv, (size_t) index, items, (size_t) count) + +#define sc_vector_insert_all_(pv, index, items, count) \ +({ \ + sc_vector_check_same_ptr_type_((pv)->data, items); \ + bool ok = sc_vector_insert_hole_(pv, index, count); \ + if (ok) { \ + memcpy(&(pv)->data[index], items, count * sizeof(*(pv)->data)); \ + } \ + ok; \ +}) + +/** Reverse a char array in place */ +static inline void +sc_char_array_reverse(char *array, size_t len) +{ + for (size_t i = 0; i < len / 2; ++i) + { + char c = array[i]; + array[i] = array[len - i - 1]; + array[len - i - 1] = c; + } +} + +/** + * Right-rotate a (char) array in place + * + * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with + * distance 4 will result in {5, 6, 1, 2, 3, 4}. + * + * Private. + */ +static inline void +sc_char_array_rotate_left(char *array, size_t len, size_t distance) +{ + sc_char_array_reverse(array, distance); + sc_char_array_reverse(&array[distance], len - distance); + sc_char_array_reverse(array, len); +} + +/** + * Right-rotate a (char) array in place + * + * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with + * distance 2 will result in {5, 6, 1, 2, 3, 4}. + * + * Private. + */ +static inline void +sc_char_array_rotate_right(char *array, size_t len, size_t distance) +{ + sc_char_array_rotate_left(array, len, len - distance); +} + +/** + * Move items in a (char) array in place + * + * Move slice [index, count] to target. + */ +static inline void +sc_char_array_move(char *array, size_t idx, size_t count, size_t target) +{ + if (idx < target) { + sc_char_array_rotate_left(&array[idx], target - idx + count, count); + } else { + sc_char_array_rotate_right(&array[target], idx - target + count, count); + } +} + +/** + * Move a slice of items to a given target index + * + * The items in range [index; count] will be moved so that the *new* position + * of the first item is `target`. + * + * \param pv a pointer to the vector + * \param index the index of the first item to move + * \param count the number of items to move + * \param target the new index of the moved slice + */ +#define sc_vector_move_slice(pv, index, count, target) \ + sc_vector_move_slice_(pv, (size_t) index, count, (size_t) target); + +#define sc_vector_move_slice_(pv, index, count, target) \ +({ \ + sc_char_array_move((char *) (pv)->data, \ + (index) * sizeof(*(pv)->data), \ + (count) * sizeof(*(pv)->data), \ + (target) * sizeof(*(pv)->data)); \ +}) + +/** + * Move an item to a given target index + * + * The items will be moved so that its *new* position is `target`. + * + * \param pv a pointer to the vector + * \param index the index of the item to move + * \param target the new index of the moved item + */ +#define sc_vector_move(pv, index, target) \ + sc_vector_move_slice(pv, index, 1, target) + +/** + * Remove a slice of items, without shrinking the array + * + * If you have no good reason to use the _noshrink() version, use + * sc_vector_remove_slice() instead. + * + * The items in range [index+count; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of the first item to remove + * \param count the number of items to remove + */ +#define sc_vector_remove_slice_noshrink(pv, index, count) \ + sc_vector_remove_slice_noshrink_(pv, (size_t) index, (size_t) count) + +#define sc_vector_remove_slice_noshrink_(pv, index, count) \ +({ \ + if ((index) + (count) < (pv)->size) { \ + memmove(&(pv)->data[index], \ + &(pv)->data[(index) + (count)], \ + ((pv)->size - (index) - (count)) * sizeof(*(pv)->data)); \ + } \ + (pv)->size -= count; \ +}) + +/** + * Remove a slice of items + * + * The items in range [index+count; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of the first item to remove + * \param count the number of items to remove + */ +#define sc_vector_remove_slice(pv, index, count) \ +({ \ + sc_vector_remove_slice_noshrink(pv, index, count); \ + sc_vector_autoshrink(pv); \ +}) + +/** + * Remove an item, without shrinking the array + * + * If you have no good reason to use the _noshrink() version, use + * sc_vector_remove() instead. + * + * The items in range [index+1; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of item to remove + */ +#define sc_vector_remove_noshrink(pv, index) \ + sc_vector_remove_slice_noshrink(pv, index, 1) + +/** + * Remove an item + * + * The items in range [index+1; size-1] will be moved. + * + * \param pv a pointer to the vector + * \param index the index of item to remove + */ +#define sc_vector_remove(pv, index) \ +({ \ + sc_vector_remove_noshrink(pv, index); \ + sc_vector_autoshrink(pv); \ +}) + +/** + * Remove an item + * + * The removed item is replaced by the last item of the vector. + * + * This does not preserve ordering, but is O(1). This is useful when the order + * of items is not meaningful. + * + * \param pv a pointer to the vector + * \param index the index of item to remove + */ +#define sc_vector_swap_remove(pv, index) \ + sc_vector_swap_remove_(pv, (size_t) index); + +#define sc_vector_swap_remove_(pv, index) \ +({ \ + (pv)->data[index] = (pv)->data[(pv)->size-1]; \ + (pv)->size--; \ +}); + +/** + * Return the index of an item + * + * Iterate over all items to find a given item. + * + * Use only for vectors of primitive types or pointers. + * + * Return the index, or -1 if not found. + * + * \param pv a pointer to the vector + * \param item the item to find (compared with ==) + */ +#define sc_vector_index_of(pv, item) \ +({ \ + ssize_t idx = -1; \ + for (size_t i = 0; i < (pv)->size; ++i) { \ + if ((pv)->data[i] == (item)) { \ + idx = (ssize_t) i; \ + break; \ + } \ + } \ + idx; \ +}) + +#endif diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index cae3eee9..3b3eb8d0 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,7 +1,9 @@ #include "v4l2_sink.h" +#include + #include "util/log.h" -#include "util/str_util.h" +#include "util/str.h" /** Downcast frame_sink to sc_v4l2_sink */ #define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink) @@ -21,7 +23,7 @@ find_muxer(const char *name) { oformat = av_oformat_next(oformat); #endif // until null or containing the requested name - } while (oformat && !strlist_contains(oformat->name, ',', name)); + } while (oformat && !sc_str_list_contains(oformat->name, ',', name)); return oformat; } @@ -31,7 +33,7 @@ write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) { uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { - LOGC("Could not allocate extradata"); + LOG_OOM(); return false; } @@ -124,7 +126,7 @@ run_v4l2_sink(void *data) { vs->has_frame = false; sc_mutex_unlock(&vs->mutex); - sc_video_buffer_consume(&vs->vb, vs->frame); + sc_frame_buffer_consume(&vs->fb, vs->frame); bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); @@ -139,52 +141,31 @@ run_v4l2_sink(void *data) { return 0; } -static void -sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, - void *userdata) { - (void) vb; - struct sc_v4l2_sink *vs = userdata; - - if (!previous_skipped) { - sc_mutex_lock(&vs->mutex); - vs->has_frame = true; - sc_cond_signal(&vs->cond); - sc_mutex_unlock(&vs->mutex); - } -} - static bool -sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { - static const struct sc_video_buffer_callbacks cbs = { - .on_new_frame = sc_video_buffer_on_new_frame, - }; +sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) { + assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P); + (void) ctx; - bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs); + bool ok = sc_frame_buffer_init(&vs->fb); if (!ok) { - LOGE("Could not initialize video buffer"); return false; } - ok = sc_video_buffer_start(&vs->vb); - if (!ok) { - LOGE("Could not start video buffer"); - goto error_video_buffer_destroy; - } - ok = sc_mutex_init(&vs->mutex); if (!ok) { - LOGC("Could not create mutex"); - goto error_video_buffer_stop_and_join; + goto error_frame_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"); + const AVOutputFormat *format = find_muxer("v4l2"); + if (!format) { + // Alternative name + format = find_muxer("video4linux2"); + } if (!format) { LOGE("Could not find v4l2 muxer"); goto error_cond_destroy; @@ -198,7 +179,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->format_ctx = avformat_alloc_context(); if (!vs->format_ctx) { - LOGE("Could not allocate v4l2 output context"); + LOG_OOM(); return false; } @@ -210,9 +191,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { #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"); + LOG_OOM(); goto error_avformat_free_context; - return false; } #else strncpy(vs->format_ctx->filename, vs->device_name, @@ -221,16 +201,17 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder); if (!ostream) { - LOGE("Could not allocate new v4l2 stream"); + LOG_OOM(); goto error_avformat_free_context; - return false; } - ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + int r = avcodec_parameters_from_context(ostream->codecpar, ctx); + if (r < 0) { + goto error_avformat_free_context; + } + + // The codec is from the v4l2 encoder, not from the decoder 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) { @@ -241,12 +222,12 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->encoder_ctx = avcodec_alloc_context3(encoder); if (!vs->encoder_ctx) { - LOGC("Could not allocate codec context for v4l2"); + LOG_OOM(); goto error_avio_close; } - vs->encoder_ctx->width = vs->frame_size.width; - vs->encoder_ctx->height = vs->frame_size.height; + vs->encoder_ctx->width = ctx->width; + vs->encoder_ctx->height = ctx->height; vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P; vs->encoder_ctx->time_base.num = 1; vs->encoder_ctx->time_base.den = 1; @@ -258,13 +239,13 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->frame = av_frame_alloc(); if (!vs->frame) { - LOGE("Could not create v4l2 frame"); + LOG_OOM(); goto error_avcodec_close; } vs->packet = av_packet_alloc(); if (!vs->packet) { - LOGE("Could not allocate packet"); + LOG_OOM(); goto error_av_frame_free; } @@ -273,9 +254,9 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { vs->stopped = false; LOGD("Starting v4l2 thread"); - ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); + ok = sc_thread_create(&vs->thread, run_v4l2_sink, "scrcpy-v4l2", vs); if (!ok) { - LOGC("Could not start v4l2 thread"); + LOGE("Could not start v4l2 thread"); goto error_av_packet_free; } @@ -299,11 +280,8 @@ error_cond_destroy: sc_cond_destroy(&vs->cond); error_mutex_destroy: sc_mutex_destroy(&vs->mutex); -error_video_buffer_stop_and_join: - sc_video_buffer_stop(&vs->vb); - sc_video_buffer_join(&vs->vb); -error_video_buffer_destroy: - sc_video_buffer_destroy(&vs->vb); +error_frame_buffer_destroy: + sc_frame_buffer_destroy(&vs->fb); return false; } @@ -315,10 +293,7 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { sc_cond_signal(&vs->cond); sc_mutex_unlock(&vs->mutex); - sc_video_buffer_stop(&vs->vb); - sc_thread_join(&vs->thread, NULL); - sc_video_buffer_join(&vs->vb); av_packet_free(&vs->packet); av_frame_free(&vs->frame); @@ -328,18 +303,31 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { avformat_free_context(vs->format_ctx); sc_cond_destroy(&vs->cond); sc_mutex_destroy(&vs->mutex); - sc_video_buffer_destroy(&vs->vb); + sc_frame_buffer_destroy(&vs->fb); } static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { - return sc_video_buffer_push(&vs->vb, frame); + bool previous_skipped; + bool ok = sc_frame_buffer_push(&vs->fb, frame, &previous_skipped); + if (!ok) { + return false; + } + + if (!previous_skipped) { + sc_mutex_lock(&vs->mutex); + vs->has_frame = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + } + + return true; } static bool -sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) { +sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_v4l2_sink *vs = DOWNCAST(sink); - return sc_v4l2_sink_open(vs); + return sc_v4l2_sink_open(vs, ctx); } static void @@ -355,17 +343,13 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { } bool -sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size, sc_tick buffering_time) { +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); return false; } - vs->frame_size = frame_size; - vs->buffering_time = buffering_time; - static const struct sc_frame_sink_ops ops = { .open = sc_v4l2_frame_sink_open, .close = sc_v4l2_frame_sink_close, diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 6773cd26..365a739d 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -3,23 +3,22 @@ #include "common.h" +#include +#include + #include "coords.h" #include "trait/frame_sink.h" -#include "video_buffer.h" +#include "frame_buffer.h" #include "util/tick.h" -#include - struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait - struct sc_video_buffer vb; + struct sc_frame_buffer fb; AVFormatContext *format_ctx; AVCodecContext *encoder_ctx; char *device_name; - struct size frame_size; - sc_tick buffering_time; sc_thread thread; sc_mutex mutex; @@ -33,8 +32,7 @@ struct sc_v4l2_sink { }; bool -sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size, sc_tick buffering_time); +sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); diff --git a/app/src/version.c b/app/src/version.c new file mode 100644 index 00000000..90ea3334 --- /dev/null +++ b/app/src/version.c @@ -0,0 +1,67 @@ +#include "version.h" + +#include +#include +#include +#ifdef HAVE_V4L2 +# include +#endif +#ifdef HAVE_USB +# include +#endif + +void +scrcpy_print_version(void) { + printf("\nDependencies (compiled / linked):\n"); + + SDL_version sdl; + SDL_GetVersion(&sdl); + printf(" - SDL: %u.%u.%u / %u.%u.%u\n", + SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL, + (unsigned) sdl.major, (unsigned) sdl.minor, (unsigned) sdl.patch); + + unsigned avcodec = avcodec_version(); + printf(" - libavcodec: %u.%u.%u / %u.%u.%u\n", + LIBAVCODEC_VERSION_MAJOR, + LIBAVCODEC_VERSION_MINOR, + LIBAVCODEC_VERSION_MICRO, + AV_VERSION_MAJOR(avcodec), + AV_VERSION_MINOR(avcodec), + AV_VERSION_MICRO(avcodec)); + + unsigned avformat = avformat_version(); + printf(" - libavformat: %u.%u.%u / %u.%u.%u\n", + LIBAVFORMAT_VERSION_MAJOR, + LIBAVFORMAT_VERSION_MINOR, + LIBAVFORMAT_VERSION_MICRO, + AV_VERSION_MAJOR(avformat), + AV_VERSION_MINOR(avformat), + AV_VERSION_MICRO(avformat)); + + unsigned avutil = avutil_version(); + printf(" - libavutil: %u.%u.%u / %u.%u.%u\n", + LIBAVUTIL_VERSION_MAJOR, + LIBAVUTIL_VERSION_MINOR, + LIBAVUTIL_VERSION_MICRO, + AV_VERSION_MAJOR(avutil), + AV_VERSION_MINOR(avutil), + AV_VERSION_MICRO(avutil)); + +#ifdef HAVE_V4L2 + unsigned avdevice = avdevice_version(); + printf(" - libavdevice: %u.%u.%u / %u.%u.%u\n", + LIBAVDEVICE_VERSION_MAJOR, + LIBAVDEVICE_VERSION_MINOR, + LIBAVDEVICE_VERSION_MICRO, + AV_VERSION_MAJOR(avdevice), + AV_VERSION_MINOR(avdevice), + AV_VERSION_MICRO(avdevice)); +#endif + +#ifdef HAVE_USB + const struct libusb_version *usb = libusb_get_version(); + // The compiled version may not be known + printf(" - libusb: - / %u.%u.%u\n", + (unsigned) usb->major, (unsigned) usb->minor, (unsigned) usb->micro); +#endif +} diff --git a/app/src/version.h b/app/src/version.h new file mode 100644 index 00000000..920360e8 --- /dev/null +++ b/app/src/version.h @@ -0,0 +1,9 @@ +#ifndef SC_VERSION_H +#define SC_VERSION_H + +#include "common.h" + +void +scrcpy_print_version(void); + +#endif diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c deleted file mode 100644 index f71a4e78..00000000 --- a/app/src/video_buffer.c +++ /dev/null @@ -1,255 +0,0 @@ -#include "video_buffer.h" - -#include -#include - -#include -#include - -#include "util/log.h" - -#define SC_BUFFERING_NDEBUG // comment to debug - -static struct sc_video_buffer_frame * -sc_video_buffer_frame_new(const AVFrame *frame) { - struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame)); - if (!vb_frame) { - return NULL; - } - - vb_frame->frame = av_frame_alloc(); - if (!vb_frame->frame) { - free(vb_frame); - return NULL; - } - - if (av_frame_ref(vb_frame->frame, frame)) { - av_frame_free(&vb_frame->frame); - free(vb_frame); - return NULL; - } - - return vb_frame; -} - -static void -sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) { - av_frame_unref(vb_frame->frame); - av_frame_free(&vb_frame->frame); - free(vb_frame); -} - -static bool -sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) { - bool previous_skipped; - bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped); - if (!ok) { - return false; - } - - vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); - return true; -} - -static int -run_buffering(void *data) { - struct sc_video_buffer *vb = data; - - assert(vb->buffering_time > 0); - - for (;;) { - sc_mutex_lock(&vb->b.mutex); - - while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) { - sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex); - } - - if (vb->b.stopped) { - sc_mutex_unlock(&vb->b.mutex); - goto stopped; - } - - struct sc_video_buffer_frame *vb_frame; - sc_queue_take(&vb->b.queue, next, &vb_frame); - - sc_tick max_deadline = sc_tick_now() + vb->buffering_time; - // PTS (written by the server) are expressed in microseconds - sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts); - - bool timed_out = false; - while (!vb->b.stopped && !timed_out) { - sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts) - + vb->buffering_time; - if (deadline > max_deadline) { - deadline = max_deadline; - } - - timed_out = - !sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline); - } - - if (vb->b.stopped) { - sc_video_buffer_frame_delete(vb_frame); - sc_mutex_unlock(&vb->b.mutex); - goto stopped; - } - - sc_mutex_unlock(&vb->b.mutex); - -#ifndef SC_BUFFERING_NDEBUG - LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, - pts, vb_frame->push_date, sc_tick_now()); -#endif - - sc_video_buffer_offer(vb, vb_frame->frame); - - sc_video_buffer_frame_delete(vb_frame); - } - -stopped: - // Flush queue - while (!sc_queue_is_empty(&vb->b.queue)) { - struct sc_video_buffer_frame *vb_frame; - sc_queue_take(&vb->b.queue, next, &vb_frame); - sc_video_buffer_frame_delete(vb_frame); - } - - LOGD("Buffering thread ended"); - - return 0; -} - -bool -sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, - const struct sc_video_buffer_callbacks *cbs, - void *cbs_userdata) { - bool ok = sc_frame_buffer_init(&vb->fb); - if (!ok) { - return false; - } - - assert(buffering_time >= 0); - if (buffering_time) { - ok = sc_mutex_init(&vb->b.mutex); - if (!ok) { - LOGC("Could not create mutex"); - sc_frame_buffer_destroy(&vb->fb); - return false; - } - - ok = sc_cond_init(&vb->b.queue_cond); - if (!ok) { - LOGC("Could not create cond"); - sc_mutex_destroy(&vb->b.mutex); - sc_frame_buffer_destroy(&vb->fb); - return false; - } - - ok = sc_cond_init(&vb->b.wait_cond); - if (!ok) { - LOGC("Could not create wait cond"); - sc_cond_destroy(&vb->b.queue_cond); - sc_mutex_destroy(&vb->b.mutex); - sc_frame_buffer_destroy(&vb->fb); - return false; - } - - sc_clock_init(&vb->b.clock); - sc_queue_init(&vb->b.queue); - } - - assert(cbs); - assert(cbs->on_new_frame); - - vb->buffering_time = buffering_time; - vb->cbs = cbs; - vb->cbs_userdata = cbs_userdata; - return true; -} - -bool -sc_video_buffer_start(struct sc_video_buffer *vb) { - if (vb->buffering_time) { - bool ok = - sc_thread_create(&vb->b.thread, run_buffering, "buffering", vb); - if (!ok) { - LOGE("Could not start buffering thread"); - return false; - } - } - - return true; -} - -void -sc_video_buffer_stop(struct sc_video_buffer *vb) { - if (vb->buffering_time) { - sc_mutex_lock(&vb->b.mutex); - vb->b.stopped = true; - sc_cond_signal(&vb->b.queue_cond); - sc_cond_signal(&vb->b.wait_cond); - sc_mutex_unlock(&vb->b.mutex); - } -} - -void -sc_video_buffer_join(struct sc_video_buffer *vb) { - if (vb->buffering_time) { - sc_thread_join(&vb->b.thread, NULL); - } -} - -void -sc_video_buffer_destroy(struct sc_video_buffer *vb) { - sc_frame_buffer_destroy(&vb->fb); - if (vb->buffering_time) { - sc_cond_destroy(&vb->b.wait_cond); - sc_cond_destroy(&vb->b.queue_cond); - sc_mutex_destroy(&vb->b.mutex); - } -} - -bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { - if (!vb->buffering_time) { - // No buffering - return sc_video_buffer_offer(vb, frame); - } - - sc_mutex_lock(&vb->b.mutex); - - sc_tick pts = SC_TICK_FROM_US(frame->pts); - sc_clock_update(&vb->b.clock, sc_tick_now(), pts); - sc_cond_signal(&vb->b.wait_cond); - - if (vb->b.clock.count == 1) { - sc_mutex_unlock(&vb->b.mutex); - // First frame, offer it immediately, for two reasons: - // - not to delay the opening of the scrcpy window - // - the buffering estimation needs at least two clock points, so it - // could not handle the first frame - return sc_video_buffer_offer(vb, frame); - } - - struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame); - if (!vb_frame) { - sc_mutex_unlock(&vb->b.mutex); - LOGE("Could not allocate frame"); - return false; - } - -#ifndef SC_BUFFERING_NDEBUG - vb_frame->push_date = sc_tick_now(); -#endif - sc_queue_push(&vb->b.queue, next, vb_frame); - sc_cond_signal(&vb->b.queue_cond); - - sc_mutex_unlock(&vb->b.mutex); - - return true; -} - -void -sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) { - sc_frame_buffer_consume(&vb->fb, dst); -} diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h deleted file mode 100644 index 48777703..00000000 --- a/app/src/video_buffer.h +++ /dev/null @@ -1,76 +0,0 @@ -#ifndef SC_VIDEO_BUFFER_H -#define SC_VIDEO_BUFFER_H - -#include "common.h" - -#include - -#include "clock.h" -#include "frame_buffer.h" -#include "util/queue.h" -#include "util/thread.h" -#include "util/tick.h" - -// forward declarations -typedef struct AVFrame AVFrame; - -struct sc_video_buffer_frame { - AVFrame *frame; - struct sc_video_buffer_frame *next; -#ifndef NDEBUG - sc_tick push_date; -#endif -}; - -struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame); - -struct sc_video_buffer { - struct sc_frame_buffer fb; - - sc_tick buffering_time; - - // only if buffering_time > 0 - struct { - sc_thread thread; - sc_mutex mutex; - sc_cond queue_cond; - sc_cond wait_cond; - - struct sc_clock clock; - struct sc_video_buffer_frame_queue queue; - bool stopped; - } b; // buffering - - const struct sc_video_buffer_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_video_buffer_callbacks { - void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, - void *userdata); -}; - -bool -sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, - const struct sc_video_buffer_callbacks *cbs, - void *cbs_userdata); - -bool -sc_video_buffer_start(struct sc_video_buffer *vb); - -void -sc_video_buffer_stop(struct sc_video_buffer *vb); - -void -sc_video_buffer_join(struct sc_video_buffer *vb); - -void -sc_video_buffer_destroy(struct sc_video_buffer *vb); - -bool -sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame); - -void -sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst); - -#endif diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c new file mode 100644 index 00000000..362b254f --- /dev/null +++ b/app/tests/test_adb_parser.c @@ -0,0 +1,280 @@ +#include "common.h" + +#include + +#include "adb/adb_device.h" +#include "adb/adb_parser.h" + +static void test_adb_devices(void) { + char output[] = + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n" + "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " + "device:MyWifiDevice trandport_id:2\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 2); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + device = &vec.data[1]; + assert(!strcmp("192.168.1.1:5555", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyWifiModel", device->model)); + + sc_adb_devices_destroy(&vec); +} + +static void test_adb_devices_cr(void) { + char output[] = + "List of devices attached\r\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\r\n" + "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " + "device:MyWifiDevice trandport_id:2\r\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 2); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + device = &vec.data[1]; + assert(!strcmp("192.168.1.1:5555", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyWifiModel", device->model)); + + sc_adb_devices_destroy(&vec); +} + +static void test_adb_devices_daemon_start(void) { + char output[] = + "* daemon not running; starting now at tcp:5037\n" + "* daemon started successfully\n" + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_devices_destroy(&vec); +} + +static void test_adb_devices_daemon_start_mixed(void) { + char output[] = + "List of devices attached\n" + "adb server version (41) doesn't match this client (39); killing...\n" + "* daemon started successfully *\n" + "0123456789abcdef unauthorized usb:1-1\n" + "87654321 device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 2); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("unauthorized", device->state)); + assert(!device->model); + + device = &vec.data[1]; + assert(!strcmp("87654321", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_devices_destroy(&vec); +} + +static void test_adb_devices_without_eol(void) { + char output[] = + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_devices_destroy(&vec); +} + +static void test_adb_devices_without_header(void) { + char output[] = + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(!ok); +} + +static void test_adb_devices_corrupted(void) { + char output[] = + "List of devices attached\n" + "corrupted_garbage\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 0); +} + +static void test_adb_devices_spaces(void) { + char output[] = + "List of devices attached\n" + "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("unauthorized", device->state)); + assert(!device->model); + + sc_adb_devices_destroy(&vec); +} + +static void test_get_ip_single_line(void) { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.12.34\r\r\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.12.34")); + free(ip); +} + +static void test_get_ip_single_line_without_eol(void) { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.12.34"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.12.34")); + free(ip); +} + +static void test_get_ip_single_line_with_trailing_space(void) { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.12.34 \n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.12.34")); + free(ip); +} + +static void test_get_ip_multiline_first_ok(void) { + char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.1.2\r\n" + "10.0.0.0/24 dev rmnet proto kernel scope link src " + "10.0.0.2\r\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.1.2")); + free(ip); +} + +static void test_get_ip_multiline_second_ok(void) { + char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " + "10.0.0.3\r\n" + "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.1.3\r\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.1.3")); + free(ip); +} + +static void test_get_ip_multiline_second_ok_without_cr(void) { + char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " + "10.0.0.3\n" + "192.168.1.0/24 dev wlan0 proto kernel scope link src " + "192.168.1.3\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(ip); + assert(!strcmp(ip, "192.168.1.3")); + free(ip); +} + +static void test_get_ip_no_wlan(void) { + char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " + "192.168.12.34\r\r\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(!ip); +} + +static void test_get_ip_no_wlan_without_eol(void) { + char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " + "192.168.12.34"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(!ip); +} + +static void test_get_ip_truncated(void) { + char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " + "\n"; + + char *ip = sc_adb_parse_device_ip(ip_route); + assert(!ip); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_adb_devices(); + test_adb_devices_cr(); + test_adb_devices_daemon_start(); + test_adb_devices_daemon_start_mixed(); + test_adb_devices_without_eol(); + test_adb_devices_without_header(); + test_adb_devices_corrupted(); + test_adb_devices_spaces(); + + test_get_ip_single_line(); + test_get_ip_single_line_without_eol(); + test_get_ip_single_line_with_trailing_space(); + test_get_ip_multiline_first_ok(); + test_get_ip_multiline_second_ok(); + test_get_ip_multiline_second_ok_without_cr(); + test_get_ip_no_wlan(); + test_get_ip_no_wlan_without_eol(); + test_get_ip_truncated(); + + return 0; +} diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c new file mode 100644 index 00000000..94d0f07a --- /dev/null +++ b/app/tests/test_audiobuf.c @@ -0,0 +1,128 @@ +#include "common.h" + +#include +#include + +#include "util/audiobuf.h" + +static void test_audiobuf_simple(void) { + struct sc_audiobuf buf; + uint32_t data[20]; + + bool ok = sc_audiobuf_init(&buf, 4, 20); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5}; + uint32_t w = sc_audiobuf_write(&buf, samples, 5); + assert(w == 5); + + uint32_t r = sc_audiobuf_read(&buf, data, 4); + assert(r == 4); + assert(!memcmp(data, samples, 16)); + + uint32_t samples2[] = {6, 7, 8}; + w = sc_audiobuf_write(&buf, samples2, 3); + assert(w == 3); + + uint32_t single = 9; + w = sc_audiobuf_write(&buf, &single, 1); + assert(w == 1); + + r = sc_audiobuf_read(&buf, &data[4], 8); + assert(r == 5); + + uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + assert(!memcmp(data, expected, 36)); + + sc_audiobuf_destroy(&buf); +} + +static void test_audiobuf_boundaries(void) { + struct sc_audiobuf buf; + uint32_t data[20]; + + bool ok = sc_audiobuf_init(&buf, 4, 20); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5, 6}; + uint32_t w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + uint32_t r = sc_audiobuf_read(&buf, data, 9); + assert(r == 9); + + uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3}; + assert(!memcmp(data, expected, 36)); + + uint32_t samples2[] = {7, 8, 9, 10, 11}; + w = sc_audiobuf_write(&buf, samples2, 5); + assert(w == 5); + + uint32_t single = 12; + w = sc_audiobuf_write(&buf, &single, 1); + assert(w == 1); + + w = sc_audiobuf_read(&buf, NULL, 3); + assert(w == 3); + + assert(sc_audiobuf_can_read(&buf) == 12); + + r = sc_audiobuf_read(&buf, data, 12); + assert(r == 12); + + uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + assert(!memcmp(data, expected2, 48)); + + sc_audiobuf_destroy(&buf); +} + +static void test_audiobuf_partial_read_write(void) { + struct sc_audiobuf buf; + uint32_t data[15]; + + bool ok = sc_audiobuf_init(&buf, 4, 10); + assert(ok); + + uint32_t samples[] = {1, 2, 3, 4, 5, 6}; + uint32_t w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 6); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 4); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 0); + + uint32_t r = sc_audiobuf_read(&buf, data, 3); + assert(r == 3); + + uint32_t expected[] = {1, 2, 3}; + assert(!memcmp(data, expected, 12)); + + w = sc_audiobuf_write(&buf, samples, 6); + assert(w == 3); + + r = sc_audiobuf_read(&buf, data, 15); + assert(r == 10); + uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; + assert(!memcmp(data, expected2, 12)); + + sc_audiobuf_destroy(&buf); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_audiobuf_simple(); + test_audiobuf_boundaries(); + test_audiobuf_partial_read_write(); + + return 0; +} diff --git a/app/tests/test_binary.c b/app/tests/test_binary.c new file mode 100644 index 00000000..82a9c1e0 --- /dev/null +++ b/app/tests/test_binary.c @@ -0,0 +1,114 @@ +#include "common.h" + +#include + +#include "util/binary.h" + +static void test_write16be(void) { + uint16_t val = 0xABCD; + uint8_t buf[2]; + + sc_write16be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); +} + +static void test_write32be(void) { + uint32_t val = 0xABCD1234; + uint8_t buf[4]; + + sc_write32be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); + assert(buf[2] == 0x12); + assert(buf[3] == 0x34); +} + +static void test_write64be(void) { + uint64_t val = 0xABCD1234567890EF; + uint8_t buf[8]; + + sc_write64be(buf, val); + + assert(buf[0] == 0xAB); + assert(buf[1] == 0xCD); + assert(buf[2] == 0x12); + assert(buf[3] == 0x34); + assert(buf[4] == 0x56); + assert(buf[5] == 0x78); + assert(buf[6] == 0x90); + assert(buf[7] == 0xEF); +} + +static void test_read16be(void) { + uint8_t buf[2] = {0xAB, 0xCD}; + + uint16_t val = sc_read16be(buf); + + assert(val == 0xABCD); +} + +static void test_read32be(void) { + uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; + + uint32_t val = sc_read32be(buf); + + assert(val == 0xABCD1234); +} + +static void test_read64be(void) { + uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, + 0x56, 0x78, 0x90, 0xEF}; + + uint64_t val = sc_read64be(buf); + + assert(val == 0xABCD1234567890EF); +} + +static void test_float_to_u16fp(void) { + assert(sc_float_to_u16fp(0.0f) == 0); + assert(sc_float_to_u16fp(0.03125f) == 0x800); + assert(sc_float_to_u16fp(0.0625f) == 0x1000); + assert(sc_float_to_u16fp(0.125f) == 0x2000); + assert(sc_float_to_u16fp(0.25f) == 0x4000); + assert(sc_float_to_u16fp(0.5f) == 0x8000); + assert(sc_float_to_u16fp(0.75f) == 0xc000); + assert(sc_float_to_u16fp(1.0f) == 0xffff); +} + +static void test_float_to_i16fp(void) { + assert(sc_float_to_i16fp(0.0f) == 0); + assert(sc_float_to_i16fp(0.03125f) == 0x400); + assert(sc_float_to_i16fp(0.0625f) == 0x800); + assert(sc_float_to_i16fp(0.125f) == 0x1000); + assert(sc_float_to_i16fp(0.25f) == 0x2000); + assert(sc_float_to_i16fp(0.5f) == 0x4000); + assert(sc_float_to_i16fp(0.75f) == 0x6000); + assert(sc_float_to_i16fp(1.0f) == 0x7fff); + + assert(sc_float_to_i16fp(-0.03125f) == -0x400); + assert(sc_float_to_i16fp(-0.0625f) == -0x800); + assert(sc_float_to_i16fp(-0.125f) == -0x1000); + assert(sc_float_to_i16fp(-0.25f) == -0x2000); + assert(sc_float_to_i16fp(-0.5f) == -0x4000); + assert(sc_float_to_i16fp(-0.75f) == -0x6000); + assert(sc_float_to_i16fp(-1.0f) == -0x8000); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_write16be(); + test_write32be(); + test_write64be(); + test_read16be(); + test_read32be(); + test_read64be(); + + test_float_to_u16fp(); + test_float_to_i16fp(); + return 0; +} diff --git a/app/tests/test_buffer_util.c b/app/tests/test_buffer_util.c deleted file mode 100644 index c7c13bdd..00000000 --- a/app/tests/test_buffer_util.c +++ /dev/null @@ -1,81 +0,0 @@ -#include "common.h" - -#include - -#include "util/buffer_util.h" - -static void test_buffer_write16be(void) { - uint16_t val = 0xABCD; - uint8_t buf[2]; - - buffer_write16be(buf, val); - - assert(buf[0] == 0xAB); - assert(buf[1] == 0xCD); -} - -static void test_buffer_write32be(void) { - uint32_t val = 0xABCD1234; - uint8_t buf[4]; - - buffer_write32be(buf, val); - - assert(buf[0] == 0xAB); - assert(buf[1] == 0xCD); - assert(buf[2] == 0x12); - assert(buf[3] == 0x34); -} - -static void test_buffer_write64be(void) { - uint64_t val = 0xABCD1234567890EF; - uint8_t buf[8]; - - buffer_write64be(buf, val); - - assert(buf[0] == 0xAB); - assert(buf[1] == 0xCD); - assert(buf[2] == 0x12); - assert(buf[3] == 0x34); - assert(buf[4] == 0x56); - assert(buf[5] == 0x78); - assert(buf[6] == 0x90); - assert(buf[7] == 0xEF); -} - -static void test_buffer_read16be(void) { - uint8_t buf[2] = {0xAB, 0xCD}; - - uint16_t val = buffer_read16be(buf); - - assert(val == 0xABCD); -} - -static void test_buffer_read32be(void) { - uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; - - uint32_t val = buffer_read32be(buf); - - assert(val == 0xABCD1234); -} - -static void test_buffer_read64be(void) { - uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, - 0x56, 0x78, 0x90, 0xEF}; - - uint64_t val = buffer_read64be(buf); - - assert(val == 0xABCD1234567890EF); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_buffer_write16be(); - test_buffer_write32be(); - test_buffer_write64be(); - test_buffer_read16be(); - test_buffer_read32be(); - test_buffer_read64be(); - return 0; -} diff --git a/app/tests/test_cbuf.c b/app/tests/test_cbuf.c deleted file mode 100644 index 16674e92..00000000 --- a/app/tests/test_cbuf.c +++ /dev/null @@ -1,78 +0,0 @@ -#include "common.h" - -#include -#include - -#include "util/cbuf.h" - -struct int_queue CBUF(int, 32); - -static void test_cbuf_empty(void) { - struct int_queue queue; - cbuf_init(&queue); - - assert(cbuf_is_empty(&queue)); - - bool push_ok = cbuf_push(&queue, 42); - assert(push_ok); - assert(!cbuf_is_empty(&queue)); - - int item; - bool take_ok = cbuf_take(&queue, &item); - assert(take_ok); - assert(cbuf_is_empty(&queue)); - - bool take_empty_ok = cbuf_take(&queue, &item); - assert(!take_empty_ok); // the queue is empty -} - -static void test_cbuf_full(void) { - struct int_queue queue; - cbuf_init(&queue); - - assert(!cbuf_is_full(&queue)); - - // fill the queue - for (int i = 0; i < 32; ++i) { - bool ok = cbuf_push(&queue, i); - assert(ok); - } - bool ok = cbuf_push(&queue, 42); - assert(!ok); // the queue if full - - int item; - bool take_ok = cbuf_take(&queue, &item); - assert(take_ok); - assert(!cbuf_is_full(&queue)); -} - -static void test_cbuf_push_take(void) { - struct int_queue queue; - cbuf_init(&queue); - - bool push1_ok = cbuf_push(&queue, 42); - assert(push1_ok); - - bool push2_ok = cbuf_push(&queue, 35); - assert(push2_ok); - - int item; - - bool take1_ok = cbuf_take(&queue, &item); - assert(take1_ok); - assert(item == 42); - - bool take2_ok = cbuf_take(&queue, &item); - assert(take2_ok); - assert(item == 35); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_cbuf_empty(); - test_cbuf_full(); - test_cbuf_push_take(); - return 0; -} diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 94740a9a..f2a17272 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -4,11 +4,11 @@ #include #include "cli.h" -#include "scrcpy.h" +#include "options.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -23,7 +23,7 @@ static void test_flag_version(void) { static void test_flag_help(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -38,7 +38,7 @@ static void test_flag_help(void) { static void test_options(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -46,14 +46,14 @@ static void test_options(void) { char *argv[] = { "scrcpy", "--always-on-top", - "--bit-rate", "5M", + "--video-bit-rate", "5M", "--crop", "100:200:300:400", "--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" + // "--no-playback" is not compatible with "--fulscreen" "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", @@ -75,7 +75,7 @@ static void test_options(void) { const struct scrcpy_options *opts = &args.opts; assert(opts->always_on_top); - assert(opts->bit_rate == 5000000); + assert(opts->video_bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); assert(opts->max_fps == 30); @@ -89,7 +89,7 @@ static void test_options(void) { assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); assert(opts->turn_screen_off); - assert(opts->prefer_text); + assert(opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT); assert(!strcmp(opts->window_title, "my device")); assert(opts->window_x == 100); assert(opts->window_y == -1); @@ -100,7 +100,7 @@ static void test_options(void) { static void test_options2(void) { struct scrcpy_cli_args args = { - .opts = SCRCPY_OPTIONS_DEFAULT, + .opts = scrcpy_options_default, .help = false, .version = false, }; @@ -108,8 +108,8 @@ static void test_options2(void) { char *argv[] = { "scrcpy", "--no-control", - "--no-display", - "--record", "file.mp4", // cannot enable --no-display without recording + "--no-playback", + "--record", "file.mp4", // cannot enable --no-playback without recording }; bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); @@ -117,7 +117,8 @@ static void test_options2(void) { const struct scrcpy_options *opts = &args.opts; assert(!opts->control); - assert(!opts->display); + assert(!opts->video_playback); + assert(!opts->audio_playback); assert(!strcmp(opts->record_filename, "file.mp4")); assert(opts->record_format == SC_RECORD_FORMAT_MP4); } @@ -129,25 +130,26 @@ static void test_parse_shortcut_mods(void) { ok = sc_parse_shortcut_mods("lctrl", &mods); assert(ok); assert(mods.count == 1); - assert(mods.data[0] == SC_MOD_LCTRL); + assert(mods.data[0] == SC_SHORTCUT_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)); + assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_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); + assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL); + assert(mods.data[1] == SC_SHORTCUT_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)); + assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER); + assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT)); + assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL | + SC_SHORTCUT_MOD_RALT)); ok = sc_parse_shortcut_mods("", &mods); assert(!ok); @@ -169,4 +171,4 @@ int main(int argc, char *argv[]) { test_options2(); test_parse_shortcut_mods(); return 0; -}; +} diff --git a/app/tests/test_clock.c b/app/tests/test_clock.c deleted file mode 100644 index a88d5800..00000000 --- a/app/tests/test_clock.c +++ /dev/null @@ -1,79 +0,0 @@ -#include "common.h" - -#include - -#include "clock.h" - -void test_small_rolling_sum(void) { - struct sc_clock clock; - sc_clock_init(&clock); - - assert(clock.count == 0); - assert(clock.left_sum.system == 0); - assert(clock.left_sum.stream == 0); - assert(clock.right_sum.system == 0); - assert(clock.right_sum.stream == 0); - - sc_clock_update(&clock, 2, 3); - assert(clock.count == 1); - assert(clock.left_sum.system == 0); - assert(clock.left_sum.stream == 0); - assert(clock.right_sum.system == 2); - assert(clock.right_sum.stream == 3); - - sc_clock_update(&clock, 10, 20); - assert(clock.count == 2); - assert(clock.left_sum.system == 2); - assert(clock.left_sum.stream == 3); - assert(clock.right_sum.system == 10); - assert(clock.right_sum.stream == 20); - - sc_clock_update(&clock, 40, 80); - assert(clock.count == 3); - assert(clock.left_sum.system == 2); - assert(clock.left_sum.stream == 3); - assert(clock.right_sum.system == 50); - assert(clock.right_sum.stream == 100); - - sc_clock_update(&clock, 400, 800); - assert(clock.count == 4); - assert(clock.left_sum.system == 12); - assert(clock.left_sum.stream == 23); - assert(clock.right_sum.system == 440); - assert(clock.right_sum.stream == 880); -} - -void test_large_rolling_sum(void) { - const unsigned half_range = SC_CLOCK_RANGE / 2; - - struct sc_clock clock1; - sc_clock_init(&clock1); - for (unsigned i = 0; i < 5 * half_range; ++i) { - sc_clock_update(&clock1, i, 2 * i + 1); - } - - struct sc_clock clock2; - sc_clock_init(&clock2); - for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) { - sc_clock_update(&clock2, i, 2 * i + 1); - } - - assert(clock1.count == SC_CLOCK_RANGE); - assert(clock2.count == SC_CLOCK_RANGE); - - // The values before the last SC_CLOCK_RANGE points in clock1 should have - // no impact - assert(clock1.left_sum.system == clock2.left_sum.system); - assert(clock1.left_sum.stream == clock2.left_sum.stream); - assert(clock1.right_sum.system == clock2.right_sum.system); - assert(clock1.right_sum.stream == clock2.right_sum.stream); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_small_rolling_sum(); - test_large_rolling_sum(); - return 0; -}; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index ef9247ca..7a978f2b 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -1,13 +1,14 @@ #include "common.h" #include +#include #include #include "control_msg.h" static void test_serialize_inject_keycode(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, @@ -16,12 +17,12 @@ static void test_serialize_inject_keycode(void) { }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 14); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_KEYCODE, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0X05, // repeat @@ -31,19 +32,19 @@ static void test_serialize_inject_keycode(void) { } static void test_serialize_inject_text(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TEXT, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_TEXT, .inject_text = { .text = "hello, world!", }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 18); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_TEXT, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_INJECT_TEXT, 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; @@ -51,34 +52,34 @@ 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_INJECT_TEXT_MAX_LENGTH + 1]; - memset(text, 'a', sizeof(text)); - text[CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; + char text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); - assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); - unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; - expected[0] = CONTROL_MSG_TYPE_INJECT_TEXT; + uint8_t expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; + expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; 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); + memset(&expected[5], 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_touch_event(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, - .pointer_id = 0x1234567887654321L, + .pointer_id = UINT64_C(0x1234567887654321), .position = { .point = { .x = 100, @@ -90,29 +91,31 @@ static void test_serialize_inject_touch_event(void) { }, }, .pressure = 1.0f, + .action_button = AMOTION_EVENT_BUTTON_PRIMARY, .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); - assert(size == 28); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 32); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 0x00, // AKEY_EVENT_ACTION_DOWN 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 0xff, 0xff, // pressure - 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button) + 0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons) }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_scroll_event(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = { .point = { @@ -126,117 +129,125 @@ static void test_serialize_inject_scroll_event(void) { }, .hscroll = 1, .vscroll = -1, + .buttons = 1, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 21); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 + 0x7F, 0xFF, // 1 (float encoded as i16) + 0x80, 0x00, // -1 (float encoded as i16) 0x00, 0x00, 0x00, 0x01, // 1 - 0xFF, 0xFF, 0xFF, 0xFF, // -1 }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_back_or_screen_on(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .back_or_screen_on = { .action = AKEY_EVENT_ACTION_UP, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 0x01, // AKEY_EVENT_ACTION_UP }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_notification_panel(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_settings_panel(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, + const uint8_t expected[] = { + SC_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, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_COLLAPSE_PANELS, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_get_clipboard(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, + .get_clipboard = { + .copy_key = SC_COPY_KEY_COPY, + }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); - assert(size == 1); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 2); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_GET_CLIPBOARD, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, + SC_COPY_KEY_COPY, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_clipboard(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_SET_CLIPBOARD, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { + .sequence = UINT64_C(0x0102030405060708), .paste = true, .text = "hello, world!", }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); - assert(size == 19); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 27); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_SET_CLIPBOARD, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text @@ -244,36 +255,132 @@ static void test_serialize_set_clipboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_set_clipboard_long(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, + .set_clipboard = { + .sequence = UINT64_C(0x0102030405060708), + .paste = true, + .text = NULL, + }, + }; + + char text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; + memset(text, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); + text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; + msg.set_clipboard.text = text; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == SC_CONTROL_MSG_MAX_SIZE); + + uint8_t expected[SC_CONTROL_MSG_MAX_SIZE] = { + SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence + 1, // paste + // text length + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, + (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, + (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, + SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, + }; + memset(expected + 14, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); + + assert(!memcmp(buf, expected, sizeof(expected))); +} + static void test_serialize_set_screen_power_mode(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, .set_screen_power_mode = { - .mode = SCREEN_POWER_MODE_NORMAL, + .mode = SC_SCREEN_POWER_MODE_NORMAL, }, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - 0x02, // SCREEN_POWER_MODE_NORMAL + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + 0x02, // SC_SCREEN_POWER_MODE_NORMAL }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_rotate_device(void) { - struct control_msg msg = { - .type = CONTROL_MSG_TYPE_ROTATE_DEVICE, + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 1); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_uhid_create(void) { + const uint8_t report_desc[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, + .uhid_create = { + .id = 42, + .report_desc_size = sizeof(report_desc), + .report_desc = report_desc, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 16); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_CREATE, + 0, 42, // id + 0, 11, // size + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_uhid_input(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_UHID_INPUT, + .uhid_input = { + .id = 42, + .size = 5, + .data = {1, 2, 3, 4, 5}, + }, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 10); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_UHID_INPUT, + 0, 42, // id + 0, 5, // size + 1, 2, 3, 4, 5, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + +static void test_serialize_open_hard_keyboard(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; - unsigned char buf[CONTROL_MSG_MAX_SIZE]; - size_t size = control_msg_serialize(&msg, buf); + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); - const unsigned char expected[] = { - CONTROL_MSG_TYPE_ROTATE_DEVICE, + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -293,7 +400,11 @@ int main(int argc, char *argv[]) { test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); + test_serialize_set_clipboard_long(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); + test_serialize_uhid_create(); + test_serialize_uhid_input(); + test_serialize_open_hard_keyboard(); return 0; } diff --git a/app/tests/test_device_msg_deserialize.c b/app/tests/test_device_msg_deserialize.c index 3427d640..a64a3eb7 100644 --- a/app/tests/test_device_msg_deserialize.c +++ b/app/tests/test_device_msg_deserialize.c @@ -1,32 +1,32 @@ #include "common.h" #include +#include +#include #include #include "device_msg.h" -#include - static void test_deserialize_clipboard(void) { - const unsigned char input[] = { + const uint8_t input[] = { DEVICE_MSG_TYPE_CLIPBOARD, 0x00, 0x00, 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" }; - struct device_msg msg; - ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); assert(r == 8); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); assert(!strcmp("ABC", msg.clipboard.text)); - device_msg_destroy(&msg); + sc_device_msg_destroy(&msg); } static void test_deserialize_clipboard_big(void) { - unsigned char input[DEVICE_MSG_MAX_SIZE]; + uint8_t 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; @@ -35,8 +35,8 @@ static void test_deserialize_clipboard_big(void) { memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); - struct device_msg msg; - ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); assert(r == DEVICE_MSG_MAX_SIZE); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); @@ -44,7 +44,43 @@ static void test_deserialize_clipboard_big(void) { assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); assert(msg.clipboard.text[0] == 'a'); - device_msg_destroy(&msg); + sc_device_msg_destroy(&msg); +} + +static void test_deserialize_ack_set_clipboard(void) { + const uint8_t input[] = { + DEVICE_MSG_TYPE_ACK_CLIPBOARD, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence + }; + + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); + assert(r == 9); + + assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD); + assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708)); +} + +static void test_deserialize_uhid_output(void) { + const uint8_t input[] = { + DEVICE_MSG_TYPE_UHID_OUTPUT, + 0, 42, // id + 0, 5, // size + 0x01, 0x02, 0x03, 0x04, 0x05, // data + }; + + struct sc_device_msg msg; + ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg); + assert(r == 10); + + assert(msg.type == DEVICE_MSG_TYPE_UHID_OUTPUT); + assert(msg.uhid_output.id == 42); + assert(msg.uhid_output.size == 5); + + uint8_t expected[] = {1, 2, 3, 4, 5}; + assert(!memcmp(msg.uhid_output.data, expected, sizeof(expected))); + + sc_device_msg_destroy(&msg); } int main(int argc, char *argv[]) { @@ -53,5 +89,7 @@ int main(int argc, char *argv[]) { test_deserialize_clipboard(); test_deserialize_clipboard_big(); + test_deserialize_ack_set_clipboard(); + test_deserialize_uhid_output(); return 0; } diff --git a/app/tests/test_orientation.c b/app/tests/test_orientation.c new file mode 100644 index 00000000..153211fa --- /dev/null +++ b/app/tests/test_orientation.c @@ -0,0 +1,91 @@ +#include "common.h" + +#include + +#include "options.h" + +static void test_transforms(void) { + #define O(X) SC_ORIENTATION_ ## X + #define ASSERT_TRANSFORM(SRC, TR, RES) \ + assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES)); + + ASSERT_TRANSFORM(0, 0, 0); + ASSERT_TRANSFORM(0, 90, 90); + ASSERT_TRANSFORM(0, 180, 180); + ASSERT_TRANSFORM(0, 270, 270); + ASSERT_TRANSFORM(0, FLIP_0, FLIP_0); + ASSERT_TRANSFORM(0, FLIP_90, FLIP_90); + ASSERT_TRANSFORM(0, FLIP_180, FLIP_180); + ASSERT_TRANSFORM(0, FLIP_270, FLIP_270); + + ASSERT_TRANSFORM(90, 0, 90); + ASSERT_TRANSFORM(90, 90, 180); + ASSERT_TRANSFORM(90, 180, 270); + ASSERT_TRANSFORM(90, 270, 0); + ASSERT_TRANSFORM(90, FLIP_0, FLIP_270); + ASSERT_TRANSFORM(90, FLIP_90, FLIP_0); + ASSERT_TRANSFORM(90, FLIP_180, FLIP_90); + ASSERT_TRANSFORM(90, FLIP_270, FLIP_180); + + ASSERT_TRANSFORM(180, 0, 180); + ASSERT_TRANSFORM(180, 90, 270); + ASSERT_TRANSFORM(180, 180, 0); + ASSERT_TRANSFORM(180, 270, 90); + ASSERT_TRANSFORM(180, FLIP_0, FLIP_180); + ASSERT_TRANSFORM(180, FLIP_90, FLIP_270); + ASSERT_TRANSFORM(180, FLIP_180, FLIP_0); + ASSERT_TRANSFORM(180, FLIP_270, FLIP_90); + + ASSERT_TRANSFORM(270, 0, 270); + ASSERT_TRANSFORM(270, 90, 0); + ASSERT_TRANSFORM(270, 180, 90); + ASSERT_TRANSFORM(270, 270, 180); + ASSERT_TRANSFORM(270, FLIP_0, FLIP_90); + ASSERT_TRANSFORM(270, FLIP_90, FLIP_180); + ASSERT_TRANSFORM(270, FLIP_180, FLIP_270); + ASSERT_TRANSFORM(270, FLIP_270, FLIP_0); + + ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0); + ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90); + ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180); + ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270); + ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0); + ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90); + ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180); + ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270); + + ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90); + ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180); + ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270); + ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0); + ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270); + ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0); + ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90); + ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180); + + ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180); + ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270); + ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0); + ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90); + ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180); + ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270); + ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0); + ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90); + + ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270); + ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0); + ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90); + ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180); + ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90); + ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180); + ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270); + ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_transforms(); + return 0; +} diff --git a/app/tests/test_queue.c b/app/tests/test_queue.c deleted file mode 100644 index d8b2b4ec..00000000 --- a/app/tests/test_queue.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "common.h" - -#include - -#include "util/queue.h" - -struct foo { - int value; - struct foo *next; -}; - -static void test_queue(void) { - struct my_queue SC_QUEUE(struct foo) queue; - sc_queue_init(&queue); - - assert(sc_queue_is_empty(&queue)); - - struct foo v1 = { .value = 42 }; - struct foo v2 = { .value = 27 }; - - sc_queue_push(&queue, next, &v1); - sc_queue_push(&queue, next, &v2); - - struct foo *foo; - - assert(!sc_queue_is_empty(&queue)); - sc_queue_take(&queue, next, &foo); - assert(foo->value == 42); - - assert(!sc_queue_is_empty(&queue)); - sc_queue_take(&queue, next, &foo); - assert(foo->value == 27); - - assert(sc_queue_is_empty(&queue)); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_queue(); - return 0; -} diff --git a/app/tests/test_str.c b/app/tests/test_str.c new file mode 100644 index 00000000..5d365ef5 --- /dev/null +++ b/app/tests/test_str.c @@ -0,0 +1,401 @@ +#include "common.h" + +#include +#include +#include +#include +#include + +#include "util/str.h" + +static void test_strncpy_simple(void) { + char s[] = "xxxxxxxxxx"; + size_t w = sc_strncpy(s, "abcdef", sizeof(s)); + + // returns strlen of copied string + assert(w == 6); + + // is nul-terminated + assert(s[6] == '\0'); + + // does not write useless bytes + assert(s[7] == 'x'); + + // copies the content as expected + assert(!strcmp("abcdef", s)); +} + +static void test_strncpy_just_fit(void) { + char s[] = "xxxxxx"; + size_t w = sc_strncpy(s, "abcdef", sizeof(s)); + + // returns strlen of copied string + assert(w == 6); + + // is nul-terminated + assert(s[6] == '\0'); + + // copies the content as expected + assert(!strcmp("abcdef", s)); +} + +static void test_strncpy_truncated(void) { + char s[] = "xxx"; + size_t w = sc_strncpy(s, "abcdef", sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 4); + + // is nul-terminated + assert(s[3] == '\0'); + + // copies the content as expected + assert(!strncmp("abcdef", s, 3)); +} + +static void test_join_simple(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxxxxxxxxxx"; + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); + + // returns strlen of concatenation + assert(w == 11); + + // is nul-terminated + assert(s[11] == '\0'); + + // does not write useless bytes + assert(s[12] == 'x'); + + // copies the content as expected + assert(!strcmp("abc de fghi", s)); +} + +static void test_join_just_fit(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxxxxxxx"; + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); + + // returns strlen of concatenation + assert(w == 11); + + // is nul-terminated + assert(s[11] == '\0'); + + // copies the content as expected + assert(!strcmp("abc de fghi", s)); +} + +static void test_join_truncated_in_token(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxx"; + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 6); + + // is nul-terminated + assert(s[5] == '\0'); + + // copies the content as expected + assert(!strcmp("abc d", s)); +} + +static void test_join_truncated_before_sep(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxx"; + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 7); + + // is nul-terminated + assert(s[6] == '\0'); + + // copies the content as expected + assert(!strcmp("abc de", s)); +} + +static void test_join_truncated_after_sep(void) { + const char *const tokens[] = { "abc", "de", "fghi", NULL }; + char s[] = "xxxxxxx"; + size_t w = sc_str_join(s, tokens, ' ', sizeof(s)); + + // returns 'n' (sizeof(s)) + assert(w == 8); + + // is nul-terminated + assert(s[7] == '\0'); + + // copies the content as expected + assert(!strcmp("abc de ", s)); +} + +static void test_quote(void) { + const char *s = "abcde"; + char *out = sc_str_quote(s); + + // add '"' at the beginning and the end + assert(!strcmp("\"abcde\"", out)); + + free(out); +} + +static void test_utf8_truncate(void) { + const char *s = "aÉbÔc"; + assert(strlen(s) == 7); // É and Ô are 2 bytes-wide + + size_t count; + + count = sc_str_utf8_truncation_index(s, 1); + assert(count == 1); + + count = sc_str_utf8_truncation_index(s, 2); + assert(count == 1); // É is 2 bytes-wide + + count = sc_str_utf8_truncation_index(s, 3); + assert(count == 3); + + count = sc_str_utf8_truncation_index(s, 4); + assert(count == 4); + + count = sc_str_utf8_truncation_index(s, 5); + assert(count == 4); // Ô is 2 bytes-wide + + count = sc_str_utf8_truncation_index(s, 6); + assert(count == 6); + + count = sc_str_utf8_truncation_index(s, 7); + assert(count == 7); + + count = sc_str_utf8_truncation_index(s, 8); + assert(count == 7); // no more chars +} + +static void test_parse_integer(void) { + long value; + bool ok = sc_str_parse_integer("1234", &value); + assert(ok); + assert(value == 1234); + + ok = sc_str_parse_integer("-1234", &value); + assert(ok); + assert(value == -1234); + + ok = sc_str_parse_integer("1234k", &value); + assert(!ok); + + ok = sc_str_parse_integer("123456789876543212345678987654321", &value); + assert(!ok); // out-of-range +} + +static void test_parse_integers(void) { + long values[5]; + + size_t count = sc_str_parse_integers("1234", ':', 5, values); + assert(count == 1); + assert(values[0] == 1234); + + count = sc_str_parse_integers("1234:5678", ':', 5, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = sc_str_parse_integers("1234:5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == 5678); + + count = sc_str_parse_integers("1234:-5678", ':', 2, values); + assert(count == 2); + assert(values[0] == 1234); + assert(values[1] == -5678); + + count = sc_str_parse_integers("1:2:3:4:5", ':', 5, values); + assert(count == 5); + assert(values[0] == 1); + assert(values[1] == 2); + assert(values[2] == 3); + assert(values[3] == 4); + assert(values[4] == 5); + + count = sc_str_parse_integers("1234:5678", ':', 1, values); + assert(count == 0); // max_items == 1 + + count = sc_str_parse_integers("1:2:3:4:5", ':', 3, values); + assert(count == 0); // max_items == 3 + + count = sc_str_parse_integers(":1234", ':', 5, values); + assert(count == 0); // invalid + + count = sc_str_parse_integers("1234:", ':', 5, values); + assert(count == 0); // invalid + + count = sc_str_parse_integers("1234:", ':', 1, values); + assert(count == 0); // invalid, even when max_items == 1 + + count = sc_str_parse_integers("1234::5678", ':', 5, values); + assert(count == 0); // invalid +} + +static void test_parse_integer_with_suffix(void) { + long value; + bool ok = sc_str_parse_integer_with_suffix("1234", &value); + assert(ok); + assert(value == 1234); + + ok = sc_str_parse_integer_with_suffix("-1234", &value); + assert(ok); + assert(value == -1234); + + ok = sc_str_parse_integer_with_suffix("1234k", &value); + assert(ok); + assert(value == 1234000); + + ok = sc_str_parse_integer_with_suffix("1234m", &value); + assert(ok); + assert(value == 1234000000); + + ok = sc_str_parse_integer_with_suffix("-1234k", &value); + assert(ok); + assert(value == -1234000); + + ok = sc_str_parse_integer_with_suffix("-1234m", &value); + assert(ok); + assert(value == -1234000000); + + ok = sc_str_parse_integer_with_suffix("123456789876543212345678987654321", &value); + assert(!ok); // out-of-range + + char buf[32]; + + int r = snprintf(buf, sizeof(buf), "%ldk", LONG_MAX / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); + ok = sc_str_parse_integer_with_suffix(buf, &value); + assert(ok); + assert(value == LONG_MAX / 2000 * 1000); + + r = snprintf(buf, sizeof(buf), "%ldm", LONG_MAX / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); + ok = sc_str_parse_integer_with_suffix(buf, &value); + assert(!ok); + + r = snprintf(buf, sizeof(buf), "%ldk", LONG_MIN / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); + ok = sc_str_parse_integer_with_suffix(buf, &value); + assert(ok); + assert(value == LONG_MIN / 2000 * 1000); + + r = snprintf(buf, sizeof(buf), "%ldm", LONG_MIN / 2000); + assert(r >= 0 && (size_t) r < sizeof(buf)); + ok = sc_str_parse_integer_with_suffix(buf, &value); + assert(!ok); +} + +static void test_strlist_contains(void) { + assert(sc_str_list_contains("a,bc,def", ',', "bc")); + assert(!sc_str_list_contains("a,bc,def", ',', "b")); + assert(sc_str_list_contains("", ',', "")); + assert(sc_str_list_contains("abc,", ',', "")); + assert(sc_str_list_contains(",abc", ',', "")); + assert(sc_str_list_contains("abc,,def", ',', "")); + assert(!sc_str_list_contains("abc", ',', "")); + assert(sc_str_list_contains(",,|x", '|', ",,")); + assert(sc_str_list_contains("xyz", '\0', "xyz")); +} + +static void test_wrap_lines(void) { + const char *s = "This is a text to test line wrapping. The lines must be " + "wrapped at a space or a line break.\n" + "\n" + "This rectangle must remains a rectangle because it is " + "drawn in lines having lengths lower than the specified " + "number of columns:\n" + " +----+\n" + " | |\n" + " +----+\n"; + + // |---- 1 1 2 2| + // |0 5 0 5 0 3| <-- 24 columns + const char *expected = " This is a text to\n" + " test line wrapping.\n" + " The lines must be\n" + " wrapped at a space\n" + " or a line break.\n" + " \n" + " This rectangle must\n" + " remains a rectangle\n" + " because it is drawn\n" + " in lines having\n" + " lengths lower than\n" + " the specified number\n" + " of columns:\n" + " +----+\n" + " | |\n" + " +----+\n"; + + char *formatted = sc_str_wrap_lines(s, 24, 4); + assert(formatted); + + assert(!strcmp(formatted, expected)); + + free(formatted); +} + +static void test_index_of_column(void) { + assert(sc_str_index_of_column("a bc d", 0, " ") == 0); + assert(sc_str_index_of_column("a bc d", 1, " ") == 2); + assert(sc_str_index_of_column("a bc d", 2, " ") == 6); + assert(sc_str_index_of_column("a bc d", 3, " ") == -1); + + assert(sc_str_index_of_column("a ", 0, " ") == 0); + assert(sc_str_index_of_column("a ", 1, " ") == -1); + + assert(sc_str_index_of_column("", 0, " ") == 0); + assert(sc_str_index_of_column("", 1, " ") == -1); + + assert(sc_str_index_of_column("a \t \t bc \t d\t", 0, " \t") == 0); + assert(sc_str_index_of_column("a \t \t bc \t d\t", 1, " \t") == 8); + assert(sc_str_index_of_column("a \t \t bc \t d\t", 2, " \t") == 15); + assert(sc_str_index_of_column("a \t \t bc \t d\t", 3, " \t") == -1); + + assert(sc_str_index_of_column(" a bc d", 1, " ") == 2); +} + +static void test_remove_trailing_cr(void) { + char s[] = "abc\r"; + sc_str_remove_trailing_cr(s, sizeof(s) - 1); + assert(!strcmp(s, "abc")); + + char s2[] = "def\r\r\r\r"; + sc_str_remove_trailing_cr(s2, sizeof(s2) - 1); + assert(!strcmp(s2, "def")); + + char s3[] = "adb\rdef\r"; + sc_str_remove_trailing_cr(s3, sizeof(s3) - 1); + assert(!strcmp(s3, "adb\rdef")); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_strncpy_simple(); + test_strncpy_just_fit(); + test_strncpy_truncated(); + test_join_simple(); + test_join_just_fit(); + test_join_truncated_in_token(); + test_join_truncated_before_sep(); + test_join_truncated_after_sep(); + test_quote(); + test_utf8_truncate(); + test_parse_integer(); + test_parse_integers(); + test_parse_integer_with_suffix(); + test_strlist_contains(); + test_wrap_lines(); + test_index_of_column(); + test_remove_trailing_cr(); + return 0; +} diff --git a/app/tests/test_strbuf.c b/app/tests/test_strbuf.c new file mode 100644 index 00000000..58562522 --- /dev/null +++ b/app/tests/test_strbuf.c @@ -0,0 +1,48 @@ +#include "common.h" + +#include +#include +#include +#include + +#include "util/strbuf.h" + +static void test_strbuf_simple(void) { + struct sc_strbuf buf; + bool ok = sc_strbuf_init(&buf, 10); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "Hello"); + assert(ok); + + ok = sc_strbuf_append_char(&buf, ' '); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "world"); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "!\n"); + assert(ok); + + ok = sc_strbuf_append_staticstr(&buf, "This is a test"); + assert(ok); + + ok = sc_strbuf_append_n(&buf, '.', 3); + assert(ok); + + assert(!strcmp(buf.s, "Hello world!\nThis is a test...")); + + sc_strbuf_shrink(&buf); + assert(buf.len == buf.cap); + assert(!strcmp(buf.s, "Hello world!\nThis is a test...")); + + free(buf.s); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_strbuf_simple(); + return 0; +} diff --git a/app/tests/test_strutil.c b/app/tests/test_strutil.c deleted file mode 100644 index dfd99658..00000000 --- a/app/tests/test_strutil.c +++ /dev/null @@ -1,321 +0,0 @@ -#include "common.h" - -#include -#include -#include -#include - -#include "util/str_util.h" - -static void test_xstrncpy_simple(void) { - char s[] = "xxxxxxxxxx"; - size_t w = xstrncpy(s, "abcdef", sizeof(s)); - - // returns strlen of copied string - assert(w == 6); - - // is nul-terminated - assert(s[6] == '\0'); - - // does not write useless bytes - assert(s[7] == 'x'); - - // copies the content as expected - assert(!strcmp("abcdef", s)); -} - -static void test_xstrncpy_just_fit(void) { - char s[] = "xxxxxx"; - size_t w = xstrncpy(s, "abcdef", sizeof(s)); - - // returns strlen of copied string - assert(w == 6); - - // is nul-terminated - assert(s[6] == '\0'); - - // copies the content as expected - assert(!strcmp("abcdef", s)); -} - -static void test_xstrncpy_truncated(void) { - char s[] = "xxx"; - size_t w = xstrncpy(s, "abcdef", sizeof(s)); - - // returns 'n' (sizeof(s)) - assert(w == 4); - - // is nul-terminated - assert(s[3] == '\0'); - - // copies the content as expected - assert(!strncmp("abcdef", s, 3)); -} - -static void test_xstrjoin_simple(void) { - const char *const tokens[] = { "abc", "de", "fghi", NULL }; - char s[] = "xxxxxxxxxxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); - - // returns strlen of concatenation - assert(w == 11); - - // is nul-terminated - assert(s[11] == '\0'); - - // does not write useless bytes - assert(s[12] == 'x'); - - // copies the content as expected - assert(!strcmp("abc de fghi", s)); -} - -static void test_xstrjoin_just_fit(void) { - const char *const tokens[] = { "abc", "de", "fghi", NULL }; - char s[] = "xxxxxxxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); - - // returns strlen of concatenation - assert(w == 11); - - // is nul-terminated - assert(s[11] == '\0'); - - // copies the content as expected - assert(!strcmp("abc de fghi", s)); -} - -static void test_xstrjoin_truncated_in_token(void) { - const char *const tokens[] = { "abc", "de", "fghi", NULL }; - char s[] = "xxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); - - // returns 'n' (sizeof(s)) - assert(w == 6); - - // is nul-terminated - assert(s[5] == '\0'); - - // copies the content as expected - assert(!strcmp("abc d", s)); -} - -static void test_xstrjoin_truncated_before_sep(void) { - const char *const tokens[] = { "abc", "de", "fghi", NULL }; - char s[] = "xxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); - - // returns 'n' (sizeof(s)) - assert(w == 7); - - // is nul-terminated - assert(s[6] == '\0'); - - // copies the content as expected - assert(!strcmp("abc de", s)); -} - -static void test_xstrjoin_truncated_after_sep(void) { - const char *const tokens[] = { "abc", "de", "fghi", NULL }; - char s[] = "xxxxxxx"; - size_t w = xstrjoin(s, tokens, ' ', sizeof(s)); - - // returns 'n' (sizeof(s)) - assert(w == 8); - - // is nul-terminated - assert(s[7] == '\0'); - - // copies the content as expected - assert(!strcmp("abc de ", s)); -} - -static void test_strquote(void) { - const char *s = "abcde"; - char *out = strquote(s); - - // add '"' at the beginning and the end - assert(!strcmp("\"abcde\"", out)); - - free(out); -} - -static void test_utf8_truncate(void) { - const char *s = "aÉbÔc"; - assert(strlen(s) == 7); // É and Ô are 2 bytes-wide - - size_t count; - - count = utf8_truncation_index(s, 1); - assert(count == 1); - - count = utf8_truncation_index(s, 2); - assert(count == 1); // É is 2 bytes-wide - - count = utf8_truncation_index(s, 3); - assert(count == 3); - - count = utf8_truncation_index(s, 4); - assert(count == 4); - - count = utf8_truncation_index(s, 5); - assert(count == 4); // Ô is 2 bytes-wide - - count = utf8_truncation_index(s, 6); - assert(count == 6); - - count = utf8_truncation_index(s, 7); - assert(count == 7); - - count = utf8_truncation_index(s, 8); - assert(count == 7); // no more chars -} - -static void test_parse_integer(void) { - long value; - bool ok = parse_integer("1234", &value); - assert(ok); - assert(value == 1234); - - ok = parse_integer("-1234", &value); - assert(ok); - assert(value == -1234); - - ok = parse_integer("1234k", &value); - assert(!ok); - - ok = parse_integer("123456789876543212345678987654321", &value); - assert(!ok); // out-of-range -} - -static void test_parse_integers(void) { - long values[5]; - - size_t count = parse_integers("1234", ':', 5, values); - assert(count == 1); - assert(values[0] == 1234); - - count = parse_integers("1234:5678", ':', 5, values); - assert(count == 2); - assert(values[0] == 1234); - assert(values[1] == 5678); - - count = parse_integers("1234:5678", ':', 2, values); - assert(count == 2); - assert(values[0] == 1234); - assert(values[1] == 5678); - - count = parse_integers("1234:-5678", ':', 2, values); - assert(count == 2); - assert(values[0] == 1234); - assert(values[1] == -5678); - - count = parse_integers("1:2:3:4:5", ':', 5, values); - assert(count == 5); - assert(values[0] == 1); - assert(values[1] == 2); - assert(values[2] == 3); - assert(values[3] == 4); - assert(values[4] == 5); - - count = parse_integers("1234:5678", ':', 1, values); - assert(count == 0); // max_items == 1 - - count = parse_integers("1:2:3:4:5", ':', 3, values); - assert(count == 0); // max_items == 3 - - count = parse_integers(":1234", ':', 5, values); - assert(count == 0); // invalid - - count = parse_integers("1234:", ':', 5, values); - assert(count == 0); // invalid - - count = parse_integers("1234:", ':', 1, values); - assert(count == 0); // invalid, even when max_items == 1 - - count = parse_integers("1234::5678", ':', 5, values); - assert(count == 0); // invalid -} - -static void test_parse_integer_with_suffix(void) { - long value; - bool ok = parse_integer_with_suffix("1234", &value); - assert(ok); - assert(value == 1234); - - ok = parse_integer_with_suffix("-1234", &value); - assert(ok); - assert(value == -1234); - - ok = parse_integer_with_suffix("1234k", &value); - assert(ok); - assert(value == 1234000); - - ok = parse_integer_with_suffix("1234m", &value); - assert(ok); - assert(value == 1234000000); - - ok = parse_integer_with_suffix("-1234k", &value); - assert(ok); - assert(value == -1234000); - - ok = parse_integer_with_suffix("-1234m", &value); - assert(ok); - assert(value == -1234000000); - - ok = parse_integer_with_suffix("123456789876543212345678987654321", &value); - assert(!ok); // out-of-range - - char buf[32]; - - sprintf(buf, "%ldk", LONG_MAX / 2000); - ok = parse_integer_with_suffix(buf, &value); - assert(ok); - assert(value == LONG_MAX / 2000 * 1000); - - sprintf(buf, "%ldm", LONG_MAX / 2000); - ok = parse_integer_with_suffix(buf, &value); - assert(!ok); - - sprintf(buf, "%ldk", LONG_MIN / 2000); - ok = parse_integer_with_suffix(buf, &value); - assert(ok); - assert(value == LONG_MIN / 2000 * 1000); - - sprintf(buf, "%ldm", LONG_MIN / 2000); - ok = parse_integer_with_suffix(buf, &value); - assert(!ok); -} - -static void test_strlist_contains(void) { - assert(strlist_contains("a,bc,def", ',', "bc")); - assert(!strlist_contains("a,bc,def", ',', "b")); - assert(strlist_contains("", ',', "")); - assert(strlist_contains("abc,", ',', "")); - assert(strlist_contains(",abc", ',', "")); - assert(strlist_contains("abc,,def", ',', "")); - assert(!strlist_contains("abc", ',', "")); - assert(strlist_contains(",,|x", '|', ",,")); - assert(strlist_contains("xyz", '\0', "xyz")); -} - -int main(int argc, char *argv[]) { - (void) argc; - (void) argv; - - test_xstrncpy_simple(); - test_xstrncpy_just_fit(); - test_xstrncpy_truncated(); - test_xstrjoin_simple(); - test_xstrjoin_just_fit(); - test_xstrjoin_truncated_in_token(); - test_xstrjoin_truncated_before_sep(); - test_xstrjoin_truncated_after_sep(); - test_strquote(); - test_utf8_truncate(); - test_parse_integer(); - test_parse_integers(); - test_parse_integer_with_suffix(); - test_strlist_contains(); - return 0; -} diff --git a/app/tests/test_vecdeque.c b/app/tests/test_vecdeque.c new file mode 100644 index 00000000..44d33560 --- /dev/null +++ b/app/tests/test_vecdeque.c @@ -0,0 +1,197 @@ +#include "common.h" + +#include + +#include "util/vecdeque.h" + +#define pr(pv) \ +({ \ + fprintf(stderr, "cap=%lu origin=%lu size=%lu\n", (pv)->cap, (pv)->origin, (pv)->size); \ + for (size_t i = 0; i < (pv)->cap; ++i) \ + fprintf(stderr, "%d ", (pv)->data[i]); \ + fprintf(stderr, "\n"); \ +}) + +static void test_vecdeque_push_pop(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + assert(sc_vecdeque_is_empty(&vdq)); + assert(sc_vecdeque_size(&vdq) == 0); + + bool ok = sc_vecdeque_push(&vdq, 5); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 1); + + ok = sc_vecdeque_push(&vdq, 12); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 2); + + int v = sc_vecdeque_pop(&vdq); + assert(v == 5); + assert(sc_vecdeque_size(&vdq) == 1); + + ok = sc_vecdeque_push(&vdq, 7); + assert(ok); + assert(sc_vecdeque_size(&vdq) == 2); + + int *p = sc_vecdeque_popref(&vdq); + assert(p); + assert(*p == 12); + assert(sc_vecdeque_size(&vdq) == 1); + + v = sc_vecdeque_pop(&vdq); + assert(v == 7); + assert(sc_vecdeque_size(&vdq) == 0); + assert(sc_vecdeque_is_empty(&vdq)); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_reserve(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (size_t i = 0; i < 20; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 20); + + // It is now full + + for (int i = 0; i < 5; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + assert(sc_vecdeque_size(&vdq) == 15); + + for (int i = 20; i < 25; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 20); + assert(vdq.cap == 20); + + // Now, the content wraps around the ring buffer: + // 20 21 22 23 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + // ^ + // origin + + // It is now full, let's reserve some space + ok = sc_vecdeque_reserve(&vdq, 30); + assert(ok); + assert(vdq.cap == 30); + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 0; i < 20; ++i) { + // We should retrieve the items we inserted in order + int v = sc_vecdeque_pop(&vdq); + assert(v == i + 5); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_grow(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (int i = 0; i < 500; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 500); + + for (int i = 0; i < 100; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + + assert(sc_vecdeque_size(&vdq) == 400); + + for (int i = 500; i < 1000; ++i) { + ok = sc_vecdeque_push(&vdq, i); + assert(ok); + } + + assert(sc_vecdeque_size(&vdq) == 900); + + for (int i = 100; i < 1000; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +static void test_vecdeque_push_hole(void) { + struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; + + bool ok = sc_vecdeque_reserve(&vdq, 20); + assert(ok); + assert(vdq.cap == 20); + + assert(sc_vecdeque_size(&vdq) == 0); + + for (int i = 0; i < 20; ++i) { + int *p = sc_vecdeque_push_hole(&vdq); + assert(p); + *p = i * 10; + } + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 0; i < 10; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i * 10); + } + + assert(sc_vecdeque_size(&vdq) == 10); + + for (int i = 20; i < 30; ++i) { + int *p = sc_vecdeque_push_hole(&vdq); + assert(p); + *p = i * 10; + } + + assert(sc_vecdeque_size(&vdq) == 20); + + for (int i = 10; i < 30; ++i) { + int v = sc_vecdeque_pop(&vdq); + assert(v == i * 10); + } + + assert(sc_vecdeque_size(&vdq) == 0); + + sc_vecdeque_destroy(&vdq); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_vecdeque_push_pop(); + test_vecdeque_reserve(); + test_vecdeque_grow(); + test_vecdeque_push_hole(); + + return 0; +} diff --git a/app/tests/test_vector.c b/app/tests/test_vector.c new file mode 100644 index 00000000..459b4e0f --- /dev/null +++ b/app/tests/test_vector.c @@ -0,0 +1,421 @@ +#include "common.h" + +#include + +#include "util/vector.h" + +static void test_vector_insert_remove(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_push(&vec, 42); + assert(ok); + assert(vec.data[0] == 42); + assert(vec.size == 1); + + ok = sc_vector_push(&vec, 37); + assert(ok); + assert(vec.size == 2); + assert(vec.data[0] == 42); + assert(vec.data[1] == 37); + + ok = sc_vector_insert(&vec, 1, 100); + assert(ok); + assert(vec.size == 3); + assert(vec.data[0] == 42); + assert(vec.data[1] == 100); + assert(vec.data[2] == 37); + + ok = sc_vector_push(&vec, 77); + assert(ok); + assert(vec.size == 4); + assert(vec.data[0] == 42); + assert(vec.data[1] == 100); + assert(vec.data[2] == 37); + assert(vec.data[3] == 77); + + sc_vector_remove(&vec, 1); + assert(vec.size == 3); + assert(vec.data[0] == 42); + assert(vec.data[1] == 37); + assert(vec.data[2] == 77); + + sc_vector_clear(&vec); + assert(vec.size == 0); + + sc_vector_destroy(&vec); +} + +static void test_vector_push_array(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + bool ok; + + ok = sc_vector_push(&vec, 3); assert(ok); + ok = sc_vector_push(&vec, 14); assert(ok); + ok = sc_vector_push(&vec, 15); assert(ok); + ok = sc_vector_push(&vec, 92); assert(ok); + ok = sc_vector_push(&vec, 65); assert(ok); + assert(vec.size == 5); + + int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + ok = sc_vector_push_all(&vec, items, 8); + + assert(ok); + assert(vec.size == 13); + assert(vec.data[0] == 3); + assert(vec.data[1] == 14); + assert(vec.data[2] == 15); + assert(vec.data[3] == 92); + assert(vec.data[4] == 65); + assert(vec.data[5] == 1); + assert(vec.data[6] == 2); + assert(vec.data[7] == 3); + assert(vec.data[8] == 4); + assert(vec.data[9] == 5); + assert(vec.data[10] == 6); + assert(vec.data[11] == 7); + assert(vec.data[12] == 8); + + sc_vector_destroy(&vec); +} + +static void test_vector_insert_array(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + bool ok; + + ok = sc_vector_push(&vec, 3); assert(ok); + ok = sc_vector_push(&vec, 14); assert(ok); + ok = sc_vector_push(&vec, 15); assert(ok); + ok = sc_vector_push(&vec, 92); assert(ok); + ok = sc_vector_push(&vec, 65); assert(ok); + assert(vec.size == 5); + + int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + ok = sc_vector_insert_all(&vec, 3, items, 8); + assert(ok); + assert(vec.size == 13); + assert(vec.data[0] == 3); + assert(vec.data[1] == 14); + assert(vec.data[2] == 15); + assert(vec.data[3] == 1); + assert(vec.data[4] == 2); + assert(vec.data[5] == 3); + assert(vec.data[6] == 4); + assert(vec.data[7] == 5); + assert(vec.data[8] == 6); + assert(vec.data[9] == 7); + assert(vec.data[10] == 8); + assert(vec.data[11] == 92); + assert(vec.data[12] == 65); + + sc_vector_destroy(&vec); +} + +static void test_vector_remove_slice(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + for (int i = 0; i < 100; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + } + + assert(vec.size == 100); + + sc_vector_remove_slice(&vec, 32, 60); + assert(vec.size == 40); + assert(vec.data[31] == 31); + assert(vec.data[32] == 92); + + sc_vector_destroy(&vec); +} + +static void test_vector_swap_remove(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_push(&vec, 3); assert(ok); + ok = sc_vector_push(&vec, 14); assert(ok); + ok = sc_vector_push(&vec, 15); assert(ok); + ok = sc_vector_push(&vec, 92); assert(ok); + ok = sc_vector_push(&vec, 65); assert(ok); + assert(vec.size == 5); + + sc_vector_swap_remove(&vec, 1); + assert(vec.size == 4); + assert(vec.data[0] == 3); + assert(vec.data[1] == 65); + assert(vec.data[2] == 15); + assert(vec.data[3] == 92); + + sc_vector_destroy(&vec); +} + +static void test_vector_index_of(void) { + struct SC_VECTOR(int) vec; + sc_vector_init(&vec); + + bool ok; + + for (int i = 0; i < 10; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + } + + ssize_t idx; + + idx = sc_vector_index_of(&vec, 0); + assert(idx == 0); + + idx = sc_vector_index_of(&vec, 1); + assert(idx == 1); + + idx = sc_vector_index_of(&vec, 4); + assert(idx == 4); + + idx = sc_vector_index_of(&vec, 9); + assert(idx == 9); + + idx = sc_vector_index_of(&vec, 12); + assert(idx == -1); + + sc_vector_destroy(&vec); +} + +static void test_vector_grow(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + for (int i = 0; i < 50; ++i) + { + ok = sc_vector_push(&vec, i); /* append */ + assert(ok); + } + + assert(vec.cap >= 50); + assert(vec.size == 50); + + for (int i = 0; i < 25; ++i) + { + ok = sc_vector_insert(&vec, 20, i); /* insert in the middle */ + assert(ok); + } + + assert(vec.cap >= 75); + assert(vec.size == 75); + + for (int i = 0; i < 25; ++i) + { + ok = sc_vector_insert(&vec, 0, i); /* prepend */ + assert(ok); + } + + assert(vec.cap >= 100); + assert(vec.size == 100); + + for (int i = 0; i < 50; ++i) + sc_vector_remove(&vec, 20); /* remove from the middle */ + + assert(vec.cap >= 50); + assert(vec.size == 50); + + for (int i = 0; i < 25; ++i) + sc_vector_remove(&vec, 0); /* remove from the head */ + + assert(vec.cap >= 25); + assert(vec.size == 25); + + for (int i = 24; i >=0; --i) + sc_vector_remove(&vec, i); /* remove from the tail */ + + assert(vec.size == 0); + + sc_vector_destroy(&vec); +} + +static void test_vector_exp_growth(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + size_t oldcap = vec.cap; + int realloc_count = 0; + bool ok; + for (int i = 0; i < 10000; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + if (vec.cap != oldcap) + { + realloc_count++; + oldcap = vec.cap; + } + } + + /* Test speciically for an expected growth factor of 1.5. In practice, the + * result is even lower (19) due to the first alloc of size 10 */ + assert(realloc_count <= 23); /* ln(10000) / ln(1.5) ~= 23 */ + + realloc_count = 0; + for (int i = 9999; i >= 0; --i) + { + sc_vector_remove(&vec, i); + if (vec.cap != oldcap) + { + realloc_count++; + oldcap = vec.cap; + } + } + + assert(realloc_count <= 23); /* same expectations for removals */ + assert(realloc_count > 0); /* sc_vector_remove() must autoshrink */ + + sc_vector_destroy(&vec); +} + +static void test_vector_reserve(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_reserve(&vec, 800); + assert(ok); + assert(vec.cap >= 800); + assert(vec.size == 0); + + size_t initial_cap = vec.cap; + + for (int i = 0; i < 800; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + assert(vec.cap == initial_cap); /* no realloc */ + } + + sc_vector_destroy(&vec); +} + +static void test_vector_shrink_to_fit(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + bool ok; + + ok = sc_vector_reserve(&vec, 800); + assert(ok); + for (int i = 0; i < 250; ++i) + { + ok = sc_vector_push(&vec, i); + assert(ok); + } + + assert(vec.cap >= 800); + assert(vec.size == 250); + + sc_vector_shrink_to_fit(&vec); + assert(vec.cap == 250); + assert(vec.size == 250); + + sc_vector_destroy(&vec); +} + +static void test_vector_move(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + for (int i = 0; i < 7; ++i) + { + bool ok = sc_vector_push(&vec, i); + assert(ok); + } + + /* move item at 1 so that its new position is 4 */ + sc_vector_move(&vec, 1, 4); + + assert(vec.size == 7); + assert(vec.data[0] == 0); + assert(vec.data[1] == 2); + assert(vec.data[2] == 3); + assert(vec.data[3] == 4); + assert(vec.data[4] == 1); + assert(vec.data[5] == 5); + assert(vec.data[6] == 6); + + sc_vector_destroy(&vec); +} + +static void test_vector_move_slice_forward(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + for (int i = 0; i < 10; ++i) + { + bool ok = sc_vector_push(&vec, i); + assert(ok); + } + + /* move slice {2, 3, 4, 5} so that its new position is 5 */ + sc_vector_move_slice(&vec, 2, 4, 5); + + assert(vec.size == 10); + assert(vec.data[0] == 0); + assert(vec.data[1] == 1); + assert(vec.data[2] == 6); + assert(vec.data[3] == 7); + assert(vec.data[4] == 8); + assert(vec.data[5] == 2); + assert(vec.data[6] == 3); + assert(vec.data[7] == 4); + assert(vec.data[8] == 5); + assert(vec.data[9] == 9); + + sc_vector_destroy(&vec); +} + +static void test_vector_move_slice_backward(void) { + struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; + + for (int i = 0; i < 10; ++i) + { + bool ok = sc_vector_push(&vec, i); + assert(ok); + } + + /* move slice {5, 6, 7} so that its new position is 2 */ + sc_vector_move_slice(&vec, 5, 3, 2); + + assert(vec.size == 10); + assert(vec.data[0] == 0); + assert(vec.data[1] == 1); + assert(vec.data[2] == 5); + assert(vec.data[3] == 6); + assert(vec.data[4] == 7); + assert(vec.data[5] == 2); + assert(vec.data[6] == 3); + assert(vec.data[7] == 4); + assert(vec.data[8] == 8); + assert(vec.data[9] == 9); + + sc_vector_destroy(&vec); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_vector_insert_remove(); + test_vector_push_array(); + test_vector_insert_array(); + test_vector_remove_slice(); + test_vector_swap_remove(); + test_vector_move(); + test_vector_move_slice_forward(); + test_vector_move_slice_backward(); + test_vector_index_of(); + test_vector_grow(); + test_vector_exp_growth(); + test_vector_reserve(); + test_vector_shrink_to_fit(); + return 0; +} diff --git a/build.gradle b/build.gradle index c977c398..b27befb6 100644 --- a/build.gradle +++ b/build.gradle @@ -4,10 +4,10 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.0.1' + classpath 'com.android.tools.build:gradle:8.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -17,13 +17,9 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:deprecation" } } - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/bump_version b/bump_version new file mode 100755 index 00000000..a0963666 --- /dev/null +++ b/bump_version @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# This script bump scrcpy version by editing all the necessary files. +# +# Usage: +# +# ./bump_version 1.23.4 +# +# Then check the diff manually to confirm that everything is ok. + +set -e + +if [[ $# != 1 ]] +then + echo "Syntax: $0 " >&2 + exit 1 +fi + +VERSION="$1" + +a=( ${VERSION//./ } ) +MAJOR="${a[0]:-0}" +MINOR="${a[1]:-0}" +PATCH="${a[2]:-0}" + +# If VERSION is 1.23.4, then VERSION_CODE is 12304 +VERSION_CODE="$(( $MAJOR * 10000 + $MINOR * 100 + "$PATCH" ))" + +echo "$VERSION: major=$MAJOR minor=$MINOR patch=$PATCH [versionCode=$VERSION_CODE]" +sed -i "s/^\(\s*version: \)'[^']*'/\1'$VERSION'/" meson.build +sed -i "s/^\(\s*versionCode \).*/\1$VERSION_CODE/;s/^\(\s*versionName \).*/\1\"$VERSION\"/" server/build.gradle +sed -i "s/^\(SCRCPY_VERSION_NAME=\).*/\1$VERSION/" server/build_without_gradle.sh +sed -i "s/^\(\s*VALUE \"ProductVersion\", \)\"[^\"]*\"/\1\"$VERSION\"/" app/scrcpy-windows.rc +echo done diff --git a/config/android-checkstyle.gradle b/config/android-checkstyle.gradle index f998530e..1e5ce3ba 100644 --- a/config/android-checkstyle.gradle +++ b/config/android-checkstyle.gradle @@ -2,7 +2,7 @@ apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { - toolVersion = '6.19' + toolVersion = '10.12.5' } task checkstyle(type: Checkstyle) { diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index e65ee33b..4bf05558 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -37,6 +37,14 @@ page at http://checkstyle.sourceforge.net/config.html --> + + + + + + + + @@ -72,13 +80,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - - - @@ -154,26 +155,6 @@ page at http://checkstyle.sourceforge.net/config.html --> - - - - - - - - - - - - - - - - - - - - diff --git a/cross_win32.txt b/cross_win32.txt index 4db17be7..05f9a86b 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -6,15 +6,11 @@ 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' +pkg-config = 'i686-w64-mingw32-pkg-config' +windres = 'i686-w64-mingw32-windres' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'i686' endian = 'little' - -[properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' -prebuilt_sdl2 = 'SDL2-2.0.16/i686-w64-mingw32' diff --git a/cross_win64.txt b/cross_win64.txt index d03f0272..86364ad6 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -6,15 +6,11 @@ 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' +pkg-config = 'x86_64-w64-mingw32-pkg-config' +windres = 'x86_64-w64-mingw32-windres' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'x86_64' endian = 'little' - -[properties] -prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' -prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' -prebuilt_sdl2 = 'SDL2-2.0.16/x86_64-w64-mingw32' diff --git a/data/scrcpy-console.bat b/data/scrcpy-console.bat deleted file mode 100644 index b90be29a..00000000 --- a/data/scrcpy-console.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -scrcpy.exe %* -:: if the exit code is >= 1, then pause -if errorlevel 1 pause diff --git a/doc/audio.md b/doc/audio.md new file mode 100644 index 00000000..f1d4d8e7 --- /dev/null +++ b/doc/audio.md @@ -0,0 +1,154 @@ +# Audio + +Audio forwarding is supported for devices with Android 11 or higher, and it is +enabled by default: + + - For **Android 12 or newer**, it works out-of-the-box. + - For **Android 11**, you'll need to ensure that the device screen is unlocked + when starting scrcpy. A fake popup will briefly appear to make the system + think that the shell app is in the foreground. Without this, audio capture + will fail. + - For **Android 10 or earlier**, audio cannot be captured and is automatically + disabled. + +If audio capture fails, then mirroring continues with video only (since audio is +enabled by default, it is not acceptable to make scrcpy fail if it is not +available), unless `--require-audio` is set. + + +## No audio + +To disable audio: + +``` +scrcpy --no-audio +``` + +To disable only the audio playback, see [no playback](video.md#no-playback). + +## Audio only + +To play audio only, disable the video: + +```bash +scrcpy --no-video +# interrupt with Ctrl+C +``` + +Without video, the audio latency is typically not critical, so it might be +interesting to add [buffering](#buffering) to minimize glitches: + +``` +scrcpy --no-video --audio-buffer=200 +``` + +## Source + +By default, the device audio output is forwarded. + +It is possible to capture the device microphone instead: + +``` +scrcpy --audio-source=mic +``` + +For example, to use the device as a dictaphone and record a capture directly on +the computer: + +``` +scrcpy --audio-source=mic --no-video --no-playback --record=file.opus +``` + + +## Codec + +The audio codec can be selected. The possible values are `opus` (default), +`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE): + +```bash +scrcpy --audio-codec=opus # default +scrcpy --audio-codec=aac +scrcpy --audio-codec=flac +scrcpy --audio-codec=raw +``` + +In particular, if you get the following error: + +> Failed to initialize audio/opus, error 0xfffffffe + +then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. + +For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], +check `--audio-codec-options` in the manpage or in `scrcpy --help`. + +For example, to change the [FLAC compression level]: + +```bash +scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8 +``` + +[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat +[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL + + +## Encoder + +Several encoders may be available on the device. They can be listed by: + +```bash +scrcpy --list-encoders +``` + +To select a specific encoder: + +```bash +scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder' +``` + + +## Bit rate + +The default audio bit rate is 128Kbps. To change it: + +```bash +scrcpy --audio-bit-rate=64K +scrcpy --audio-bit-rate=64000 # equivalent +``` + +_This parameter does not apply to RAW audio codec (`--audio-codec=raw`)._ + + +## Buffering + +Audio buffering is unavoidable. It must be kept small enough so that the latency +is acceptable, but large enough to minimize buffer underrun (causing audio +glitches). + +The default buffer size is set to 50ms. It can be adjusted: + +```bash +scrcpy --audio-buffer=40 # smaller than default +scrcpy --audio-buffer=100 # higher than default +``` + +Note that this option changes the _target_ buffering. It is possible that this +target buffering might not be reached (on frequent buffer underflow typically). + +If you don't interact with the device (to watch a video for example), a higher +latency (for both [video](video.md#buffering) and audio) might be preferable to +avoid glitches and smooth the playback: + +``` +scrcpy --display-buffer=200 --audio-buffer=200 +``` + +It is also possible to configure another audio buffer (the audio output buffer), +by default set to 5ms. Don't change it, unless you get some [robotic and glitchy +sound][#3793]: + +```bash +# Only if absolutely necessary +scrcpy --audio-output-buffer=10 +``` + +[#3793]: https://github.com/Genymobile/scrcpy/issues/3793 diff --git a/BUILD.md b/doc/build.md similarity index 73% rename from BUILD.md rename to doc/build.md index 87078b71..751cf831 100644 --- a/BUILD.md +++ b/doc/build.md @@ -2,56 +2,16 @@ Here are the instructions to build _scrcpy_ (client and 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 -``` - +If you just want to build and install the latest release, follow the simplified +process described in [doc/linux.md](linux.md). ## Branches -### `master` - -The `master` branch concerns the latest release, and is the home page of the -project on Github. - - -### `dev` - -`dev` is the current development branch. Every commit present in `dev` will be -in the next release. +There are two main branches: + - `master`: contains the latest release. It is the home page of the project on + GitHub. + - `dev`: the current development branch. Every commit present in `dev` will be + in the next release. If you want to contribute code, please base your commits on the latest `dev` branch. @@ -68,6 +28,8 @@ the following files to a directory accessible from your `PATH`: - `AdbWinApi.dll` - `AdbWinUsbApi.dll` +It is also available in scrcpy releases. + The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions. [adb]: https://developer.android.com/studio/command-line/adb.html @@ -88,14 +50,15 @@ Install the required packages from your package manager. ```bash # runtime dependencies -sudo apt install ffmpeg libsdl2-2.0-0 adb +sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0 # client build dependencies sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ - libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev + libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \ + libswresample-dev libusb-1.0-0-dev # server build dependencies -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install @@ -114,7 +77,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make # server build dependencies sudo dnf install java-devel @@ -137,7 +100,7 @@ sudo apt install mingw-w64 mingw-w64-tools You also need the JDK to build the server: ```bash -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk ``` Then generate the releases: @@ -159,7 +122,8 @@ install the required packages: ```bash # runtime dependencies pacman -S mingw-w64-x86_64-SDL2 \ - mingw-w64-x86_64-ffmpeg + mingw-w64-x86_64-ffmpeg \ + mingw-w64-x86_64-libusb # client build dependencies pacman -S mingw-w64-x86_64-make \ @@ -173,7 +137,8 @@ For a 32 bits version, replace `x86_64` by `i686`: ```bash # runtime dependencies pacman -S mingw-w64-i686-SDL2 \ - mingw-w64-i686-ffmpeg + mingw-w64-i686-ffmpeg \ + mingw-w64-i686-libusb # client build dependencies pacman -S mingw-w64-i686-make \ @@ -197,19 +162,19 @@ Install the packages with [Homebrew]: ```bash # runtime dependencies -brew install sdl2 ffmpeg +brew install sdl2 ffmpeg libusb # client build dependencies brew install pkg-config meson ``` -Additionally, if you want to build the server, install Java 8 from Caskroom, and +Additionally, if you want to build the server, install Java 17 from Caskroom, and make it available from the `PATH`: ```bash brew tap homebrew/cask-versions -brew install adoptopenjdk/openjdk/adoptopenjdk11 -export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)" +brew install adoptopenjdk/openjdk/adoptopenjdk17 +export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export PATH="$JAVA_HOME/bin:$PATH" ``` @@ -256,7 +221,7 @@ set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk Then, build: ```bash -meson x --buildtype release --strip -Db_lto=true +meson setup x --buildtype=release --strip -Db_lto=true ninja -Cx # DO NOT RUN AS ROOT ``` @@ -268,16 +233,16 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v1.18`][direct-scrcpy-server] - _(SHA-256: 641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3)_ + - [`scrcpy-server-v2.4`][direct-scrcpy-server] + SHA-256: `93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash -meson x --buildtype release --strip -Db_lto=true \ +meson setup x --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=/path/to/scrcpy-server ninja -Cx # DO NOT RUN AS ROOT ``` @@ -301,13 +266,17 @@ After a successful build, you can install _scrcpy_ on the system: sudo ninja -Cx install # without sudo on Windows ``` -This installs three files: +This installs several files: + + - `/usr/local/bin/scrcpy` (main app) + - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) + - `/usr/local/share/man/man1/scrcpy.1` (manpage) + - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon) + - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) + - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) - - `/usr/local/bin/scrcpy` - - `/usr/local/share/scrcpy/scrcpy-server` - - `/usr/local/share/man/man1/scrcpy.1` +You can then run `scrcpy`. -You can then [run](README.md#run) _scrcpy_. ### Uninstall diff --git a/doc/camera.md b/doc/camera.md new file mode 100644 index 00000000..32417694 --- /dev/null +++ b/doc/camera.md @@ -0,0 +1,171 @@ +# Camera + +Camera mirroring is supported for devices with Android 12 or higher. + +To capture the camera instead of the device screen: + +``` +scrcpy --video-source=camera +``` + +By default, it automatically switches [audio source](audio.md#source) to +microphone (as if `--audio-source=mic` were also passed). + +```bash +scrcpy --video-source=display # default is --audio-source=output +scrcpy --video-source=camera # default is --audio-source=mic +scrcpy --video-source=display --audio-source=mic # force display AND microphone +scrcpy --video-source=camera --audio-source=output # force camera AND device audio output +``` + +Audio can be disabled: + +```bash +# audio not captured at all +scrcpy --video-source=camera --no-audio +scrcpy --video-source=camera --no-audio --record=file.mp4 + +# audio captured and recorded, but not played +scrcpy --video-source=camera --no-audio-playback --record=file.mp4 +``` + + +## List + +To list the cameras available (with their declared valid sizes and frame rates): + +``` +scrcpy --list-cameras +scrcpy --list-camera-sizes +``` + +_Note that the sizes and frame rates are declarative. They are not accurate on +all devices: some of them are declared but not supported, while some others are +not declared but supported._ + + +## Selection + +It is possible to pass an explicit camera id (as listed by `--list-cameras`): + +``` +scrcpy --video-source=camera --camera-id=0 +``` + +Alternatively, the camera may be selected automatically: + +```bash +scrcpy --video-source=camera # use the first camera +scrcpy --video-source=camera --camera-facing=front # use the first front camera +scrcpy --video-source=camera --camera-facing=back # use the first back camera +scrcpy --video-source=camera --camera-facing=external # use the first external camera +``` + +If `--camera-id` is specified, then `--camera-facing` is forbidden (the id +already determines the camera): + +```bash +scrcpy --video-source=camera --camera-id=0 --camera-facing=front # error +``` + + +### Size selection + +It is possible to pass an explicit camera size: + +``` +scrcpy --video-source=camera --camera-size=1920x1080 +``` + +The given size may be listed among the declared valid sizes +(`--list-camera-sizes`), but may also be anything else (some devices support +arbitrary sizes): + +``` +scrcpy --video-source=camera --camera-size=1840x444 +``` + +Alternatively, a declared valid size (among the ones listed by +`list-camera-sizes`) may be selected automatically. + +Two constraints are supported: + - `-m`/`--max-size` (already used for display mirroring), for example `-m1920`; + - `--camera-ar` to specify an aspect ratio (`:`, `` or + `sensor`). + +Some examples: + +```bash +scrcpy --video-source=camera # use the greatest width and the greatest associated height +scrcpy --video-source=camera -m1920 # use the greatest width not above 1920 and the greatest associated height +scrcpy --video-source=camera --camera-ar=4:3 # use the greatest size with an aspect ratio of 4:3 (+/- 10%) +scrcpy --video-source=camera --camera-ar=1.6 # use the greatest size with an aspect ratio of 1.6 (+/- 10%) +scrcpy --video-source=camera --camera-ar=sensor # use the greatest size with the aspect ratio of the camera sensor (+/- 10%) +scrcpy --video-source=camera -m1920 --camera-ar=16:9 # use the greatest width not above 1920 and the closest to 16:9 aspect ratio +``` + +If `--camera-size` is specified, then `-m`/`--max-size` and `--camera-ar` are +forbidden (the size is determined by the value given explicitly): + +```bash +scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error +``` + + +## Rotation + +To rotate the captured video, use the [video orientation](video.md#orientation) +option: + +``` +scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90 +``` + + +## Frame rate + +By default, camera is captured at Android's default frame rate (30 fps). + +To configure a different frame rate: + +``` +scrcpy --video-source=camera --camera-fps=60 +``` + + +## High speed capture + +The Android camera API also supports a [high speed capture mode][high speed]. + +This mode is restricted to specific resolutions and frame rates, listed by +`--list-camera-sizes`. + +``` +scrcpy --video-source=camera --camera-size=1920x1080 --camera-fps=240 +``` + +[high speed]: https://developer.android.com/reference/android/hardware/camera2/CameraConstrainedHighSpeedCaptureSession + + +## Brace expansion tip + +All camera options start with `--camera-`, so if your shell supports it, you can +benefit from [brace expansion] (for example, it is supported _bash_ and _zsh_): + +```bash +scrcpy --video-source=camera --camera-{facing=back,ar=16:9,high-speed,fps=120} +``` + +This will be expanded as: + +```bash +scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high-speed --camera-fps=120 +``` + +[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html + + +## Webcam + +Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera +may be used as a webcam on the computer. diff --git a/doc/connection.md b/doc/connection.md new file mode 100644 index 00000000..17efbbdc --- /dev/null +++ b/doc/connection.md @@ -0,0 +1,125 @@ +# Connection + +## Selection + +If exactly one device is connected (i.e. listed by `adb devices`), then it is +automatically selected. + +However, if there are multiple devices connected, you must specify the one to +use in one of 4 ways: + - by its serial: + ```bash + scrcpy --serial=0123456789abcdef + scrcpy -s 0123456789abcdef # short version + + # the serial is the ip:port if connected over TCP/IP (same behavior as adb) + scrcpy --serial=192.168.1.1:5555 + ``` + - the one connected over USB (if there is exactly one): + ```bash + scrcpy --select-usb + scrcpy -d # short version + ``` + - the one connected over TCP/IP (if there is exactly one): + ```bash + scrcpy --select-tcpip + scrcpy -e # short version + ``` + - a device already listening on TCP/IP (see [below](#tcpip-wireless)): + ```bash + scrcpy --tcpip=192.168.1.1:5555 + scrcpy --tcpip=192.168.1.1 # default port is 5555 + ``` + +The serial may also be provided via the environment variable `ANDROID_SERIAL` +(also used by `adb`): + +```bash +# in bash +export ANDROID_SERIAL=0123456789abcdef +scrcpy +``` + +```cmd +:: in cmd +set ANDROID_SERIAL=0123456789abcdef +scrcpy +``` + +```powershell +# in PowerShell +$env:ANDROID_SERIAL = '0123456789abcdef' +scrcpy +``` + + +## TCP/IP (wireless) + +_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a +device over TCP/IP. The device must be connected on the same network as the +computer. + +[connect]: https://developer.android.com/studio/command-line/adb.html#wireless + + +### Automatic + +An option `--tcpip` allows to configure the connection automatically. There are +two variants. + +If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP +address), connect the device over USB, then run: + +```bash +scrcpy --tcpip # without arguments +``` + +It will automatically find the device IP address and adb port, enable TCP/IP +mode if necessary, then connect to the device before starting. + +If the device (accessible at 192.168.1.1 in this example) already listens on a +port (typically 5555) for incoming _adb_ connections, then run: + +```bash +scrcpy --tcpip=192.168.1.1 # default port is 5555 +scrcpy --tcpip=192.168.1.1:5555 +``` + + +### Manual + +Alternatively, it is possible to enable the TCP/IP connection manually using +`adb`: + +1. Plug the device into a USB port on your computer. +2. Connect the device to the same Wi-Fi network as your computer. +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` +with the device IP address you found)_. +7. Run `scrcpy` as usual. +8. Run `adb disconnect` once you're done. + +Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass +having to physically connect your device directly to your computer. + +[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line + + +## Autostart + +A small tool (by the scrcpy author) allows to run arbitrary commands whenever a +new Android device is connected: [AutoAdb]. It can be used to start scrcpy: + +```bash +autoadb scrcpy -s '{}' +``` + +[AutoAdb]: https://github.com/rom1v/autoadb diff --git a/doc/control.md b/doc/control.md new file mode 100644 index 00000000..e9fd9e9b --- /dev/null +++ b/doc/control.md @@ -0,0 +1,114 @@ +# Control + +## Read-only + +To disable controls (everything which can interact with the device: input keys, +mouse events, drag&drop files): + +```bash +scrcpy --no-control +scrcpy -n # short version +``` + +## Keyboard and mouse + +Read [keyboard](keyboard.md) and [mouse](mouse.md). + + +## Copy-paste + +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 injects 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 Android clipboard. As a consequence, any Android application could read +its content. You should avoid pasting sensitive content (like passwords) that +way. + +Some Android 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). + +To disable automatic clipboard synchronization, use +`--no-clipboard-autosync`. + + +## Pinch-to-zoom, rotate and tilt simulation + +To simulate "pinch-to-zoom": Ctrl+_click-and-move_. + +More precisely, hold down 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. + +https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767 + +To simulate a tilt gesture: Shift+_click-and-move-up-or-down_. + +https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f + +Technically, _scrcpy_ generates additional touch events from a "virtual finger" +at a location inverted through the center of the screen. When pressing +Ctrl the _x_ and _y_ coordinates are inverted. Using Shift +only inverts _x_. + +This only works for the default mouse mode (`--mouse=sdk`). + + +## 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 + +To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_ +window. + +There is no visual feedback, a log is printed to the console. + + +### Push file to device + +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/Movies/ +``` diff --git a/doc/develop.md b/doc/develop.md new file mode 100644 index 00000000..e5274783 --- /dev/null +++ b/doc/develop.md @@ -0,0 +1,488 @@ +# scrcpy for developers + +## Overview + +This application is composed of two parts: + - the server (`scrcpy-server`), to be executed on the device, + - the client (the `scrcpy` binary), executed on the host computer. + +The client is responsible to push the server to the device and start its +execution. + +The client and the server establish communication using separate sockets for +video, audio and controls. Any of them may be disabled (but not all), so +there are 1, 2 or 3 socket(s). + +The server initially sends the device name on the first socket (it is used for +the scrcpy window title), then each socket is used for its own purpose. All +reads and writes are performed from a dedicated thread for each socket, both on +the client and on the server. + +If video is enabled, then the server sends a raw video stream (H.264 by default) +of the device screen, with some additional headers for each packet. The client +decodes the video frames, and displays them as soon as possible, without +buffering (unless `--display-buffer=delay` is specified) to minimize latency. +The client is not aware of the device rotation (which is handled by the server), +it just knows the dimensions of the video frames it receives. + +Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS +by default) of the device audio output (or the microphone if +`--audio-source=mic` is specified), with some additional headers for each +packet. The client decodes the stream, attempts to keep a minimal latency by +maintaining an average buffering. The [blog post][scrcpy2] of the scrcpy v2.0 +release gives more details about the audio feature. + +If control is enabled, then the client captures relevant keyboard and mouse +events, that it transmits to the server, which injects them to the device. This +is the only socket which is used in both direction: input events are sent from +the client to the device, and when the device clipboard changes, the new content +is sent from the device to the client to support seamless copy-paste. + +[scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/ + +Note that the client-server roles are expressed at the application level: + + - the server _serves_ video and audio streams, and handle requests from the + client, + - the client _controls_ the device through the server. + +However, by default (when `--force-adb-forward` is not set), the roles are +reversed at the network level: + + - the client opens a server socket and listen on a port before starting the + server, + - the server connects to the client. + +This role inversion guarantees that the connection will not fail due to race +conditions without polling. + + +## Server + + +### Privileges + +Capturing the screen requires some privileges, which are granted to `shell`. + +The server is a Java application (with a [`public static void main(String... +args)`][main] method), compiled against the Android framework, and executed as +`shell` on the Android device. + +[main]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Server.java#L193 + +To run such a Java application, the classes must be [_dexed_][dex] (typically, +to `classes.dex`). If `my.package.MainClass` is the main class, compiled to +`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run +with: + + adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / my.package.MainClass + +_The path `/data/local/tmp` is a good candidate to push the server, since it's +readable and writable by `shell`, but not world-writable, so a malicious +application may not replace the server just before the client executes it._ + +Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing +`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle +build system, the server is built to an (unsigned) APK (renamed to +`scrcpy-server.jar`). + +[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) +[apk]: https://en.wikipedia.org/wiki/Android_application_package + + +### Hidden methods + +Although compiled against the Android framework, [hidden] methods and classes are +not directly accessible (and they may differ from one Android version to +another). + +They can be called using reflection though. The communication with hidden +components is provided by [_wrappers_ classes][wrappers] and [aidl]. + +[hidden]: https://stackoverflow.com/a/31908373/1987178 +[wrappers]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/java/com/genymobile/scrcpy/wrappers +[aidl]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/aidl + + + +### Execution + +The server is started by the client basically by executing the following +commands: + +```bash +adb push scrcpy-server /data/local/tmp/scrcpy-server.jar +adb forward tcp:27183 localabstract:scrcpy +adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 +``` + +The first argument (`2.1` in the example) is the client scrcpy version. The +server fails if the client and the server do not have the exact same version. +The protocol between the client and the server may change from version to +version (see [protocol](#protocol) below), and there is no backward or forward +compatibility (there is no point to use different client and server versions). +This check allows to detect misconfiguration (running an older or newer server +by mistake). + +It is followed by any number of arguments, in the form of `key=value` pairs. +Their order is irrelevant. The possible keys and associated value types can be +found in the [server][server-options] and [client][client-options] code. + +[server-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L181 +[client-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L226 + +For example, if we execute `scrcpy -m1920 --no-audio`, then the server +execution will look like this: + +```bash +# scid is a random number to identify different clients running on the same device +adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 scid=12345678 log_level=info audio=false max_size=1920 +``` + +### Components + +When executed, its [`main()`][main] method is executed (on the "main" thread). +It parses the arguments, establishes the connection with the client and starts +the other "components": + - the **video** streamer: it captures the video screen and send encoded video + packets on the _video_ socket (from the _video_ thread). + - the **audio** streamer: it uses several threads to capture raw packets, + submits them to encoding and retrieve encoded packets, which it sends on the + _audio_ socket. + - the **controller**: it receives _control messages_ (typically input events) + on the _control_ socket from one thread, and sends _device messages_ (e.g. to + transmit the device clipboard content to the client) on the same _control + socket_ from another thread. Thus, the _control_ socket is used in both + directions (contrary to the _video_ and _audio_ sockets). + + +### Screen video encoding + +The encoding is managed by [`ScreenEncoder`]. + +The video is encoded using the [`MediaCodec`] API. The codec encodes the content +of a `Surface` associated to the display, and writes the encoding packets to the +client (on the _video_ socket). + +[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html + +On device rotation (or folding), the encoding session is [reset] and restarted. + +New frames are produced only when changes occur on the surface. This avoids to +send unnecessary frames, but by default there might be drawbacks: + + - it does not send any frame on start if the device screen does not change, + - after fast motion changes, the last frame may have poor quality. + +Both problems are [solved][repeat] by the flag +[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. + +[reset]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L179 +[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 +[repeat]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L246-L247 +[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER + + +### Audio encoding + +Similarly, the audio is [captured] using an [`AudioRecord`], and [encoded] using +the [`MediaCodec`] asynchronous API. + +More details are available on the [blog post][scrcpy2] introducing the audio feature. + +[captured]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +[encoded]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +[`AudioRecord`]: https://developer.android.com/reference/android/media/AudioRecord + + +### Input events injection + +_Control messages_ are received from the client by the [`Controller`] (run in a +separate thread). There are several types of input events: + - keycode (cf [`KeyEvent`]), + - text (special characters may not be handled by keycodes directly), + - mouse motion/click, + - mouse scroll, + - other commands (e.g. to switch the screen on or to copy the clipboard). + +Some of them need to inject input events to the system. To do so, they use the +_hidden_ method [`InputManager.injectInputEvent()`] (exposed by the +[`InputManager` wrapper][inject-wrapper]). + +[`Controller`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Controller.java +[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html +[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html +[`InputManager.injectInputEvent()`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L34 +[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 + + + +## Client + +The client relies on [SDL], which provides cross-platform API for UI, input +events, threading, etc. + +The video and audio streams are decoded by [FFmpeg]. + +[SDL]: https://www.libsdl.org +[ffmpeg]: https://ffmpeg.org/ + + +### Initialization + +The client parses the command line arguments, then [runs one of two code +paths][run]: + - scrcpy in "normal" mode ([`scrcpy.c`]) + - scrcpy in [OTG mode](otg.md) ([`scrcpy_otg.c`]) + +[run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82 +[`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293 +[`scrcpy_otg.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/usb/scrcpy_otg.c#L51-L52 + +In the remaining of this document, we assume that the "normal" mode is used +(read the code for the OTG mode). + +On startup, the client: + - opens the _video_, _audio_ and _control_ sockets; + - pushes and starts the server on the device; + - initializes its components (demuxers, decoders, recorder…). + + +### Video and audio streams + +Depending on the arguments passed to `scrcpy`, several components may be used. +Here is an overview of the video and audio components: + +``` + V4L2 sink + / + decoder + / \ + VIDEO -------------> demuxer display + \ + recorder + / + AUDIO -------------> demuxer + \ + decoder --- audio player +``` + +The _demuxer_ is responsible to extract video and audio packets (read some +header, split the video stream into packets at correct boundaries, etc.). + +The demuxed packets may be sent to a _decoder_ (one per stream, to produce +frames) and to a recorder (receiving both video and audio stream to record a +single file). The packets are encoded on the device (by `MediaCodec`), but when +recording, they are _muxed_ (asynchronously) into a container (MKV or MP4) on +the client side. + +Video frames are sent to the screen/display to be rendered in the scrcpy window. +They may also be sent to a [V4L2 sink](v4l2.md). + +Audio "frames" (an array of decoded samples) are sent to the audio player. + + +### Controller + +The _controller_ is responsible to send _control messages_ to the device. It +runs in a separate thread, to avoid I/O on the main thread. + +On SDL event, received on the main thread, the _input manager_ creates +appropriate _control messages_. It is responsible to convert SDL events to +Android events. It then pushes the _control messages_ to a queue hold by the +controller. On its own thread, the controller takes messages from the queue, +that it serializes and sends to the client. + + +## Protocol + +The protocol between the client and the server must be considered _internal_: it +may (and will) change at any time for any reason. Everything may change (the +number of sockets, the order in which the sockets must be opened, the data +format on the wire…) from version to version. A client must always be run with a +matching server version. + +This section documents the current protocol in scrcpy v2.1. + +### Connection + +Firstly, the client sets up an adb tunnel: + +```bash +# By default, a reverse redirection: the computer listens, the device connects +adb reverse localabstract:scrcpy_ tcp:27183 + +# As a fallback (or if --force-adb forward is set), a forward redirection: +# the device listens, the computer connects +adb forward tcp:27183 localabstract:scrcpy_ +``` + +(`` is a 31-bit random number, so that it does not fail when several +scrcpy instances start "at the same time" for the same device.) + +Then, up to 3 sockets are opened, in that order: + - a _video_ socket + - an _audio_ socket + - a _control_ socket + +Each one may be disabled (respectively by `--no-video`, `--no-audio` and +`--no-control`, directly or indirectly). For example, if `--no-audio` is set, +then the _video_ socket is opened first, then the _control_ socket. + +On the _first_ socket opened (whichever it is), if the tunnel is _forward_, then +a [dummy byte] is sent from the device to the client. This allows to detect a +connection error (the client connection does not fail as long as there is an adb +forward redirection, even if nothing is listening on the device side). + +Still on this _first_ socket, the device sends some [metadata][device meta] to +the client (currently only the device name, used as the window title, but there +might be other fields in the future). + +[dummy byte]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L93 +[device meta]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L151 + +You can read the [client][client-connection] and [server][server-connection] +code for more details. + +[client-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L465-L466 +[server-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L63 + +Then each socket is used for its intended purpose. + +### Video and audio + +On the _video_ and _audio_ sockets, the device first sends some [codec +metadata]: + - On the _video_ socket, 12 bytes: + - the codec id (`u32`) (H264, H265 or AV1) + - the initial video width (`u32`) + - the initial video height (`u32`) + - On the _audio_ socket, 4 bytes: + - the codec id (`u32`) (OPUS, AAC or RAW) + +[codec metadata]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L33-L51 + +Then each packet produced by `MediaCodec` is sent, prefixed by a 12-byte [frame +header]: + - config packet flag (`u1`) + - key frame flag (`u1`) + - PTS (`u62`) + - packet size (`u32`) + +Here is a schema describing the frame header: + +``` + [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... + <-------------> <-----> <-----------------------------... + PTS packet raw packet + size + <---------------------> + frame header + +The most significant bits of the PTS are used for packet flags: + + byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 + CK...... ........ ........ ........ ........ ........ ........ ........ + ^^<-------------------------------------------------------------------> + || PTS + | `- key frame + `-- config packet +``` + +[frame header]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L83 + + +### Controls + +Controls messages are sent via a custom binary protocol. + +The only documentation for this protocol is the set of unit tests on both sides: + - `ControlMessage` (from client to device): [serialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_control_msg_serialize.c) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java) + - `DeviceMessage` (from device to client) [serialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_device_msg_deserialize.c) + + +## Standalone server + +Although the server is designed to work for the scrcpy client, it can be used +with any client which uses the same protocol. + +For simplicity, some [server-specific options] have been added to produce raw +streams easily: + - `send_device_meta=false`: disable the device metata (in practice, the device + name) sent on the _first_ socket + - `send_frame_meta=false`: disable the 12-byte header for each packet + - `send_dummy_byte`: disable the dummy byte sent on forward connections + - `send_codec_meta`: disable the codec information (and initial device size for + video) + - `raw_stream`: disable all the above + +[server-specific options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L309-L329 + +Concretely, here is how to expose a raw H.264 stream on a TCP socket: + +```bash +adb push scrcpy-server-v2.1 /data/local/tmp/scrcpy-server-manual.jar +adb forward tcp:1234 localabstract:scrcpy +adb shell CLASSPATH=/data/local/tmp/scrcpy-server-manual.jar \ + app_process / com.genymobile.scrcpy.Server 2.1 \ + tunnel_forward=true audio=false control=false cleanup=false \ + raw_stream=true max_size=1920 +``` + +As soon as a client connects over TCP on port 1234, the device will start +streaming the video. For example, VLC can play the video (although you will +experience a very high latency, more details [here][vlc-0latency]): + +``` +vlc -Idummy --demux=h264 --network-caching=0 tcp://localhost:1234 +``` + +[vlc-0latency]: https://code.videolan.org/rom1v/vlc/-/merge_requests/20 + + +## Hack + +For more details, go read the code! + +If you find a bug, or have an awesome idea to implement, please discuss and +contribute ;-) + + +### Debug the server + +The server is pushed to the device by the client on startup. + +To debug it, enable the server debugger during configuration: + +```bash +meson setup x -Dserver_debugger=true +# or, if x is already configured +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 setup 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. +Redirect that port to the computer: + +```bash +adb forward tcp:5005 tcp:5005 +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on +`+`, _Remote_, and fill the form: + + - Host: `localhost` + - Port: `5005` + +Then click on _Debug_. diff --git a/doc/device.md b/doc/device.md new file mode 100644 index 00000000..988ad417 --- /dev/null +++ b/doc/device.md @@ -0,0 +1,80 @@ +# Device + +Some command line arguments perform actions on the device itself while scrcpy is +running. + +## Stay awake + +To prevent the device from sleeping after a delay **when the device is plugged +in**: + +```bash +scrcpy --stay-awake +scrcpy -w +``` + +The initial state is restored when _scrcpy_ is closed. + +If the device is not plugged in (i.e. only connected over TCP/IP), +`--stay-awake` has no effect (this is the Android behavior). + + +## Turn screen off + +It is possible to turn the device screen off while mirroring on start with a +command-line option: + +```bash +scrcpy --turn-screen-off +scrcpy -S # short version +``` + +Or by pressing MOD+o at any time (see +[shortcuts](shortcuts.md)). + +To turn it back on, press MOD+Shift+o. + +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. + +It can also be useful to prevent the device from sleeping: + +```bash +scrcpy --turn-screen-off --stay-awake +scrcpy -Sw # short version +``` + + +## Show touches + +For presentations, it may be useful to show physical touches (on the physical +device). Android exposes this feature in _Developers options_. + +_Scrcpy_ provides an option to enable this feature on start and restore the +initial value on exit: + +```bash +scrcpy --show-touches +scrcpy -t # short version +``` + +Note that it only shows _physical_ touches (by a finger on the device). + + +## Power off on close + +To turn the device screen off when closing _scrcpy_: + +```bash +scrcpy --power-off-on-close +``` + +## Power on on start + +By default, on start, the device is powered on. To prevent this behavior: + +```bash +scrcpy --no-power-on +``` diff --git a/doc/keyboard.md b/doc/keyboard.md new file mode 100644 index 00000000..80dfe070 --- /dev/null +++ b/doc/keyboard.md @@ -0,0 +1,136 @@ +# Keyboard + +Several keyboard input modes are available: + + - `--keyboard=sdk` (default) + - `--keyboard=uhid` (or `-K`): simulates a physical HID keyboard using the UHID + kernel module on the device + - `--keyboard=aoa`: simulates a physical HID keyboard using the AOAv2 protocol + - `--keyboard=disabled` + +By default, `sdk` is used, but if you use scrcpy regularly, it is recommended to +use [`uhid`](#uhid) and configure the keyboard layout once and for all. + + +## SDK keyboard + +In this mode (`--keyboard=sdk`, or if the parameter is omitted), keyboard input +events are injected at the Android API level. It works everywhere, but it is +limited to ASCII and some other characters. + +Note that on some devices, an additional option must be enabled in developer +options for this keyboard mode to work. See +[prerequisites](/README.md#prerequisites). + +Additional parameters (specific to `--keyboard=sdk`) described below allow to +customize the behavior. + + +### Text injection preference + +Two kinds of [events][textevents] are generated when typing text: + - _key events_, signaling that a key is pressed or released; + - _text events_, signaling that a text has been entered. + +By default, numbers and "special characters" are inserted using text events, but +letters are injected using key events, so that the keyboard behaves as expected +in games (typically for WASD keys). + +But this may [cause issues][prefertext]. If you encounter such a problem, you +can inject letters as text (or just switch to [UHID](#uhid)): + +```bash +scrcpy --prefer-text +``` + +(but this will break keyboard behavior in games) + +On the contrary, you could force to always inject raw key events: + +```bash +scrcpy --raw-key-events +``` + +[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input +[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 + + +### Key repeat + +By default, holding a key down generates repeated key events. Ths can cause +performance problems in some games, where these events are useless anyway. + +To avoid forwarding repeated key events: + +```bash +scrcpy --no-key-repeat +``` + + +## Physical keyboard simulation + +Two modes allow to simulate a physical HID keyboard on the device. + +To work properly, it is necessary to configure (once and for all) the keyboard +layout on the device to match that of the computer. + +The configuration page can be opened in one of the following ways: + - from the scrcpy window (when `uhid` or `aoa` is used), by pressing + MOD+k (see [shortcuts](shortcuts.md)) + - from the device, in Settings → System → Languages and input → Physical + devices + - from a terminal on the computer, by executing `adb shell am start -a + android.settings.HARD_KEYBOARD_SETTINGS` + +From this configuration page, it is also possible to enable or disable on-screen +keyboard. + + +### UHID + +This mode simulates a physical HID keyboard using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID keyboard, use: + +```bash +scrcpy --keyboard=uhid +scrcpy -K # short version +``` + +Once the keyboard layout is configured (see above), it is the best mode for +using the keyboard while mirroring: + + - it works for all characters and IME (contrary to `--keyboard=sdk`) + - the on-screen keyboard can be disabled (contrary to `--keyboard=sdk`) + - it works over TCP/IP (wirelessly) (contrary to `--keyboard=aoa`) + - there are no issues on Windows (contrary to `--keyboard=aoa`) + +One drawback is that it may not work on old Android versions due to permission +errors. + + +### AOA + +This mode simulates a physical HID keyboard using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA keyboard, use: + +```bash +scrcpy --keyboard=aoa +``` + +Contrary to the other modes, it works at the USB level directly (so it only +works over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/linux.md b/doc/linux.md new file mode 100644 index 00000000..68b4ee10 --- /dev/null +++ b/doc/linux.md @@ -0,0 +1,79 @@ +# On Linux + +## Install + +Packaging status + +Scrcpy is packaged in several distributions and package managers: + + - Debian/Ubuntu: `apt install scrcpy` + - Arch Linux: `pacman -S scrcpy` + - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` + - Gentoo: `emerge scrcpy` + - Snap: `snap install scrcpy` + - … (see [repology](https://repology.org/project/scrcpy/versions)) + +### Latest version + +However, the packaged version is not always the latest release. 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 \ + libswresample-dev libusb-1.0-0 libusb-1.0-0-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 +``` + +_Note that this simplified process only works for released versions (it +downloads a prebuilt server binary), so for example you can't use it for testing +the development branch (`dev`)._ + +_See [build.md](build.md) to build and install the app manually._ + + +## Run + +_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ + +Once installed, run from a terminal: + +```bash +scrcpy +``` + +or with arguments (here to disable audio and record to `file.mkv`): + +```bash +scrcpy --no-audio --record=file.mkv +``` + +Documentation for command line arguments is available: + - `man scrcpy` + - `scrcpy --help` + - on [github](/README.md) diff --git a/doc/macos.md b/doc/macos.md new file mode 100644 index 00000000..35d90e9d --- /dev/null +++ b/doc/macos.md @@ -0,0 +1,49 @@ +# On macOS + +## Install + +Scrcpy is available in [Homebrew]: + +```bash +brew install scrcpy +``` + +[Homebrew]: https://brew.sh/ + +You need `adb`, accessible from your `PATH`. If you don't have it yet: + +```bash +brew install android-platform-tools +``` + +Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: + +```bash +sudo port install scrcpy +``` + +[MacPorts]: https://www.macports.org/ + +_See [build.md](build.md) to build and install the app manually._ + + +## Run + +_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ + +Once installed, run from a terminal: + +```bash +scrcpy +``` + +or with arguments (here to disable audio and record to `file.mkv`): + +```bash +scrcpy --no-audio --record=file.mkv +``` + +Documentation for command line arguments is available: + - `man scrcpy` + - `scrcpy --help` + - on [github](/README.md) diff --git a/doc/mouse.md b/doc/mouse.md new file mode 100644 index 00000000..d0342954 --- /dev/null +++ b/doc/mouse.md @@ -0,0 +1,70 @@ +# Mouse + +Several mouse input modes are available: + + - `--mouse=sdk` (default) + - `--mouse=uhid` (or `-M`): simulates a physical HID mouse using the UHID + kernel module on the device + - `--mouse=aoa`: simulates a physical HID mouse using the AOAv2 protocol + - `--mouse=disabled` + + +## SDK mouse + +In this mode (`--mouse=sdk`, or if the parameter is omitted), mouse input events +are injected at the Android API level with absolute coordinates. + +Note that on some devices, an additional option must be enabled in developer +options for this mouse mode to work. See +[prerequisites](/README.md#prerequisites). + + +## Physical mouse simulation + +Two modes allow to simulate a physical HID mouse on the device. + +In these modes, the computer mouse is "captured": the mouse pointer disappears +from the computer and appears on the Android device instead. + +Special capture keys, either Alt or Super, toggle +(disable or enable) the mouse capture. Use one of them to give the control of +the mouse back to the computer. + + +### UHID + +This mode simulates a physical HID mouse using the [UHID] kernel module on the +device. + +[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt + +To enable UHID mouse, use: + +```bash +scrcpy --mouse=uhid +scrcpy -M # short version +``` + + +### AOA + +This mode simulates a physical HID mouse using the [AOAv2] protocol. + +[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support + +To enable AOA mouse, use: + +```bash +scrcpy --mouse=aoa +``` + +Contrary to the other modes, it works at the USB level directly (so it only +works over USB). + +It does not use the scrcpy server, and does not require `adb` (USB debugging). +Therefore, it is possible to control the device (but not mirror) even with USB +debugging disabled (see [OTG](otg.md)). + +Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring +(it is not possible to open a USB device if it is already open by another +process like the _adb daemon_). diff --git a/doc/otg.md b/doc/otg.md new file mode 100644 index 00000000..3c7ed467 --- /dev/null +++ b/doc/otg.md @@ -0,0 +1,37 @@ +# OTG + +By default, _scrcpy_ injects input events at the Android API level. As an +alternative, when connected over USB, it is possible to send HID events, so that +scrcpy behaves as if it was a physical keyboard and/or mouse connected to the +Android device. + +A special mode allows to control the device without mirroring, using AOA +[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa). Therefore, it is possible +to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if +the computer keyboard and mouse were plugged directly to the device via an OTG +cable. + +In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. + +This is similar to `--keyboard=aoa --mouse=aoa`, but without mirroring. + +To enable OTG mode: + +```bash +scrcpy --otg +# Pass the serial if several USB devices are available +scrcpy --otg -s 0123456789abcdef +``` + +It is possible to disable HID keyboard or HID mouse: + +```bash +scrcpy --otg --keyboard=disabled +scrcpy --otg --mouse=disabled +``` + +It only works if the device is connected over USB. + +## OTG issues on Windows + +See [FAQ](/FAQ.md#otg-issues-on-windows). diff --git a/doc/recording.md b/doc/recording.md new file mode 100644 index 00000000..216542e9 --- /dev/null +++ b/doc/recording.md @@ -0,0 +1,89 @@ +# Recording + +To record video and audio streams while mirroring: + +```bash +scrcpy --record=file.mp4 +scrcpy -r file.mkv +``` + +To record only the video: + +```bash +scrcpy --no-audio --record=file.mp4 +``` + +To record only the audio: + +```bash +scrcpy --no-video --record=file.opus +scrcpy --no-video --audio-codec=aac --record=file.aac +scrcpy --no-video --audio-codec=flac --record=file.flac +scrcpy --no-video --audio-codec=raw --record=file.wav +# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac +``` + +Timestamps are captured on the device, so [packet delay variation] does not +impact the recorded file, which is always clean (only if you use `--record` of +course, not if you capture your scrcpy window and audio output on the computer). + +[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation + + +## Format + +The video and audio streams are encoded on the device, but are muxed on the +client side. Several formats (containers) are supported: + - MP4 (`.mp4`, `.m4a`, `.aac`) + - Matroska (`.mkv`, `.mka`) + - OPUS (`.opus`) + - FLAC (`.flac`) + - WAV (`.wav`) + +The container is automatically selected based on the filename. + +It is also possible to explicitly select a container (in that case the filename +needs not end with a known extension): + +``` +scrcpy --record=file --record-format=mkv +``` + + +## Rotation + +The video can be recorded rotated. See [video +orientation](video.md#orientation). + + +## No playback + +To disable playback while recording: + +```bash +scrcpy --no-playback --record=file.mp4 +scrcpy -Nr file.mkv +# interrupt recording with Ctrl+C +``` + +It is also possible to disable video and audio playback separately: + +```bash +# Record both video and audio, but only play video +scrcpy --record=file.mkv --no-audio-playback +``` + +## Time limit + +To limit the recording time: + +```bash +scrcpy --record=file.mkv --time-limit=20 # in seconds +``` + +The `--time-limit` option is not limited to recording, it also impacts simple +mirroring: + +``` +scrcpy --time-limit=20 +``` diff --git a/doc/shortcuts.md b/doc/shortcuts.md new file mode 100644 index 00000000..8c402855 --- /dev/null +++ b/doc/shortcuts.md @@ -0,0 +1,72 @@ +# Shortcuts + +Actions can be performed on the scrcpy window using keyboard and mouse +shortcuts. + +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)_ + | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ + | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ + | 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 \| MOD+Backspace \| _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 + | Open keyboard settings (HID keyboard only) | MOD+k + | Enable/disable FPS counter (on stdout) | MOD+i + | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ + | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ + | Drag & drop APK file | Install APK from computer + | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) + +_¹Double-click on black borders to remove them._ +_²Right-click turns the screen on if it was off, presses BACK otherwise._ +_³4th and 5th mouse buttons, if your mouse has them._ +_⁴For react-native apps in development, `MENU` triggers development menu._ +_⁵Only on Android >= 7._ + +Shortcuts with repeated keys are executed 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. diff --git a/doc/tunnels.md b/doc/tunnels.md new file mode 100644 index 00000000..987a0293 --- /dev/null +++ b/doc/tunnels.md @@ -0,0 +1,123 @@ +# Tunnels + +Scrcpy is designed to mirror local Android devices. Tunnels allow to connect to +a remote device (e.g. over the Internet). + +To connect to a remote device, it is possible to connect a local `adb` client to +a remote `adb` server (provided they use the same version of the _adb_ +protocol). + + +## Remote ADB server + +To connect to a remote _adb server_, make the server listen on all interfaces: + +```bash +adb kill-server +adb -a nodaemon server start +# keep this open +``` + +**Warning: all communications between clients and the _adb server_ are +unencrypted.** + +Suppose that this server is accessible at 192.168.1.2. Then, from another +terminal, run `scrcpy`: + +```bash +# in bash +export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 +scrcpy --tunnel-host=192.168.1.2 +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037' +scrcpy --tunnel-host=192.168.1.2 +``` + +By default, `scrcpy` uses the local port used for `adb forward` tunnel +establishment (typically `27183`, see `--port`). It is also possible to force a +different tunnel port (it may be useful in more complex situations, when more +redirections are involved): + +``` +scrcpy --tunnel-port=1234 +``` + + +## SSH tunnel + +To communicate with a remote _adb server_ securely, it is preferable to use an +SSH tunnel. + +First, make sure the _adb server_ is running on the remote computer: + +```bash +adb start-server +``` + +Then, establish an SSH tunnel: + +```bash +# local 5038 --> remote 5037 +# local 27183 <-- remote 27183 +ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal, run `scrcpy`: + +```bash +# in bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy +``` + +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' +scrcpy +``` + +To avoid enabling remote port forwarding, you could force a forward connection +instead (notice the `-L` instead of `-R`): + +```bash +# local 5038 --> remote 5037 +# local 27183 --> remote 27183 +ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer +# keep this open +``` + +From another terminal, run `scrcpy`: + +```bash +# in bash +export ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy --force-adb-forward +``` + +```cmd +:: in cmd +set ADB_SERVER_SOCKET=tcp:localhost:5038 +scrcpy --force-adb-forward +``` + +```powershell +# in PowerShell +$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' +scrcpy --force-adb-forward +``` diff --git a/doc/v4l2.md b/doc/v4l2.md new file mode 100644 index 00000000..54272b2b --- /dev/null +++ b/doc/v4l2.md @@ -0,0 +1,72 @@ +# Video4Linux + +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. + +[v4l2]: https://en.wikipedia.org/wiki/Video4Linux + +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). + +If you encounter problems detecting your device with Chrome/WebRTC, you can try +`exclusive_caps` mode: + +``` +sudo modprobe v4l2loopback exclusive_caps=1 +``` + +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-video-playback # disable playback window +``` + +(replace `N` with 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] or within your video +conference tool. + +[OBS]: https://obsproject.com/ + + +## Buffering + +By default, there is no video buffering, to get the lowest possible latency. + +As for the [video display](video.md#buffering), it is possible to add +buffering to delay the v4l2 stream: + +```bash +scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +``` diff --git a/doc/video.md b/doc/video.md new file mode 100644 index 00000000..ed92cb22 --- /dev/null +++ b/doc/video.md @@ -0,0 +1,238 @@ +# Video + +## Source + +By default, scrcpy mirrors the device screen. + +It is possible to capture the device camera instead. + +See the dedicated [camera](camera.md) page. + + +## Size + +By default, scrcpy attempts to mirror at the Android device resolution. + +It might be useful to mirror at a lower definition to increase performance. To +limit both width and height to some maximum value (here 1024): + +```bash +scrcpy --max-size=1024 +scrcpy -m 1024 # short version +``` + +The other dimension is computed so that the Android device aspect ratio is +preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. + +If encoding fails, scrcpy automatically tries again with a lower definition +(unless `--no-downsize-on-error` is enabled). + + +## Bit rate + +The default video bit rate is 8 Mbps. To change it: + +```bash +scrcpy --video-bit-rate=2M +scrcpy --video-bit-rate=2000000 # equivalent +scrcpy -b 2M # short version +``` + + +## Frame rate + +The capture frame rate can be limited: + +```bash +scrcpy --max-fps=15 +``` + +The actual capture frame rate may be printed to the console: + +``` +scrcpy --print-fps +``` + +It may also be enabled or disabled at anytime with MOD+i +(see [shortcuts](shortcuts.md)). + +The frame rate is intrinsically variable: a new frame is produced only when the +screen content changes. For example, if you play a fullscreen video at 24fps on +your device, you should not get more than 24 frames per second in scrcpy. + + +## Codec + +The video codec can be selected. The possible values are `h264` (default), +`h265` and `av1`: + +```bash +scrcpy --video-codec=h264 # default +scrcpy --video-codec=h265 +scrcpy --video-codec=av1 +``` + +H265 may provide better quality, but H264 should provide lower latency. +AV1 encoders are not common on current Android devices. + +For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], +check `--video-codec-options` in the manpage or in `scrcpy --help`. + +[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat + + +## Encoder + +Several encoders may be available on the device. They can be listed by: + +```bash +scrcpy --list-encoders +``` + +Sometimes, the default encoder may have issues or even crash, so it is useful to +try another one: + +```bash +scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' +``` + + +## Orientation + +The orientation may be applied at 3 different levels: + - The [shortcut](shortcuts.md) 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` changes the mirroring orientation (the orientation + of the video sent from the device to the computer). This affects the + recording. + - `--orientation` is applied on the client side, and affects display and + recording. For the display, it can be changed dynamically using + [shortcuts](shortcuts.md). + +To lock the mirroring orientation (on the capture side): + +```bash +scrcpy --lock-video-orientation # initial (current) orientation +scrcpy --lock-video-orientation=0 # natural orientation +scrcpy --lock-video-orientation=90 # 90° clockwise +scrcpy --lock-video-orientation=180 # 180° +scrcpy --lock-video-orientation=270 # 270° clockwise +``` + +To orient the video (on the rendering side): + +```bash +scrcpy --orientation=0 +scrcpy --orientation=90 # 90° clockwise +scrcpy --orientation=180 # 180° +scrcpy --orientation=270 # 270° clockwise +scrcpy --orientation=flip0 # hflip +scrcpy --orientation=flip90 # hflip + 90° clockwise +scrcpy --orientation=flip180 # vflip (hflip + 180°) +scrcpy --orientation=flip270 # hflip + 270° clockwise +``` + +The orientation can be set separately for display and record if necessary, via +`--display-orientation` and `--record-orientation`. + +The rotation is applied to a recorded file by writing a display transformation +to the MP4 or MKV target file. Flipping is not supported, so only the 4 first +values are allowed when recording. + + +## Crop + +The device screen may be cropped to mirror only part of the screen. + +This is useful, for example, to mirror only one eye of the Oculus Go: + +```bash +scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0) +``` + +The values are expressed in the device natural orientation (portrait for a +phone, landscape for a tablet). + +If `--max-size` is also specified, resizing is applied after cropping. + + +## Display + +If several displays are available on the Android device, it is possible to +select the display to mirror: + +```bash +scrcpy --display-id=1 +``` + +The list of display ids can be retrieved by: + +```bash +scrcpy --list-displays +``` + +A secondary display may only be controlled if the device runs at least Android +10 (otherwise it is mirrored as read-only). + + +## Buffering + +By default, there is no video buffering, to get the lowest possible latency. + +Buffering can be added to delay the video stream and compensate for jitter to +get a smoother playback (see [#2464]). + +[#2464]: https://github.com/Genymobile/scrcpy/issues/2464 + +The configuration is available independently for the display, +[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. + +```bash +scrcpy --display-buffer=50 # add 50ms buffering for display +scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +scrcpy --audio-buffer=200 # set 200ms buffering for audio playback +``` + +They can be applied simultaneously: + +```bash +scrcpy --display-buffer=50 --v4l2-buffer=300 +``` + + +## No playback + +It is possible to capture an Android device without playing video or audio on +the computer. This option is useful when [recording](recording.md) or when +[v4l2](#video4linux) is enabled: + +```bash +scrcpy --v4l2-sink=/dev/video2 --no-playback +scrcpy --record=file.mkv --no-playback +# interrupt with Ctrl+C +``` + +It is also possible to disable video and audio playback separately: + +```bash +# Send video to V4L2 sink without playing it, but keep audio playback +scrcpy --v4l2-sink=/dev/video2 --no-video-playback + +# Record both video and audio, but only play video +scrcpy --record=file.mkv --no-audio-playback +``` + + +## No video + +To disable video forwarding completely, so that only audio is forwarded: + +``` +scrcpy --no-video +``` + + +## Video4Linux + +See the dedicated [Video4Linux](v4l2.md) page. diff --git a/doc/window.md b/doc/window.md new file mode 100644 index 00000000..b5b73921 --- /dev/null +++ b/doc/window.md @@ -0,0 +1,55 @@ +# Window + +## Title + +By default, the window title is the device model. It can be changed: + +```bash +scrcpy --window-title='My device' +``` + +## Position and size + +The initial window position and size may be specified: + +```bash +scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600 +``` + +## Borderless + +To disable window decorations: + +```bash +scrcpy --window-borderless +``` + +## Always on top + +To keep the window always on top: + +```bash +scrcpy --always-on-top +``` + +## Fullscreen + +The app may be started directly in fullscreen: + +```bash +scrcpy --fullscreen +scrcpy -f # short version +``` + +Fullscreen mode can then be toggled dynamically with MOD+f +(see [shortcuts](shortcuts.md)). + + +## Disable screensaver + +By default, _scrcpy_ does not prevent the screensaver from running on the +computer. To disable it: + +```bash +scrcpy --disable-screensaver +``` diff --git a/doc/windows.md b/doc/windows.md new file mode 100644 index 00000000..a3711f26 --- /dev/null +++ b/doc/windows.md @@ -0,0 +1,96 @@ +# On Windows + +## Install + +Download the [latest release]: + + - [`scrcpy-win64-v2.4.zip`][direct-win64] (64-bit) + SHA-256: `9dc56f21bfa455352ec0c58b40feaf2fb02d67372910a4235e298ece286ff3a9` + - [`scrcpy-win32-v2.4.zip`][direct-win32] (32-bit) + SHA-256: `cf92acc45eef37c6ee2db819f92e420ced3bc50f1348dd57f7d6ca1fc80f6116` + +[latest release]: https://github.com/Genymobile/scrcpy/releases/latest +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win64-v2.4.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win32-v2.4.zip + +and extract it. + +Alternatively, you could install it from packages manager, like [Winget]: + +```bash +winget install scrcpy +``` + +or [Chocolatey]: + +```bash +choco install scrcpy +choco install adb # if you don't have it yet +``` + +or [Scoop]: + + +```bash +scoop install scrcpy +scoop install adb # if you don't have it yet +``` + +[Winget]: https://github.com/microsoft/winget-cli +[Chocolatey]: https://chocolatey.org/ +[Scoop]: https://scoop.sh + +_See [build.md](build.md) to build and install the app manually._ + + +## Run + +_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._ + +Scrcpy is a command line application: it is mainly intended to be executed from +a terminal with command line arguments. + +To open a terminal at the expected location, double-click on +`open_a_terminal_here.bat` in your scrcpy directory, then type your command. For +example, without arguments: + +```bash +scrcpy +``` + +or with arguments (here to disable audio and record to `file.mkv`): + +``` +scrcpy --no-audio --record=file.mkv +``` + +Documentation for command line arguments is available: + - `scrcpy --help` + - on [github](/README.md) + +To start scrcpy directly without opening a terminal, double-click on one of +these files: + - `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy + terminates, unless an error occurs); + - `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error + message). + +_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would +close immediately and you won't have time to read any error message (this +executable is intended to be run from the terminal). Use `scrcpy-console.bat` +instead._ + +If you plan to always use the same arguments, create a file `myscrcpy.bat` +(enable [show file extensions] to avoid confusion) containing your command, For +example: + +```bash +scrcpy --prefer-text --turn-screen-off --stay-awake +``` + +[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ + +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. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4b44297..e411586a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/install_release.sh b/install_release.sh index 9158bdd4..0be5675c 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.18/scrcpy-server-v1.18 -PREBUILT_SERVER_SHA256=641c5c6beda9399dfae72d116f5ff43b5ed1059d871c9ebc3f47610fd33c51a3 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4 +PREBUILT_SERVER_SHA256=93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server @@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check echo "[scrcpy] Building client..." rm -rf "$BUILDDIR" -meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \ +meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \ -Dprebuilt_server=scrcpy-server cd "$BUILDDIR" ninja diff --git a/meson.build b/meson.build index fc61bcea..22d0f4ef 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '1.19', + version: '2.4', meson_version: '>= 0.48', default_options: [ 'c_std=c11', @@ -7,6 +7,8 @@ project('scrcpy', 'c', 'b_ndebug=if-release', ]) +add_project_arguments('-Wmissing-prototypes', language: 'c') + if get_option('compile_app') subdir('app') endif @@ -14,5 +16,3 @@ endif if get_option('compile_server') subdir('server') endif - -run_target('run', command: ['scripts/run-scrcpy.sh']) diff --git a/meson_options.txt b/meson_options.txt index 66ad5b25..d1030694 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,7 +1,8 @@ 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('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('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")') +option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') +option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') diff --git a/prebuilt-deps/.gitignore b/prebuilt-deps/.gitignore deleted file mode 100644 index 934bc04c..00000000 --- a/prebuilt-deps/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -* -!/.gitignore -!/Makefile -!/prepare-dep diff --git a/prebuilt-deps/Makefile b/prebuilt-deps/Makefile deleted file mode 100644 index dced047c..00000000 --- a/prebuilt-deps/Makefile +++ /dev/null @@ -1,40 +0,0 @@ -.PHONY: prepare-win32 prepare-win64 \ - prepare-ffmpeg-shared-win32 \ - prepare-ffmpeg-dev-win32 \ - prepare-ffmpeg-shared-win64 \ - prepare-ffmpeg-dev-win64 \ - prepare-sdl2 \ - prepare-adb - -prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32 prepare-adb -prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb - -prepare-ffmpeg-shared-win32: - @./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://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://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://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.16-mingw.tar.gz \ - 2bfe48628aa9635c12eac7d421907e291525de1d0b04b3bca4a5bd6e6c881a6f \ - SDL2-2.0.16 - -prepare-adb: - @./prepare-dep https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip \ - 0f4b8fdd26af2c3733539d6eebb3c2ed499ea1d4bb1f4e0ecc2d6016961a6e24 \ - platform-tools diff --git a/prebuilt-deps/prepare-dep b/prebuilt-deps/prepare-dep deleted file mode 100755 index f152e6cf..00000000 --- a/prebuilt-deps/prepare-dep +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -e -url="$1" -sum="$2" -dir="$3" - -checksum() { - local file="$1" - local sum="$2" - echo "$file: verifying checksum..." - echo "$sum $file" | sha256sum -c -} - -get_file() { - local url="$1" - local file="$2" - local sum="$3" - if [[ -f "$file" ]] - then - echo "$file: found" - else - echo "$file: not found, downloading..." - wget "$url" -O "$file" - fi - checksum "$file" "$sum" -} - -extract() { - local file="$1" - echo "Extracting $file..." - if [[ "$file" == *.zip ]] - then - unzip -q "$file" - elif [[ "$file" == *.tar.gz ]] - then - tar xf "$file" - else - echo "Unsupported file: $file" - return 1 - fi -} - -get_dep() { - local url="$1" - local sum="$2" - local dir="$3" - local file="${url##*/}" - if [[ -d "$dir" ]] - then - echo "$dir: found" - else - echo "$dir: not found" - get_file "$url" "$file" "$sum" - extract "$file" - fi -} - -get_dep "$url" "$sum" "$dir" diff --git a/release.mk b/release.mk index e327654c..89f3da21 100644 --- a/release.mk +++ b/release.mk @@ -11,7 +11,7 @@ .PHONY: default clean \ test \ build-server \ - prepare-deps-win32 prepare-deps-win64 \ + prepare-deps \ build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ @@ -24,13 +24,13 @@ SERVER_BUILD_DIR := build-server WIN32_BUILD_DIR := build-win32 WIN64_BUILD_DIR := build-win64 -DIST := dist -WIN32_TARGET_DIR := scrcpy-win32 -WIN64_TARGET_DIR := scrcpy-win64 - VERSION := $(shell git describe --tags --always) -WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip -WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip + +DIST := dist +WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) +WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) +WIN32_TARGET := $(WIN32_TARGET_DIR).zip +WIN64_TARGET := $(WIN64_TARGET_DIR).zip RELEASE_DIR := release-$(VERSION) @@ -53,77 +53,79 @@ clean: test: [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ - meson "$(TEST_BUILD_DIR)" -Db_sanitize=address ) + meson setup "$(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 ) + meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: - -$(MAKE) -C prebuilt-deps prepare-win32 + @app/deps/adb.sh win32 + @app/deps/sdl.sh win32 + @app/deps/ffmpeg.sh win32 + @app/deps/libusb.sh win32 + +prepare-deps-win64: + @app/deps/adb.sh win64 + @app/deps/sdl.sh win64 + @app/deps/ffmpeg.sh win64 + @app/deps/libusb.sh win64 build-win32: prepare-deps-win32 - [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ - meson "$(WIN32_BUILD_DIR)" \ - --cross-file cross_win32.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ - -Dcompile_server=false \ - -Dportable=true ) + rm -rf "$(WIN32_BUILD_DIR)" + mkdir -p "$(WIN32_BUILD_DIR)/local" + meson setup "$(WIN32_BUILD_DIR)" \ + --pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \ + -Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \ + --cross-file=cross_win32.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true ninja -C "$(WIN32_BUILD_DIR)" -prepare-deps-win64: - -$(MAKE) -C prebuilt-deps prepare-win64 - build-win64: prepare-deps-win64 - [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ - meson "$(WIN64_BUILD_DIR)" \ - --cross-file cross_win64.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcrossbuild_windows=true \ - -Dcompile_server=false \ - -Dportable=true ) + rm -rf "$(WIN64_BUILD_DIR)" + mkdir -p "$(WIN64_BUILD_DIR)/local" + meson setup "$(WIN64_BUILD_DIR)" \ + --pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \ + -Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \ + --cross-file=cross_win64.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true ninja -C "$(WIN64_BUILD_DIR)" 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 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.16/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" 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 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.16/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 - cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ - zip -r "../$(WIN32_TARGET)" . + cd "$(DIST)"; \ + zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" zip-win64: dist-win64 - cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ - zip -r "../$(WIN64_TARGET)" . + cd "$(DIST)"; \ + zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" diff --git a/run b/run index 628c5c7e..56f0a4e1 100755 --- a/run +++ b/run @@ -20,4 +20,6 @@ then exit 1 fi -SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" "$BUILDDIR/app/scrcpy" "$@" +SCRCPY_ICON_PATH="app/data/icon.png" \ +SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ +"$BUILDDIR/app/scrcpy" "$@" diff --git a/scripts/run-scrcpy.sh b/scripts/run-scrcpy.sh deleted file mode 100755 index e93b639f..00000000 --- a/scripts/run-scrcpy.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/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 4dbeaf14..911bf254 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -1,13 +1,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 30 + namespace 'com.genymobile.scrcpy' + compileSdk 34 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 30 - versionCode 11900 - versionName "1.19-ws5" + targetSdkVersion 34 + versionCode 20400 + versionName "2.4-ws5" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -16,6 +17,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + buildFeatures { + buildConfig true + aidl true + } } dependencies { @@ -23,7 +28,7 @@ dependencies { implementation 'org.java-websocket:Java-WebSocket:1.4.0' implementation 'org.slf4j:slf4j-api:1.7.25' implementation 'uk.uuid.slf4j:slf4j-android:1.7.25-1' - testImplementation 'junit:junit:4.13' + testImplementation 'junit:junit:4.13.2' } apply from: "$project.rootDir/config/android-checkstyle.gradle" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 05b822ba..7f7d7921 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,24 +12,29 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=1.19 +SCRCPY_VERSION_NAME=2.4 -PLATFORM=${ANDROID_PLATFORM:-30} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} +PLATFORM=${ANDROID_PLATFORM:-34} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} +BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" +GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server +ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" +LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" echo "Build dir: $BUILD_DIR" -rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex -mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" +rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex +mkdir -p "$CLASSES_DIR" +mkdir -p "$GEN_DIR/com/genymobile/scrcpy" -<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" +<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java" package com.genymobile.scrcpy; public final class BuildConfig { @@ -40,30 +45,49 @@ EOF 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" \ +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ android/content/IOnPrimaryClipChangedListener.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" \ - -cp "$CLASSES_DIR" -d "$CLASSES_DIR" -source 1.8 -target 1.8 \ +javac -bootclasspath "$ANDROID_JAR" \ + -cp "$LAMBDA_JAR:$GEN_DIR" \ + -d "$CLASSES_DIR" \ + -source 1.8 -target 1.8 \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/wrappers/*.java echo "Dexing..." cd "$CLASSES_DIR" -"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ - --output "$BUILD_DIR/classes.dex" \ - android/view/*.class \ - android/content/*.class \ - com/genymobile/scrcpy/*.class \ - com/genymobile/scrcpy/wrappers/*.class - -echo "Archiving..." -cd "$BUILD_DIR" -jar cvf "$SERVER_BINARY" classes.dex -rm -rf classes.dex classes + +if [[ $PLATFORM -lt 31 ]] +then + # use dx + "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ + android/view/*.class \ + android/content/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + + echo "Archiving..." + cd "$BUILD_DIR" + jar cvf "$SERVER_BINARY" classes.dex + rm -rf classes.dex +else + # use d8 + "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ + --output "$BUILD_DIR/classes.zip" \ + android/view/*.class \ + android/content/*.class \ + com/genymobile/scrcpy/*.class \ + com/genymobile/scrcpy/wrappers/*.class + + cd "$BUILD_DIR" + mv classes.zip "$SERVER_BINARY" +fi + +rm -rf "$GEN_DIR" "$CLASSES_DIR" echo "Server generated in $BUILD_DIR/$SERVER_BINARY" diff --git a/server/meson.build b/server/meson.build index 984daf3b..42b97981 100644 --- a/server/meson.build +++ b/server/meson.build @@ -13,8 +13,8 @@ if prebuilt_server == '' install_dir: 'share/scrcpy') else if not prebuilt_server.startswith('/') - # relative path needs some trick - prebuilt_server = meson.source_root() + '/' + prebuilt_server + # prebuilt server path is relative to the root scrcpy directory + prebuilt_server = '../' + prebuilt_server endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, diff --git a/server/src/main/AndroidManifest.xml b/server/src/main/AndroidManifest.xml index ccd69d2f..a94ad86b 100644 --- a/server/src/main/AndroidManifest.xml +++ b/server/src/main/AndroidManifest.xml @@ -1,2 +1,2 @@ - + diff --git a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl new file mode 100644 index 00000000..2c91149d --- /dev/null +++ b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2019 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.view; + +/** + * {@hide} + */ +oneway interface IDisplayFoldListener +{ + /** Called when the foldedness of a display changes */ + void onDisplayFoldChanged(int displayId, boolean folded); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java new file mode 100644 index 00000000..d5da6a90 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java @@ -0,0 +1,18 @@ +package com.genymobile.scrcpy; + +public interface AsyncProcessor { + interface TerminationListener { + /** + * Notify processor termination + * + * @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server. + */ + void onTerminated(boolean fatalError); + } + + void start(TerminationListener listener); + + void stop(); + + void join() throws InterruptedException; +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java new file mode 100644 index 00000000..3934ad49 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -0,0 +1,179 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ComponentName; +import android.content.Intent; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.AudioTimestamp; +import android.media.MediaCodec; +import android.os.Build; +import android.os.SystemClock; + +import java.nio.ByteBuffer; + +public final class AudioCapture { + + public static final int SAMPLE_RATE = 48000; + public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; + public static final int CHANNELS = 2; + public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT; + public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; + public static final int BYTES_PER_SAMPLE = 2; + + // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). + // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we + // receive 4 successive blocks without waiting, then we wait for the 4 next ones). + public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + + private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) + + private final int audioSource; + + private AudioRecord recorder; + + private final AudioTimestamp timestamp = new AudioTimestamp(); + private long previousRecorderTimestamp = -1; + private long previousPts = 0; + private long nextPts = 0; + + public AudioCapture(AudioSource audioSource) { + this.audioSource = audioSource.value(); + } + + private static AudioFormat createAudioFormat() { + AudioFormat.Builder builder = new AudioFormat.Builder(); + builder.setEncoding(ENCODING); + builder.setSampleRate(SAMPLE_RATE); + builder.setChannelMask(CHANNEL_CONFIG); + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.M) + @SuppressLint({"WrongConstant", "MissingPermission"}) + private static AudioRecord createAudioRecord(int audioSource) { + AudioRecord.Builder builder = new AudioRecord.Builder(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On older APIs, Workarounds.fillAppInfo() must be called beforehand + builder.setContext(FakeContext.get()); + } + builder.setAudioSource(audioSource); + builder.setAudioFormat(createAudioFormat()); + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING); + // This buffer size does not impact latency + builder.setBufferSizeInBytes(8 * minBufferSize); + return builder.build(); + } + + private static void startWorkaroundAndroid11() { + // Android 11 requires Apps to be at foreground to record audio. + // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. + // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android + // shell ("com.android.shell"). + // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the + // foreground. + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); + ServiceManager.getActivityManager().startActivity(intent); + } + + private static void stopWorkaroundAndroid11() { + ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); + } + + private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException { + while (attempts-- > 0) { + // Wait for activity to start + SystemClock.sleep(delayMs); + try { + startRecording(); + return; // it worked + } catch (UnsupportedOperationException e) { + if (attempts == 0) { + Ln.e("Failed to start audio capture"); + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + + "scrcpy."); + throw new AudioCaptureForegroundException(); + } else { + Ln.d("Failed to start audio capture, retrying..."); + } + } + } + } + + private void startRecording() { + try { + recorder = createAudioRecord(audioSource); + } catch (NullPointerException e) { + // Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones: + // - + // - + recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING); + } + recorder.startRecording(); + } + + public void start() throws AudioCaptureForegroundException { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + startWorkaroundAndroid11(); + try { + tryStartRecording(5, 100); + } finally { + stopWorkaroundAndroid11(); + } + } else { + startRecording(); + } + } + + public void stop() { + if (recorder != null) { + // Will call .stop() if necessary, without throwing an IllegalStateException + recorder.release(); + } + } + + @TargetApi(Build.VERSION_CODES.N) + public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) { + int r = recorder.read(directBuffer, MAX_READ_SIZE); + if (r <= 0) { + return r; + } + + long pts; + + int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); + if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { + pts = timestamp.nanoTime / 1000; + previousRecorderTimestamp = timestamp.nanoTime; + } else { + if (nextPts == 0) { + Ln.w("Could not get initial audio timestamp"); + nextPts = System.nanoTime() / 1000; + } + // compute from previous timestamp and packet size + pts = nextPts; + } + + long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); + nextPts = pts + durationUs; + + if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { + // Audio PTS may come from two sources: + // - recorder.getTimestamp() if the call works; + // - an estimation from the previous PTS and the packet size as a fallback. + // + // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. + pts = previousPts + ONE_SAMPLE_US; + } + previousPts = pts; + + outBufferInfo.set(0, r, pts, 0); + return r; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java b/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java new file mode 100644 index 00000000..baa7d846 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCaptureForegroundException.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy; + +/** + * Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground. + */ +public class AudioCaptureForegroundException extends Exception { +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java new file mode 100644 index 00000000..b4ea3680 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -0,0 +1,49 @@ +package com.genymobile.scrcpy; + +import android.media.MediaFormat; + +public enum AudioCodec implements Codec { + OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), + AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), + FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC), + RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); + + private final int id; // 4-byte ASCII representation of the name + private final String name; + private final String mimeType; + + AudioCodec(int id, String name, String mimeType) { + this.id = id; + this.name = name; + this.mimeType = mimeType; + } + + @Override + public Type getType() { + return Type.AUDIO; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getMimeType() { + return mimeType; + } + + public static AudioCodec findByName(String name) { + for (AudioCodec codec : values()) { + if (codec.name.equals(name)) { + return codec; + } + } + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java new file mode 100644 index 00000000..0b59369b --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -0,0 +1,329 @@ +package com.genymobile.scrcpy; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +public final class AudioEncoder implements AsyncProcessor { + + private static class InputTask { + private final int index; + + InputTask(int index) { + this.index = index; + } + } + + private static class OutputTask { + private final int index; + private final MediaCodec.BufferInfo bufferInfo; + + OutputTask(int index, MediaCodec.BufferInfo bufferInfo) { + this.index = index; + this.bufferInfo = bufferInfo; + } + } + + private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; + private static final int CHANNELS = AudioCapture.CHANNELS; + + private final AudioCapture capture; + private final Streamer streamer; + private final int bitRate; + private final List codecOptions; + private final String encoderName; + + // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). + // So many pending tasks would lead to an unacceptable delay anyway. + private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); + private final BlockingQueue outputTasks = new ArrayBlockingQueue<>(64); + + private Thread thread; + private HandlerThread mediaCodecThread; + + private Thread inputThread; + private Thread outputThread; + + private boolean ended; + + public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List codecOptions, String encoderName) { + this.capture = capture; + this.streamer = streamer; + this.bitRate = bitRate; + this.codecOptions = codecOptions; + this.encoderName = encoderName; + } + + private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, mimeType); + format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS); + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE); + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + String key = option.getKey(); + Object value = option.getValue(); + CodecUtils.setCodecOption(format, key, value); + Ln.d("Audio codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + } + + return format; + } + + @TargetApi(Build.VERSION_CODES.N) + private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { + final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + + while (!Thread.currentThread().isInterrupted()) { + InputTask task = inputTasks.take(); + ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); + int r = capture.read(buffer, bufferInfo); + if (r <= 0) { + throw new IOException("Could not read audio: " + r); + } + + mediaCodec.queueInputBuffer(task.index, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs, bufferInfo.flags); + } + } + + private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException { + streamer.writeAudioHeader(); + + while (!Thread.currentThread().isInterrupted()) { + OutputTask task = outputTasks.take(); + ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); + try { + streamer.writePacket(buffer, task.bufferInfo); + } finally { + mediaCodec.releaseOutputBuffer(task.index, false); + } + } + } + + @Override + public void start(TerminationListener listener) { + thread = new Thread(() -> { + boolean fatalError = false; + try { + encode(); + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + fatalError = true; + } catch (AudioCaptureForegroundException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } catch (IOException e) { + Ln.e("Audio encoding error", e); + fatalError = true; + } finally { + Ln.d("Audio encoder stopped"); + listener.onTerminated(fatalError); + } + }, "audio-encoder"); + thread.start(); + } + + @Override + public void stop() { + if (thread != null) { + // Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates + end(); + } + } + + @Override + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } + + private synchronized void end() { + ended = true; + notify(); + } + + private synchronized void waitEnded() { + try { + while (!ended) { + wait(); + } + } catch (InterruptedException e) { + // ignore + } + } + + @TargetApi(Build.VERSION_CODES.M) + public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + streamer.writeDisableStream(false); + return; + } + + MediaCodec mediaCodec = null; + + boolean mediaCodecStarted = false; + try { + Codec codec = streamer.getCodec(); + mediaCodec = createMediaCodec(codec, encoderName); + + mediaCodecThread = new HandlerThread("media-codec"); + mediaCodecThread.start(); + + MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions); + mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper())); + mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + + capture.start(); + + final MediaCodec mediaCodecRef = mediaCodec; + inputThread = new Thread(() -> { + try { + inputThread(mediaCodecRef, capture); + } catch (IOException | InterruptedException e) { + Ln.e("Audio capture error", e); + } finally { + end(); + } + }, "audio-in"); + + outputThread = new Thread(() -> { + try { + outputThread(mediaCodecRef); + } catch (InterruptedException e) { + // this is expected on close + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Audio encoding error", e); + } + } finally { + end(); + } + }, "audio-out"); + + mediaCodec.start(); + mediaCodecStarted = true; + inputThread.start(); + outputThread.start(); + + waitEnded(); + } catch (ConfigurationException e) { + // Notify the error to make scrcpy exit + streamer.writeDisableStream(true); + throw e; + } catch (Throwable e) { + // Notify the client that the audio could not be captured + streamer.writeDisableStream(false); + throw e; + } finally { + // Cleanup everything (either at the end or on error at any step of the initialization) + if (mediaCodecThread != null) { + Looper looper = mediaCodecThread.getLooper(); + if (looper != null) { + looper.quitSafely(); + } + } + if (inputThread != null) { + inputThread.interrupt(); + } + if (outputThread != null) { + outputThread.interrupt(); + } + + try { + if (mediaCodecThread != null) { + mediaCodecThread.join(); + } + if (inputThread != null) { + inputThread.join(); + } + if (outputThread != null) { + outputThread.join(); + } + } catch (InterruptedException e) { + // Should never happen + throw new AssertionError(e); + } + + if (mediaCodec != null) { + if (mediaCodecStarted) { + mediaCodec.stop(); + } + mediaCodec.release(); + } + if (capture != null) { + capture.stop(); + } + } + } + + private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { + if (encoderName != null) { + Ln.d("Creating audio encoder by name: '" + encoderName + "'"); + try { + return MediaCodec.createByCodecName(encoderName); + } catch (IllegalArgumentException e) { + Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); + throw new ConfigurationException("Unknown encoder: " + encoderName); + } catch (IOException e) { + Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); + throw e; + } + } + + try { + MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); + Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'"); + return mediaCodec; + } catch (IOException | IllegalArgumentException e) { + Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage()); + throw e; + } + } + + private final class EncoderCallback extends MediaCodec.Callback { + @TargetApi(Build.VERSION_CODES.N) + @Override + public void onInputBufferAvailable(MediaCodec codec, int index) { + try { + inputTasks.put(new InputTask(index)); + } catch (InterruptedException e) { + end(); + } + } + + @Override + public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) { + try { + outputTasks.put(new OutputTask(index, bufferInfo)); + } catch (InterruptedException e) { + end(); + } + } + + @Override + public void onError(MediaCodec codec, MediaCodec.CodecException e) { + Ln.e("MediaCodec error", e); + end(); + } + + @Override + public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { + // ignore + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java new file mode 100644 index 00000000..7e052f32 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -0,0 +1,93 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodec; +import android.os.Build; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public final class AudioRawRecorder implements AsyncProcessor { + + private final AudioCapture capture; + private final Streamer streamer; + + private Thread thread; + + public AudioRawRecorder(AudioCapture capture, Streamer streamer) { + this.capture = capture; + this.streamer = streamer; + } + + private void record() throws IOException, AudioCaptureForegroundException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Ln.w("Audio disabled: it is not supported before Android 11"); + streamer.writeDisableStream(false); + return; + } + + final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE); + final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + + try { + try { + capture.start(); + } catch (Throwable t) { + // Notify the client that the audio could not be captured + streamer.writeDisableStream(false); + throw t; + } + + streamer.writeAudioHeader(); + while (!Thread.currentThread().isInterrupted()) { + buffer.position(0); + int r = capture.read(buffer, bufferInfo); + if (r < 0) { + throw new IOException("Could not read audio: " + r); + } + buffer.limit(r); + + streamer.writePacket(buffer, bufferInfo); + } + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Audio capture error", e); + } + } finally { + capture.stop(); + } + } + + @Override + public void start(TerminationListener listener) { + thread = new Thread(() -> { + boolean fatalError = false; + try { + record(); + } catch (AudioCaptureForegroundException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } catch (Throwable t) { + Ln.e("Audio recording error", t); + fatalError = true; + } finally { + Ln.d("Audio recorder stopped"); + listener.onTerminated(fatalError); + } + }, "audio-raw"); + thread.start(); + } + + @Override + public void stop() { + if (thread != null) { + thread.interrupt(); + } + } + + @Override + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java new file mode 100644 index 00000000..466ea297 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AudioSource.java @@ -0,0 +1,30 @@ +package com.genymobile.scrcpy; + +import android.media.MediaRecorder; + +public enum AudioSource { + OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), + MIC("mic", MediaRecorder.AudioSource.MIC); + + private final String name; + private final int value; + + AudioSource(String name, int value) { + this.name = name; + this.value = value; + } + + int value() { + return value; + } + + static AudioSource findByName(String name) { + for (AudioSource audioSource : AudioSource.values()) { + if (name.equals(audioSource.name)) { + return audioSource; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Binary.java b/server/src/main/java/com/genymobile/scrcpy/Binary.java new file mode 100644 index 00000000..29534f59 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Binary.java @@ -0,0 +1,38 @@ +package com.genymobile.scrcpy; + +public final class Binary { + private Binary() { + // not instantiable + } + + public static int toUnsigned(short value) { + return value & 0xffff; + } + + public static int toUnsigned(byte value) { + return value & 0xff; + } + + /** + * Convert unsigned 16-bit fixed-point to a float between 0 and 1 + * + * @param value encoded value + * @return Float value between 0 and 1 + */ + public static float u16FixedPointToFloat(short value) { + int unsignedShort = Binary.toUnsigned(value); + // 0x1p16f is 2^16 as float + return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f); + } + + /** + * Convert signed 16-bit fixed-point to a float between -1 and 1 + * + * @param value encoded value + * @return Float value between -1 and 1 + */ + public static float i16FixedPointToFloat(short value) { + // 0x1p15f is 2^15 as float + return value == 0x7fff ? 1f : (value / 0x1p15f); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java b/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java new file mode 100644 index 00000000..4fdf4c74 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraAspectRatio.java @@ -0,0 +1,37 @@ +package com.genymobile.scrcpy; + +public final class CameraAspectRatio { + private static final float SENSOR = -1; + + private float ar; + + private CameraAspectRatio(float ar) { + this.ar = ar; + } + + public static CameraAspectRatio fromFloat(float ar) { + if (ar < 0) { + throw new IllegalArgumentException("Invalid aspect ratio: " + ar); + } + return new CameraAspectRatio(ar); + } + + public static CameraAspectRatio fromFraction(int w, int h) { + if (w <= 0 || h <= 0) { + throw new IllegalArgumentException("Invalid aspect ratio: " + w + ":" + h); + } + return new CameraAspectRatio((float) w / h); + } + + public static CameraAspectRatio sensorAspectRatio() { + return new CameraAspectRatio(SENSOR); + } + + public boolean isSensor() { + return ar == SENSOR; + } + + public float getAspectRatio() { + return ar; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java new file mode 100644 index 00000000..a1003829 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -0,0 +1,351 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.MediaCodec; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Range; +import android.view.Surface; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +public class CameraCapture extends SurfaceCapture { + + private final String explicitCameraId; + private final CameraFacing cameraFacing; + private final Size explicitSize; + private int maxSize; + private final CameraAspectRatio aspectRatio; + private final int fps; + private final boolean highSpeed; + + private String cameraId; + private Size size; + + private HandlerThread cameraThread; + private Handler cameraHandler; + private CameraDevice cameraDevice; + private Executor cameraExecutor; + + private final AtomicBoolean disconnected = new AtomicBoolean(); + + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps, + boolean highSpeed) { + this.explicitCameraId = explicitCameraId; + this.cameraFacing = cameraFacing; + this.explicitSize = explicitSize; + this.maxSize = maxSize; + this.aspectRatio = aspectRatio; + this.fps = fps; + this.highSpeed = highSpeed; + } + + @Override + public void init() throws IOException { + cameraThread = new HandlerThread("camera"); + cameraThread.start(); + cameraHandler = new Handler(cameraThread.getLooper()); + cameraExecutor = new HandlerExecutor(cameraHandler); + + try { + cameraId = selectCamera(explicitCameraId, cameraFacing); + if (cameraId == null) { + throw new IOException("No matching camera found"); + } + + size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); + if (size == null) { + throw new IOException("Could not select camera size"); + } + + Ln.i("Using camera '" + cameraId + "'"); + cameraDevice = openCamera(cameraId); + } catch (CameraAccessException | InterruptedException e) { + throw new IOException(e); + } + } + + private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { + if (explicitCameraId != null) { + return explicitCameraId; + } + + CameraManager cameraManager = ServiceManager.getCameraManager(); + + String[] cameraIds = cameraManager.getCameraIdList(); + if (cameraFacing == null) { + // Use the first one + return cameraIds.length > 0 ? cameraIds[0] : null; + } + + for (String cameraId : cameraIds) { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (cameraFacing.value() == facing) { + return cameraId; + } + } + + // Not found + return null; + } + + @TargetApi(Build.VERSION_CODES.N) + private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed) + throws CameraAccessException { + if (explicitSize != null) { + return explicitSize; + } + + CameraManager cameraManager = ServiceManager.getCameraManager(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class); + Stream stream = Arrays.stream(sizes); + if (maxSize > 0) { + stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); + } + + Float targetAspectRatio = resolveAspectRatio(aspectRatio, characteristics); + if (targetAspectRatio != null) { + stream = stream.filter(it -> { + float ar = ((float) it.getWidth() / it.getHeight()); + float arRatio = ar / targetAspectRatio; + // Accept if the aspect ratio is the target aspect ratio + or - 10% + return arRatio >= 0.9f && arRatio <= 1.1f; + }); + } + + Optional selected = stream.max((s1, s2) -> { + // Greater width is better + int cmp = Integer.compare(s1.getWidth(), s2.getWidth()); + if (cmp != 0) { + return cmp; + } + + if (targetAspectRatio != null) { + // Closer to the target aspect ratio is better + float ar1 = ((float) s1.getWidth() / s1.getHeight()); + float arRatio1 = ar1 / targetAspectRatio; + float distance1 = Math.abs(1 - arRatio1); + + float ar2 = ((float) s2.getWidth() / s2.getHeight()); + float arRatio2 = ar2 / targetAspectRatio; + float distance2 = Math.abs(1 - arRatio2); + + // Reverse the order because lower distance is better + cmp = Float.compare(distance2, distance1); + if (cmp != 0) { + return cmp; + } + } + + // Greater height is better + return Integer.compare(s1.getHeight(), s2.getHeight()); + }); + + if (selected.isPresent()) { + android.util.Size size = selected.get(); + return new Size(size.getWidth(), size.getHeight()); + } + + // Not found + return null; + } + + private static Float resolveAspectRatio(CameraAspectRatio ratio, CameraCharacteristics characteristics) { + if (ratio == null) { + return null; + } + + if (ratio.isSensor()) { + Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + return (float) activeSize.width() / activeSize.height(); + } + + return ratio.getAspectRatio(); + } + + @Override + public void start(Surface surface) throws IOException { + try { + CameraCaptureSession session = createCaptureSession(cameraDevice, surface); + CaptureRequest request = createCaptureRequest(surface); + setRepeatingRequest(session, request); + } catch (CameraAccessException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void release() { + if (cameraDevice != null) { + cameraDevice.close(); + } + if (cameraThread != null) { + cameraThread.quitSafely(); + } + } + + @Override + public Size getSize() { + return size; + } + + @Override + public boolean setMaxSize(int maxSize) { + if (explicitSize != null) { + return false; + } + + this.maxSize = maxSize; + try { + size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed); + return size != null; + } catch (CameraAccessException e) { + Ln.w("Could not select camera size", e); + return false; + } + } + + @SuppressLint("MissingPermission") + @TargetApi(Build.VERSION_CODES.S) + private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() { + @Override + public void onOpened(CameraDevice camera) { + Ln.d("Camera opened successfully"); + future.complete(camera); + } + + @Override + public void onDisconnected(CameraDevice camera) { + Ln.w("Camera disconnected"); + disconnected.set(true); + requestReset(); + } + + @Override + public void onError(CameraDevice camera, int error) { + int cameraAccessExceptionErrorCode; + switch (error) { + case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE: + cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_IN_USE; + break; + case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE: + cameraAccessExceptionErrorCode = CameraAccessException.MAX_CAMERAS_IN_USE; + break; + case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED: + cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_DISABLED; + break; + case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE: + case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE: + default: + cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_ERROR; + break; + } + future.completeExceptionally(new CameraAccessException(cameraAccessExceptionErrorCode)); + } + }, cameraHandler); + + try { + return future.get(); + } catch (ExecutionException e) { + throw (CameraAccessException) e.getCause(); + } + } + + @TargetApi(Build.VERSION_CODES.S) + private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + OutputConfiguration outputConfig = new OutputConfiguration(surface); + List outputs = Arrays.asList(outputConfig); + + int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR; + SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + future.complete(session); + } + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); + } + }); + + camera.createCaptureSession(sessionConfig); + + try { + return future.get(); + } catch (ExecutionException e) { + throw (CameraAccessException) e.getCause(); + } + } + + private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException { + CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); + requestBuilder.addTarget(surface); + + if (fps > 0) { + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps)); + } + + return requestBuilder.build(); + } + + @TargetApi(Build.VERSION_CODES.S) + private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { + CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) { + // Called for each frame captured, do nothing + } + + @Override + public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { + Ln.w("Camera capture failed: frame " + failure.getFrameNumber()); + } + }; + + if (highSpeed) { + CameraConstrainedHighSpeedCaptureSession highSpeedSession = (CameraConstrainedHighSpeedCaptureSession) session; + List requests = highSpeedSession.createHighSpeedRequestList(request); + highSpeedSession.setRepeatingBurst(requests, callback, cameraHandler); + } else { + session.setRepeatingRequest(request, callback, cameraHandler); + } + } + + @Override + public boolean isClosed() { + return disconnected.get(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java new file mode 100644 index 00000000..b7e8daa5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.hardware.camera2.CameraCharacteristics; + +public enum CameraFacing { + FRONT("front", CameraCharacteristics.LENS_FACING_FRONT), + BACK("back", CameraCharacteristics.LENS_FACING_BACK), + @SuppressLint("InlinedApi") // introduced in API 23 + EXTERNAL("external", CameraCharacteristics.LENS_FACING_EXTERNAL); + + private final String name; + private final int value; + + CameraFacing(String name, int value) { + this.name = name; + this.value = value; + } + + int value() { + return value; + } + + static CameraFacing findByName(String name) { + for (CameraFacing facing : CameraFacing.values()) { + if (name.equals(facing.name)) { + return facing; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ec61a1c0..f9b1efd6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -1,14 +1,8 @@ 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; +import java.io.OutputStream; /** * Handle the cleanup of scrcpy, even if the main process is killed. @@ -17,134 +11,64 @@ import java.io.IOException; */ 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 static final int MSG_TYPE_MASK = 0b11; + private static final int MSG_TYPE_RESTORE_STAY_ON = 0; + private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; + private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; + private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; - private boolean hasWork() { - return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; - } + private static final int MSG_PARAM_SHIFT = 2; - @Override - public int describeContents() { - return 0; - } + private final OutputStream out; - byte[] serialize() { - Parcel parcel = Parcel.obtain(); - writeToParcel(parcel, 0); - byte[] bytes = parcel.marshall(); - parcel.recycle(); - return bytes; - } + public CleanUp(OutputStream out) { + this.out = out; + } - static Config deserialize(byte[] bytes) { - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(bytes, 0, bytes.length); - parcel.setDataPosition(0); - return CREATOR.createFromParcel(parcel); - } + public static CleanUp configure(int displayId) throws IOException { + String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; - static Config fromBase64(String base64) { - byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); - return deserialize(bytes); - } + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("CLASSPATH", Server.SERVER_PATH); + Process process = builder.start(); + return new CleanUp(process.getOutputStream()); + } - String toBase64() { - byte[] bytes = serialize(); - return Base64.encodeToString(bytes, Base64.NO_WRAP); + private boolean sendMessage(int type, int param) { + assert (type & ~MSG_TYPE_MASK) == 0; + int msg = type | param << MSG_PARAM_SHIFT; + try { + out.write(msg); + out.flush(); + return true; + } catch (IOException e) { + Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e); + return false; } } - private CleanUp() { - // not instantiable + public boolean setRestoreStayOn(int restoreValue) { + // Restore the value (between 0 and 7), -1 to not restore + // + assert restoreValue >= -1 && restoreValue <= 7; + return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111); } - 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(); - } + public boolean setDisableShowTouches(boolean disableOnExit) { + return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); } - private static void startProcess(Config config) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; + public boolean setRestoreNormalPowerMode(boolean restoreOnExit) { + return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0); + } - ProcessBuilder builder = new ProcessBuilder(cmd); - builder.environment().put("CLASSPATH", SERVER_PATH); - builder.start(); + public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { + return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0); } - private static void unlinkSelf() { + public static void unlinkSelf() { try { - new File(SERVER_PATH).delete(); + new File(Server.SERVER_PATH).delete(); } catch (Exception e) { Ln.e("Could not unlink server", e); } @@ -153,39 +77,71 @@ public final class CleanUp { public static void main(String... args) { unlinkSelf(); + int displayId = Integer.parseInt(args[0]); + + int restoreStayOn = -1; + boolean disableShowTouches = false; + boolean restoreNormalPowerMode = false; + boolean powerOffScreen = false; + try { // Wait for the server to die - System.in.read(); + int msg; + while ((msg = System.in.read()) != -1) { + int type = msg & MSG_TYPE_MASK; + int param = msg >> MSG_PARAM_SHIFT; + switch (type) { + case MSG_TYPE_RESTORE_STAY_ON: + restoreStayOn = param > 7 ? -1 : param; + break; + case MSG_TYPE_DISABLE_SHOW_TOUCHES: + disableShowTouches = param != 0; + break; + case MSG_TYPE_RESTORE_NORMAL_POWER_MODE: + restoreNormalPowerMode = param != 0; + break; + case MSG_TYPE_POWER_OFF_SCREEN: + powerOffScreen = param != 0; + break; + default: + Ln.w("Unexpected msg type: " + type); + break; + } + } } catch (IOException e) { // Expected when the server is dead } Ln.i("Cleaning up"); - Config config = Config.fromBase64(args[0]); + if (disableShowTouches) { + Ln.i("Disabling \"show touches\""); + try { + Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0"); + } catch (SettingsException e) { + Ln.e("Could not restore \"show_touches\"", e); + } + } - 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 (restoreStayOn != -1) { + Ln.i("Restoring \"stay awake\""); + try { + Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn)); + } catch (SettingsException e) { + Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); } } if (Device.isScreenOn()) { - if (config.powerOffScreen) { + if (powerOffScreen) { Ln.i("Power off screen"); - Device.powerOffScreen(config.displayId); - } else if (config.restoreNormalPowerMode) { + Device.powerOffScreen(displayId); + } else if (restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } + + System.exit(0); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Codec.java b/server/src/main/java/com/genymobile/scrcpy/Codec.java new file mode 100644 index 00000000..7e905af3 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Codec.java @@ -0,0 +1,17 @@ +package com.genymobile.scrcpy; + +public interface Codec { + + enum Type { + VIDEO, + AUDIO, + } + + Type getType(); + + int getId(); + + String getName(); + + String getMimeType(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java index 1897bda3..22c45a90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CodecOption.java +++ b/server/src/main/java/com/genymobile/scrcpy/CodecOption.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.List; public class CodecOption { - private String key; - private Object value; + private final String key; + private final Object value; public CodecOption(String key, Object value) { this.key = key; @@ -21,7 +21,7 @@ public class CodecOption { } public static List parse(String codecOptions) { - if ("-".equals(codecOptions)) { + if (codecOptions.isEmpty()) { return null; } diff --git a/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java new file mode 100644 index 00000000..afb6f904 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CodecUtils.java @@ -0,0 +1,78 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.media.MediaFormat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class CodecUtils { + + public static final class DeviceEncoder { + private final Codec codec; + private final MediaCodecInfo info; + + DeviceEncoder(Codec codec, MediaCodecInfo info) { + this.codec = codec; + this.info = info; + } + + public Codec getCodec() { + return codec; + } + + public MediaCodecInfo getInfo() { + return info; + } + } + + private CodecUtils() { + // not instantiable + } + + public static void setCodecOption(MediaFormat format, String key, Object value) { + 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); + } + } + + private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { + List result = new ArrayList<>(); + for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { + if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { + result.add(codecInfo); + } + } + return result.toArray(new MediaCodecInfo[result.size()]); + } + + public static List listVideoEncoders() { + List encoders = new ArrayList<>(); + MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (VideoCodec codec : VideoCodec.values()) { + for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { + encoders.add(new DeviceEncoder(codec, info)); + } + } + return encoders; + } + + public static List listAudioEncoders() { + List encoders = new ArrayList<>(); + MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (AudioCodec codec : AudioCodec.values()) { + for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { + encoders.add(new DeviceEncoder(codec, info)); + } + } + return encoders; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Command.java b/server/src/main/java/com/genymobile/scrcpy/Command.java new file mode 100644 index 00000000..362504ff --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Command.java @@ -0,0 +1,43 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Scanner; + +public final class Command { + private Command() { + // not instantiable + } + + public static void exec(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + } + + public static String execReadLine(String... cmd) throws IOException, InterruptedException { + String result = null; + Process process = Runtime.getRuntime().exec(cmd); + Scanner scanner = new Scanner(process.getInputStream()); + if (scanner.hasNextLine()) { + result = scanner.nextLine(); + } + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return result; + } + + public static String execReadOutput(String... cmd) throws IOException, InterruptedException { + Process process = Runtime.getRuntime().exec(cmd); + String output = IO.toString(process.getInputStream()); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); + } + return output; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java b/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java new file mode 100644 index 00000000..76c8f52e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ConfigurationException.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy; + +public class ConfigurationException extends Exception { + public ConfigurationException(String message) { + super(message); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java b/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java new file mode 100644 index 00000000..4677cfda --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ControlChannel.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import android.net.LocalSocket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public final class ControlChannel { + private final InputStream inputStream; + private final OutputStream outputStream; + + private final ControlMessageReader reader = new ControlMessageReader(); + private final DeviceMessageWriter writer = new DeviceMessageWriter(); + + public ControlChannel(LocalSocket controlSocket) throws IOException { + this.inputStream = controlSocket.getInputStream(); + this.outputStream = controlSocket.getOutputStream(); + } + + public ControlMessage recv() throws IOException { + ControlMessage msg = reader.next(); + while (msg == null) { + reader.readFrom(inputStream); + msg = reader.next(); + } + return msg; + } + + public void send(DeviceMessage msg) throws IOException { + writer.writeTo(msg, outputStream); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java index 95db27d8..dc2de5ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java @@ -28,18 +28,29 @@ public final class ControlMessage { public static final int PUSH_STATE_APPEND = 2; public static final int PUSH_STATE_FINISH = 3; public static final int PUSH_STATE_CANCEL = 4; + public static final int TYPE_UHID_CREATE = 12; + public static final int TYPE_UHID_INPUT = 13; + public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 14; + + public static final long SEQUENCE_INVALID = 0; + + public static final int COPY_KEY_NONE = 0; + public static final int COPY_KEY_COPY = 1; + public static final int COPY_KEY_CUT = 2; private int type; private String text; private int metaState; // KeyEvent.META_* private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int keycode; // KeyEvent.KEYCODE_* + private int actionButton; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_* private long pointerId; private float pressure; private Position position; - private int hScroll; - private int vScroll; + private float hScroll; + private float vScroll; + private int copyKey; private boolean paste; private int repeat; private byte[] bytes; @@ -50,6 +61,9 @@ public final class ControlMessage { private int fileSize; private String fileName; private VideoSettings videoSettings; + private long sequence; + private int id; + private byte[] data; private ControlMessage() { } @@ -71,23 +85,26 @@ public final class ControlMessage { return msg; } - public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) { + public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton, + int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; msg.pointerId = pointerId; msg.pressure = pressure; msg.position = position; + msg.actionButton = actionButton; msg.buttons = buttons; return msg; } - public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { + public static ControlMessage createInjectScrollEvent(Position position, float hScroll, float vScroll, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; msg.position = position; msg.hScroll = hScroll; msg.vScroll = vScroll; + msg.buttons = buttons; return msg; } @@ -98,9 +115,17 @@ public final class ControlMessage { return msg; } - public static ControlMessage createSetClipboard(String text, boolean paste) { + public static ControlMessage createGetClipboard(int copyKey) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_GET_CLIPBOARD; + msg.copyKey = copyKey; + return msg; + } + + public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_CLIPBOARD; + msg.sequence = sequence; msg.text = text; msg.paste = paste; return msg; @@ -166,6 +191,22 @@ public final class ControlMessage { return msg; } + public static ControlMessage createUhidCreate(int id, byte[] reportDesc) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_CREATE; + msg.id = id; + msg.data = reportDesc; + return msg; + } + + public static ControlMessage createUhidInput(int id, byte[] data) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_UHID_INPUT; + msg.id = id; + msg.data = data; + return msg; + } + public int getType() { return type; } @@ -186,6 +227,10 @@ public final class ControlMessage { return keycode; } + public int getActionButton() { + return actionButton; + } + public int getButtons() { return buttons; } @@ -202,14 +247,18 @@ public final class ControlMessage { return position; } - public int getHScroll() { + public float getHScroll() { return hScroll; } - public int getVScroll() { + public float getVScroll() { return vScroll; } + public int getCopyKey() { + return copyKey; + } + public boolean getPaste() { return paste; } @@ -248,5 +297,15 @@ public final class ControlMessage { public VideoSettings getVideoSettings() { return videoSettings; + public long getSequence() { + return sequence; + } + + public int getId() { + return id; + } + + public byte[] getData() { + return data; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java index 3015679e..d1525463 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java @@ -9,15 +9,18 @@ import java.nio.charset.StandardCharsets; public class ControlMessageReader { static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; - static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; + static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31; 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; + static final int GET_CLIPBOARD_LENGTH = 1; + static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; + static final int UHID_CREATE_FIXED_PAYLOAD_LENGTH = 4; + static final int UHID_INPUT_FIXED_PAYLOAD_LENGTH = 4; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k - public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 6; // type: 1 byte; paste flag: 1 byte; length: 4 bytes + public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; 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]; @@ -74,6 +77,9 @@ public class ControlMessageReader { case ControlMessage.TYPE_BACK_OR_SCREEN_ON: msg = parseBackOrScreenOnEvent(buffer); break; + case ControlMessage.TYPE_GET_CLIPBOARD: + msg = parseGetClipboard(buffer); + break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(buffer); break; @@ -89,10 +95,16 @@ public class ControlMessageReader { case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: - case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_ROTATE_DEVICE: + case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: msg = ControlMessage.createEmpty(type); break; + case ControlMessage.TYPE_UHID_CREATE: + msg = parseUhidCreate(buffer); + break; + case ControlMessage.TYPE_UHID_INPUT: + msg = parseUhidInput(buffer); + break; default: Ln.w("Unknown event type: " + type); msg = null; @@ -128,25 +140,44 @@ public class ControlMessageReader { if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { return null; } - int action = toUnsigned(buffer.get()); + int action = Binary.toUnsigned(buffer.get()); int keycode = buffer.getInt(); int repeat = buffer.getInt(); int metaState = buffer.getInt(); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } - private String parseString(ByteBuffer buffer) { - if (buffer.remaining() < 4) { - return null; + private int parseBufferLength(int sizeBytes) { + assert sizeBytes > 0 && sizeBytes <= 4; + if (buffer.remaining() < sizeBytes) { + return -1; } - int len = buffer.getInt(); - if (buffer.remaining() < len) { + int value = 0; + for (int i = 0; i < sizeBytes; ++i) { + value = (value << 8) | (buffer.get() & 0xFF); + } + return value; + } + + private String parseString(ByteBuffer buffer) { + int len = parseBufferLength(4); + if (len == -1 || buffer.remaining() < len) { return null; } buffer.get(rawBuffer, 0, len); return new String(rawBuffer, 0, len, StandardCharsets.UTF_8); } + private byte[] parseByteArray(int sizeBytes) { + int len = parseBufferLength(sizeBytes); + if (len == -1 || buffer.remaining() < len) { + return null; + } + byte[] data = new byte[len]; + buffer.get(data); + return data; + } + private ControlMessage parseInjectText(ByteBuffer buffer) { String text = parseString(buffer); if (text == null) { @@ -159,15 +190,13 @@ public class ControlMessageReader { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; } - int action = toUnsigned(buffer.get()); + int action = Binary.toUnsigned(buffer.get()); long pointerId = buffer.getLong(); Position position = readPosition(buffer); - // 16 bits fixed-point - int pressureInt = toUnsigned(buffer.getShort()); - // convert it to a float between 0 and 1 (0x1p16f is 2^16 as float) - float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f); + float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); + int actionButton = buffer.getInt(); int buttons = buffer.getInt(); - return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); + return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons); } private ControlMessage parseInjectScrollEvent(ByteBuffer buffer) { @@ -175,29 +204,39 @@ public class ControlMessageReader { return null; } Position position = readPosition(buffer); - int hScroll = buffer.getInt(); - int vScroll = buffer.getInt(); - return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); + float hScroll = Binary.i16FixedPointToFloat(buffer.getShort()); + float vScroll = Binary.i16FixedPointToFloat(buffer.getShort()); + int buttons = buffer.getInt(); + return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } private ControlMessage parseBackOrScreenOnEvent(ByteBuffer buffer) { if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { return null; } - int action = toUnsigned(buffer.get()); + int action = Binary.toUnsigned(buffer.get()); return ControlMessage.createBackOrScreenOn(action); } + private ControlMessage parseGetClipboard(ByteBuffer buffer) { + if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { + return null; + } + int copyKey = Binary.toUnsigned(buffer.get()); + return ControlMessage.createGetClipboard(copyKey); + } + private ControlMessage parseSetClipboard(ByteBuffer buffer) { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; } + long sequence = buffer.getLong(); boolean paste = buffer.get() != 0; String text = parseString(buffer); if (text == null) { return null; } - return ControlMessage.createSetClipboard(text, paste); + return ControlMessage.createSetClipboard(sequence, text, paste); } private ControlMessage parseSetScreenPowerMode(ByteBuffer buffer) { @@ -208,19 +247,35 @@ public class ControlMessageReader { return ControlMessage.createSetScreenPowerMode(mode); } - private static Position readPosition(ByteBuffer buffer) { - int x = buffer.getInt(); - int y = buffer.getInt(); - int screenWidth = toUnsigned(buffer.getShort()); - int screenHeight = toUnsigned(buffer.getShort()); - return new Position(x, y, screenWidth, screenHeight); + private ControlMessage parseUhidCreate(ByteBuffer buffer) { + if (buffer.remaining() < UHID_CREATE_FIXED_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + byte[] data = parseByteArray(2); + if (data == null) { + return null; + } + return ControlMessage.createUhidCreate(id, data); } - private static int toUnsigned(short value) { - return value & 0xffff; + private ControlMessage parseUhidInput(ByteBuffer buffer) { + if (buffer.remaining() < UHID_INPUT_FIXED_PAYLOAD_LENGTH) { + return null; + } + int id = buffer.getShort(); + byte[] data = parseByteArray(2); + if (data == null) { + return null; + } + return ControlMessage.createUhidInput(id, data); } - private static int toUnsigned(byte value) { - return value & 0xff; + private static Position readPosition(ByteBuffer buffer) { + int x = buffer.getInt(); + int y = buffer.getInt(); + int screenWidth = Binary.toUnsigned(buffer.getShort()); + int screenHeight = Binary.toUnsigned(buffer.getShort()); + return new Position(x, y, screenWidth, screenHeight); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 35b2c4bd..df5a0a13 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -1,5 +1,9 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.InputManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; @@ -12,15 +16,27 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -public class Controller { +public class Controller implements AsyncProcessor { private static final int DEFAULT_DEVICE_ID = 0; + // control_msg.h values of the pointerId field in inject_touch_event message + private static final int POINTER_ID_MOUSE = -1; + private static final int POINTER_ID_VIRTUAL_MOUSE = -3; + private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private Thread thread; + + private UhidManager uhidManager; + private final Device device; private final Connection connection; + private final ControlChannel controlChannel; + private final CleanUp cleanUp; private final DeviceMessageSender sender; + private final boolean clipboardAutosync; + private final boolean powerOn; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); @@ -31,11 +47,21 @@ public class Controller { private boolean keepPowerModeOff; - public Controller(Device device, Connection connection) { + public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.device = device; - this.connection = connection; + this.controlChannel = controlChannel; + this.cleanUp = cleanUp; + this.clipboardAutosync = clipboardAutosync; + this.powerOn = powerOn; initPointers(); - sender = new DeviceMessageSender(connection); + sender = new DeviceMessageSender(controlChannel); + } + + private UhidManager getUhidManager() { + if (uhidManager == null) { + uhidManager = new UhidManager(sender); + } + return uhidManager; } private void initPointers() { @@ -52,6 +78,62 @@ public class Controller { } } + private void control() throws IOException { + // on start, power on the device + if (powerOn && !Device.isScreenOn()) { + device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + + // dirty hack + // After POWER is injected, the device is powered on asynchronously. + // To turn the device screen off while mirroring, the client will send a message that + // would be handled before the device is actually powered on, so its effect would + // be "canceled" once the device is turned back on. + // Adding this delay prevents to handle the message before the device is actually + // powered on. + SystemClock.sleep(500); + } + + boolean alive = true; + while (!Thread.currentThread().isInterrupted() && alive) { + alive = handleEvent(); + } + } + + @Override + public void start(TerminationListener listener) { + thread = new Thread(() -> { + try { + control(); + } catch (IOException e) { + Ln.e("Controller error", e); + } finally { + Ln.d("Controller stopped"); + if (uhidManager != null) { + uhidManager.closeAll(); + } + listener.onTerminated(true); + } + }, "control-recv"); + thread.start(); + sender.start(); + } + + @Override + public void stop() { + if (thread != null) { + thread.interrupt(); + } + sender.stop(); + } + + @Override + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + sender.join(); + } + public DeviceMessageSender getSender() { return sender; } @@ -70,12 +152,12 @@ public class Controller { break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: if (device.supportsInputEvents()) { - injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); + injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: if (device.supportsInputEvents()) { - injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); + injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: @@ -104,7 +186,7 @@ public class Controller { } break; case ControlMessage.TYPE_SET_CLIPBOARD: - setClipboard(msg.getText(), msg.getPaste()); + setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { @@ -113,22 +195,37 @@ public class Controller { if (setPowerModeOk) { keepPowerModeOff = mode == Device.POWER_MODE_OFF; Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + if (cleanUp != null) { + boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL; + cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit); + } } } break; case ControlMessage.TYPE_ROTATE_DEVICE: - Device.rotateDevice(); + device.rotateDevice(); + break; + case ControlMessage.TYPE_UHID_CREATE: + getUhidManager().open(msg.getId(), msg.getData()); + break; + case ControlMessage.TYPE_UHID_INPUT: + getUhidManager().writeInput(msg.getId(), msg.getData()); + break; + case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + openHardKeyboardSettings(); break; default: // do nothing } + + return true; } 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); + return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { @@ -139,7 +236,7 @@ public class Controller { return false; } for (KeyEvent event : events) { - if (!device.injectEvent(event)) { + if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -158,7 +255,7 @@ public class Controller { return successCount; } - private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); @@ -175,10 +272,23 @@ public class Controller { Pointer pointer = pointersState.get(pointerIndex); pointer.setPoint(point); pointer.setPressure(pressure); - pointer.setUp(action == MotionEvent.ACTION_UP); - int pointerCount = pointersState.update(pointerProperties, pointerCoords); + int source; + if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { + // real mouse event (forced by the client when --forward-on-click) + pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; + source = InputDevice.SOURCE_MOUSE; + pointer.setUp(buttons == 0); + } else { + // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device + pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; + source = InputDevice.SOURCE_TOUCHSCREEN; + // Buttons must not be set for touch events + buttons = 0; + pointer.setUp(action == MotionEvent.ACTION_UP); + } + int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerCount == 1) { if (action == MotionEvent.ACTION_DOWN) { lastTouchDown = now; @@ -192,21 +302,68 @@ 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; + /* If the input device is a mouse (on API >= 23): + * - the first button pressed must first generate ACTION_DOWN; + * - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS; + * - all button released (including the last one) must generate ACTION_BUTTON_RELEASE; + * - the last button released must in addition generate ACTION_UP. + * + * Otherwise, Chrome does not work properly: + */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) { + if (action == MotionEvent.ACTION_DOWN) { + if (actionButton == buttons) { + // First button pressed: ACTION_DOWN + MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + } + + // Any button pressed: ACTION_BUTTON_PRESS + MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!InputManager.setActionButton(pressEvent, actionButton)) { + return false; + } + if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + + return true; + } + + if (action == MotionEvent.ACTION_UP) { + // Any button released: ACTION_BUTTON_RELEASE + MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!InputManager.setActionButton(releaseEvent, actionButton)) { + return false; + } + if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + + if (buttons == 0) { + // Last button released: ACTION_UP + MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, + pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); + if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { + return false; + } + } + + return true; + } } - MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, - 0); - return device.injectEvent(event); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, + DEFAULT_DEVICE_ID, 0, source, 0); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } - private boolean injectScroll(Position position, int hScroll, int vScroll) { + private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); if (point == null) { @@ -223,28 +380,24 @@ public class Controller { coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0, - InputDevice.SOURCE_MOUSE, 0); - return device.injectEvent(event); + MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, + DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); + return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } /** * 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); - } + EXECUTOR.schedule(() -> { + Ln.i("Forcing screen off"); + Device.setScreenPowerMode(Device.POWER_MODE_OFF); }, 200, TimeUnit.MILLISECONDS); } private boolean pressBackOrTurnScreenOn(int action) { if (Device.isScreenOn()) { - return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0); + return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } // Screen is off @@ -257,10 +410,30 @@ public class Controller { if (keepPowerModeOff) { schedulePowerModeOff(); } - return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); + return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } - private boolean setClipboard(String text, boolean paste) { + private void getClipboard(int copyKey) { + // On Android >= 7, press the COPY or CUT key if requested + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; + // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one + device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + } + + // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in + // particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than + // copying an old clipboard content. + if (!clipboardAutosync) { + String clipboardText = Device.getClipboardText(); + if (clipboardText != null) { + DeviceMessage msg = DeviceMessage.createClipboard(clipboardText); + sender.send(msg); + } + } + } + + private boolean setClipboard(String text, boolean paste, long sequence) { boolean ok = device.setClipboardText(text); if (ok) { Ln.i("Device clipboard set"); @@ -268,7 +441,13 @@ public class Controller { // 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); + device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); + } + + if (sequence != ControlMessage.SEQUENCE_INVALID) { + // Acknowledgement requested + DeviceMessage msg = DeviceMessage.createAckClipboard(sequence); + sender.send(msg); } return ok; @@ -277,4 +456,9 @@ public class Controller { public void turnScreenOn() { device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER); } + + private void openHardKeyboardSettings() { + Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); + ServiceManager.getActivityManager().startActivity(intent); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java index e5a6bb85..11aba54e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java +++ b/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java @@ -7,21 +7,24 @@ import android.os.SystemClock; import java.io.FileDescriptor; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; + import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public final class DesktopConnection extends Connection { + private static final String SOCKET_NAME_PREFIX = "scrcpy"; + private static final String SOCKET_NAME = "scrcpy"; private final LocalSocket videoSocket; private final FileDescriptor videoFd; + private final LocalSocket audioSocket; + private final FileDescriptor audioFd; + private final LocalSocket controlSocket; - private final InputStream controlInputStream; - private final OutputStream controlOutputStream; + private final ControlChannel controlChannel; private final DeviceMessageWriter writer = new DeviceMessageWriter(); @@ -73,28 +76,62 @@ public final class DesktopConnection extends Connection { screenEncoder.run(); } + private static String getSocketName(int scid) { + if (scid == -1) { + // If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone + return SOCKET_NAME_PREFIX; + } + + return SOCKET_NAME_PREFIX + String.format("_%08x", scid); + } + + private LocalSocket getFirstSocket() { + if (videoSocket != null) { + return videoSocket; + } + if (audioSocket != null) { + return audioSocket; + } + return controlSocket; + } + + public void shutdown() throws IOException { + if (videoSocket != null) { + videoSocket.shutdownInput(); + videoSocket.shutdownOutput(); + } + if (audioSocket != null) { + audioSocket.shutdownInput(); + audioSocket.shutdownOutput(); + } + if (controlSocket != null) { + controlSocket.shutdownInput(); + controlSocket.shutdownOutput(); + } + } + public void close() throws IOException { - videoSocket.shutdownInput(); - videoSocket.shutdownOutput(); - videoSocket.close(); - controlSocket.shutdownInput(); - controlSocket.shutdownOutput(); - controlSocket.close(); + if (videoSocket != null) { + videoSocket.close(); + } + if (audioSocket != null) { + audioSocket.close(); + } + if (controlSocket != null) { + controlSocket.close(); + } } - private void send(String deviceName, int width, int height) throws IOException { - byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; + public void sendDeviceMeta(String deviceName) throws IOException { + byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH]; byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1); System.arraycopy(deviceNameBytes, 0, buffer, 0, len); // byte[] are always 0-initialized in java, no need to set '\0' explicitly - buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8); - buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width; - buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8); - buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height; - IO.writeFully(videoFd, buffer, 0, buffer.length); + FileDescriptor fd = getFirstSocket().getFileDescriptor(); + IO.writeFully(fd, buffer, 0, buffer.length); } public void send(ByteBuffer data) { @@ -155,8 +192,12 @@ public final class DesktopConnection extends Connection { } return msg; } + + public FileDescriptor getAudioFd() { + return audioFd; + } - public void sendDeviceMessage(DeviceMessage msg) throws IOException { - writer.writeTo(msg, controlOutputStream); + public ControlChannel getControlChannel() { + return controlChannel; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 49bae007..03074c0f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ClipboardManager; -import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -12,6 +12,7 @@ import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; +import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; import android.view.InputDevice; import android.view.InputEvent; @@ -25,21 +26,33 @@ 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 INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; + public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; + public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; + 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); } + public interface FoldListener { + void onFoldChanged(int displayId, boolean folded); + } + public interface ClipboardListener { void onClipboardTextChanged(String text); } + private final Rect crop; + private int maxSize; + private final int lockVideoOrientation; + + private Size deviceSize; private ScreenInfo screenInfo; private RotationListener rotationListener; + private FoldListener foldListener; private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); @@ -56,6 +69,7 @@ public final class Device { private final boolean supportsInputEvents; private IRotationWatcher rotationWatcher; + private IDisplayFoldListener displayFoldListener; private IOnPrimaryClipChangedListener clipChangedListener; public Device(final Options options, final VideoSettings videoSettings) { @@ -86,9 +100,36 @@ public final class Device { SERVICE_MANAGER.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - if (options.getControl()) { - // If control is enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + displayFoldListener = new IDisplayFoldListener.Stub() { + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (Device.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + + synchronized (Device.this) { + DisplayInfo displayInfo = Device.getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + return; + } + + deviceSize = displayInfo.getSize(); + screenInfo = ScreenInfo.computeScreenInfo(displayInfo, videoSettings); + // notify + if (foldListener != null) { + foldListener.onFoldChanged(displayId, folded); + } + } + } + } + }; + + if (options.getControl() && options.getClipboardAutosync()) { + // If control and autosync are enabled, synchronize Android clipboard to the computer automatically + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); clipChangedListener = new IOnPrimaryClipChangedListener.Stub() { @Override public void dispatchPrimaryClipChanged() { @@ -105,12 +146,12 @@ public final class Device { } } } - }; - if (clipboardManager != null) { - clipboardManager.addPrimaryClipChangedListener(clipChangedListener); - } else { - Ln.w("No clipboard manager, copy-paste between device and computer will not work"); } + }; + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(clipChangedListener); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); } if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { @@ -124,6 +165,15 @@ public final class Device { } } + public int getDisplayId() { + return displayId; + } + + public synchronized void setMaxSize(int newMaxSize) { + maxSize = newMaxSize; + screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); + } + public synchronized ScreenInfo getScreenInfo() { return screenInfo; } @@ -177,7 +227,7 @@ public final class Device { return supportsInputEvents; } - public static boolean injectEvent(InputEvent inputEvent, int displayId) { + public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); } @@ -186,58 +236,63 @@ public final class Device { return false; } - return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); + return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); } - public boolean injectEvent(InputEvent event) { - return injectEvent(event, displayId); + public boolean injectEvent(InputEvent event, int injectMode) { + return injectEvent(event, displayId, injectMode); } - public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) { + public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { 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); + return injectEvent(event, displayId, injectMode); } - public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { - return injectKeyEvent(action, keyCode, repeat, metaState, displayId); + public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); } - 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 static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { + return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) + && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public boolean pressReleaseKeycode(int keyCode) { - return pressReleaseKeycode(keyCode, displayId); + public boolean pressReleaseKeycode(int keyCode, int injectMode) { + return pressReleaseKeycode(keyCode, displayId, injectMode); } public static boolean isScreenOn() { - return SERVICE_MANAGER.getPowerManager().isScreenOn(); + return ServiceManager.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } + public synchronized void setFoldListener(FoldListener foldlistener) { + this.foldListener = foldlistener; + } + public synchronized void setClipboardListener(ClipboardListener clipboardListener) { this.clipboardListener = clipboardListener; } public static void expandNotificationPanel() { - SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); + ServiceManager.getStatusBarManager().expandNotificationsPanel(); } public static void expandSettingsPanel() { - SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); + ServiceManager.getStatusBarManager().expandSettingsPanel(); } public static void collapsePanels() { - SERVICE_MANAGER.getStatusBarManager().collapsePanels(); + ServiceManager.getStatusBarManager().collapsePanels(); } public static String getClipboardText() { - ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return null; } @@ -249,7 +304,7 @@ public final class Device { } public boolean setClipboardText(String text) { - ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return false; } @@ -289,6 +344,28 @@ public final class Device { * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // On Android 14, these internal methods have been moved to DisplayControl + boolean useDisplayControl = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod(); + + // Change the power mode for all physical displays + long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); + if (physicalDisplayIds == null) { + Ln.e("Could not get physical display ids"); + return false; + } + + boolean allOk = true; + for (long physicalDisplayId : physicalDisplayIds) { + IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken( + physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); + allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); + } + return allOk; + } + + // Older Android versions, only 1 display IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); @@ -301,32 +378,37 @@ public final class Device { if (!isScreenOn()) { return true; } - return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId); + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); } /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public static void rotateDevice() { - WindowManager wm = SERVICE_MANAGER.getWindowManager(); + public void rotateDevice() { + WindowManager wm = ServiceManager.getWindowManager(); - boolean accelerometerRotation = !wm.isRotationFrozen(); + boolean accelerometerRotation = !wm.isRotationFrozen(displayId); - int currentRotation = wm.getRotation(); + int currentRotation = getCurrentRotation(displayId); int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0 String newRotationString = newRotation == 0 ? "portrait" : "landscape"; Ln.i("Device rotation requested: " + newRotationString); - wm.freezeRotation(newRotation); + wm.freezeRotation(displayId, newRotation); // restore auto-rotate if necessary if (accelerometerRotation) { - wm.thawRotation(); + wm.thawRotation(displayId); } } - public static ContentProvider createSettingsProvider() { - return SERVICE_MANAGER.getActivityManager().createSettingsProvider(); + private static int getCurrentRotation(int displayId) { + if (displayId == 0) { + return ServiceManager.getWindowManager().getRotation(); + } + + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + return displayInfo.getRotation(); } public static int[] getDisplayIds() { diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java index ecc43b55..59701e91 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java @@ -9,7 +9,14 @@ public abstract class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_PUSH_RESPONSE = 101; + public static final int TYPE_ACK_CLIPBOARD = 1; + public static final int TYPE_UHID_OUTPUT = 2; + private int type; + private String text; + private long sequence; + private int id; + private byte[] data; private DeviceMessage(int type) { this.type = type; @@ -67,17 +74,47 @@ public abstract class DeviceMessage { return new FilePushResponseMessage(id, result); } + public static DeviceMessage createAckClipboard(long sequence) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_ACK_CLIPBOARD; + event.sequence = sequence; + return event; + } + + public static DeviceMessage createUhidOutput(int id, byte[] data) { + DeviceMessage event = new DeviceMessage(); + event.type = TYPE_UHID_OUTPUT; + event.id = id; + event.data = data; + return event; + } + public int getType() { return type; } + public void writeToByteArray(byte[] array) { writeToByteArray(array, 0); } + public byte[] writeToByteArray(int offset) { byte[] temp = new byte[offset + this.getLen()]; writeToByteArray(temp, offset); return temp; } + public abstract void writeToByteArray(byte[] array, int offset); public abstract int getLen(); + + public long getSequence() { + return sequence; + } + + public int getId() { + return id; + } + + public byte[] getData() { + return data; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 1f3d063a..2457acda 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -1,34 +1,60 @@ package com.genymobile.scrcpy; import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; public final class DeviceMessageSender { private final Connection connection; + private final ControlChannel controlChannel; - private String clipboardText; + private Thread thread; + private final BlockingQueue queue = new ArrayBlockingQueue<>(16); public DeviceMessageSender(Connection connection) { this.connection = connection; } + + public DeviceMessageSender(ControlChannel controlChannel) { + this.controlChannel = controlChannel; + } - public synchronized void pushClipboardText(String text) { - clipboardText = text; - notify(); + public void send(DeviceMessage msg) { + if (!queue.offer(msg)) { + Ln.w("Device message dropped: " + msg.getType()); + } + } + + private void loop() throws IOException, InterruptedException { + while (!Thread.currentThread().isInterrupted()) { + DeviceMessage msg = queue.take(); + controlChannel.send(msg); + } } - public void loop() throws IOException, InterruptedException { - while (true) { - String text; - synchronized (this) { - while (clipboardText == null) { - wait(); - } - text = clipboardText; - clipboardText = null; + public void start() { + thread = new Thread(() -> { + try { + loop(); + } catch (IOException | InterruptedException e) { + // this is expected on close + } finally { + Ln.d("Device message sender stopped"); } - DeviceMessage event = DeviceMessage.createClipboard(text); - connection.sendDeviceMessage(event); + }, "control-send"); + thread.start(); + } + + public void stop() { + if (thread != null) { + thread.interrupt(); + } + } + + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java new file mode 100644 index 00000000..2ea7bf4a --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -0,0 +1,53 @@ +package com.genymobile.scrcpy; + +import android.annotation.TargetApi; +import android.content.AttributionSource; +import android.content.Context; +import android.content.ContextWrapper; +import android.os.Build; +import android.os.Process; + +public final class FakeContext extends ContextWrapper { + + public static final String PACKAGE_NAME = "com.android.shell"; + public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 + + private static final FakeContext INSTANCE = new FakeContext(); + + public static FakeContext get() { + return INSTANCE; + } + + private FakeContext() { + super(Workarounds.getSystemContext()); + } + + @Override + public String getPackageName() { + return PACKAGE_NAME; + } + + @Override + public String getOpPackageName() { + return PACKAGE_NAME; + } + + @TargetApi(Build.VERSION_CODES.S) + @Override + public AttributionSource getAttributionSource() { + AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); + builder.setPackageName(PACKAGE_NAME); + return builder.build(); + } + + // @Override to be added on SDK upgrade for Android 14 + @SuppressWarnings("unused") + public int getDeviceId() { + return 0; + } + + @Override + public Context getApplicationContext() { + return this; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java b/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java new file mode 100644 index 00000000..1f5f0a4f --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/HandlerExecutor.java @@ -0,0 +1,23 @@ +package com.genymobile.scrcpy; + +import android.os.Handler; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +// Inspired from hidden android.os.HandlerExecutor + +public class HandlerExecutor implements Executor { + private final Handler handler; + + public HandlerExecutor(Handler handler) { + this.handler = handler; + } + + @Override + public void execute(Runnable command) { + if (!handler.post(command)) { + throw new RejectedExecutionException(handler + " is shutting down"); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/IO.java b/server/src/main/java/com/genymobile/scrcpy/IO.java index 57c017db..4a55c152 100644 --- a/server/src/main/java/com/genymobile/scrcpy/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/IO.java @@ -6,7 +6,9 @@ import android.system.OsConstants; import java.io.FileDescriptor; import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Scanner; public final class IO { private IO() { @@ -37,4 +39,18 @@ public final class IO { public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); } + + public static String toString(InputStream inputStream) { + StringBuilder builder = new StringBuilder(); + Scanner scanner = new Scanner(inputStream); + while (scanner.hasNextLine()) { + builder.append(scanner.nextLine()).append('\n'); + } + return builder.toString(); + } + + public static boolean isBrokenPipe(IOException e) { + Throwable cause = e.getCause(); + return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java b/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java deleted file mode 100644 index 81e3b903..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 1efd2989..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java +++ /dev/null @@ -1,23 +0,0 @@ -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 061cda95..cdd57b9f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Ln.java +++ b/server/src/main/java/com/genymobile/scrcpy/Ln.java @@ -2,6 +2,11 @@ package com.genymobile.scrcpy; import android.util.Log; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; + /** * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal * directly). @@ -11,6 +16,9 @@ public final class Ln { private static final String TAG = "scrcpy"; private static final String PREFIX = "[server] "; + private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out)); + private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err)); + enum Level { VERBOSE, DEBUG, INFO, WARN, ERROR } @@ -21,6 +29,12 @@ public final class Ln { // not instantiable } + public static void disableSystemStreams() { + PrintStream nullStream = new PrintStream(new NullOutputStream()); + System.setOut(nullStream); + System.setErr(nullStream); + } + /** * Initialize the log level. *

@@ -39,37 +53,44 @@ public final class Ln { public static void v(String message) { if (isEnabled(Level.VERBOSE)) { Log.v(TAG, message); - System.out.println(PREFIX + "VERBOSE: " + message); + CONSOLE_OUT.print(PREFIX + "VERBOSE: " + message + '\n'); } } public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); - System.out.println(PREFIX + "DEBUG: " + message); + CONSOLE_OUT.print(PREFIX + "DEBUG: " + message + '\n'); } } public static void i(String message) { if (isEnabled(Level.INFO)) { Log.i(TAG, message); - System.out.println(PREFIX + "INFO: " + message); + CONSOLE_OUT.print(PREFIX + "INFO: " + message + '\n'); } } - public static void w(String message) { + public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { - Log.w(TAG, message); - System.out.println(PREFIX + "WARN: " + message); + Log.w(TAG, message, throwable); + CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n'); + if (throwable != null) { + throwable.printStackTrace(CONSOLE_ERR); + } } } + public static void w(String message) { + w(message, null); + } + public static void e(String message, Throwable throwable) { if (isEnabled(Level.ERROR)) { Log.e(TAG, message, throwable); - System.out.println(PREFIX + "ERROR: " + message); + CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n'); if (throwable != null) { - throwable.printStackTrace(); + throwable.printStackTrace(CONSOLE_ERR); } } } @@ -77,4 +98,21 @@ public final class Ln { public static void e(String message) { e(message, null); } + + static class NullOutputStream extends OutputStream { + @Override + public void write(byte[] b) { + // ignore + } + + @Override + public void write(byte[] b, int off, int len) { + // ignore + } + + @Override + public void write(int b) { + // ignore + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java new file mode 100644 index 00000000..efa0672b --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -0,0 +1,151 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.DisplayManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.MediaCodec; +import android.util.Range; + +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +public final class LogUtils { + + private LogUtils() { + // not instantiable + } + + public static String buildVideoEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of video encoders:"); + List videoEncoders = CodecUtils.listVideoEncoders(); + if (videoEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : videoEncoders) { + builder.append("\n --video-codec=").append(encoder.getCodec().getName()); + builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); + } + } + return builder.toString(); + } + + public static String buildAudioEncoderListMessage() { + StringBuilder builder = new StringBuilder("List of audio encoders:"); + List audioEncoders = CodecUtils.listAudioEncoders(); + if (audioEncoders.isEmpty()) { + builder.append("\n (none)"); + } else { + for (CodecUtils.DeviceEncoder encoder : audioEncoders) { + builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); + builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); + } + } + return builder.toString(); + } + + public static String buildDisplayListMessage() { + StringBuilder builder = new StringBuilder("List of displays:"); + DisplayManager displayManager = ServiceManager.getDisplayManager(); + int[] displayIds = displayManager.getDisplayIds(); + if (displayIds == null || displayIds.length == 0) { + builder.append("\n (none)"); + } else { + for (int id : displayIds) { + builder.append("\n --display-id=").append(id).append(" ("); + DisplayInfo displayInfo = displayManager.getDisplayInfo(id); + if (displayInfo != null) { + Size size = displayInfo.getSize(); + builder.append(size.getWidth()).append("x").append(size.getHeight()); + } else { + builder.append("size unknown"); + } + builder.append(")"); + } + } + return builder.toString(); + } + + private static String getCameraFacingName(int facing) { + switch (facing) { + case CameraCharacteristics.LENS_FACING_FRONT: + return "front"; + case CameraCharacteristics.LENS_FACING_BACK: + return "back"; + case CameraCharacteristics.LENS_FACING_EXTERNAL: + return "external"; + default: + return "unknown"; + } + } + + public static String buildCameraListMessage(boolean includeSizes) { + StringBuilder builder = new StringBuilder("List of cameras:"); + CameraManager cameraManager = ServiceManager.getCameraManager(); + try { + String[] cameraIds = cameraManager.getCameraIdList(); + if (cameraIds == null || cameraIds.length == 0) { + builder.append("\n (none)"); + } else { + for (String id : cameraIds) { + builder.append("\n --camera-id=").append(id); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + builder.append(" (").append(getCameraFacingName(facing)).append(", "); + + Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + builder.append(activeSize.width()).append("x").append(activeSize.height()); + + try { + // Capture frame rates for low-FPS mode are the same for every resolution + Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); + builder.append(", fps=").append(uniqueLowFps); + } catch (Exception e) { + // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" + Ln.w("Could not get available frame rates for camera " + id, e); + } + + builder.append(')'); + + if (includeSizes) { + StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); + for (android.util.Size size : sizes) { + builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); + } + + android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); + if (highSpeedSizes.length > 0) { + builder.append("\n High speed capture (--camera-high-speed):"); + for (android.util.Size size : highSpeedSizes) { + Range[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); + SortedSet uniqueHighFps = getUniqueSet(highFpsRanges); + builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight()); + builder.append(" (fps=").append(uniqueHighFps).append(')'); + } + } + } + } + } + } catch (CameraAccessException e) { + builder.append("\n (access denied)"); + } + return builder.toString(); + } + + private static SortedSet getUniqueSet(Range[] ranges) { + SortedSet set = new TreeSet<>(); + for (Range range : ranges) { + set.add(range.getUpper()); + } + return set; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index f7711f8a..f20191cd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,25 +2,61 @@ package com.genymobile.scrcpy; import android.graphics.Rect; +import java.util.List; +import java.util.Locale; + public class Options { public static final int TYPE_LOCAL_SOCKET = 1; public static final int TYPE_WEB_SOCKET = 2; private Ln.Level logLevel = Ln.Level.ERROR; + + private int scid = -1; // 31-bit non-negative value, or -1 + private boolean video = true; + private boolean audio = true; private int maxSize; - private int bitRate; + private VideoCodec videoCodec = VideoCodec.H264; + private AudioCodec audioCodec = AudioCodec.OPUS; + private VideoSource videoSource = VideoSource.DISPLAY; + private AudioSource audioSource = AudioSource.OUTPUT; + private int videoBitRate = 8000000; + private int audioBitRate = 128000; private int maxFps; - private int lockedVideoOrientation; + private int lockVideoOrientation = -1; private boolean tunnelForward = false; private Rect crop; - private boolean sendFrameMeta; // send PTS so that the client may record properly private boolean control = true; private int displayId; + private String cameraId; + private Size cameraSize; + private CameraFacing cameraFacing; + private CameraAspectRatio cameraAspectRatio; + private int cameraFps; + private boolean cameraHighSpeed; private boolean showTouches = false; private boolean stayAwake = false; - private String codecOptions; - private String encoderName; + private List videoCodecOptions; + private List audioCodecOptions; + + private String videoEncoder; + private String audioEncoder; private boolean powerOffScreenOnClose; + private boolean clipboardAutosync = true; + private boolean downsizeOnError = true; + private boolean cleanup = true; + private boolean powerOn = true; + + private boolean listEncoders; + private boolean listDisplays; + private boolean listCameras; + private boolean listCameraSizes; + + // Options not used by the scrcpy client, but useful to use scrcpy-server directly + private boolean sendDeviceMeta = true; // send device name and size + private boolean sendFrameMeta = true; // send PTS so that the client may record properly + private boolean sendDummyByte = true; // write a byte on start to detect connection issues + private boolean sendCodecMeta = true; // write the codec metadata before the stream + private int serverType = TYPE_LOCAL_SOCKET; private int portNumber = 8886; private boolean listenOnAllInterfaces = true; @@ -29,8 +65,16 @@ public class Options { return logLevel; } - public void setLogLevel(Ln.Level logLevel) { - this.logLevel = logLevel; + public int getScid() { + return scid; + } + + public boolean getVideo() { + return video; + } + + public boolean getAudio() { + return audio; } public int getMaxSize() { @@ -40,105 +84,101 @@ public class Options { public void setMaxSize(int maxSize) { this.maxSize = (maxSize / 8) * 8; } + + public VideoCodec getVideoCodec() { + return videoCodec; + } - public int getBitRate() { - return bitRate; + public AudioCodec getAudioCodec() { + return audioCodec; } - public void setBitRate(int bitRate) { - this.bitRate = bitRate; + public VideoSource getVideoSource() { + return videoSource; } - public int getMaxFps() { - return maxFps; + public AudioSource getAudioSource() { + return audioSource; } - public void setMaxFps(int maxFps) { - this.maxFps = maxFps; + public int getVideoBitRate() { + return videoBitRate; } - public int getLockedVideoOrientation() { - return lockedVideoOrientation; + public int getAudioBitRate() { + return audioBitRate; } - public void setLockedVideoOrientation(int lockedVideoOrientation) { - this.lockedVideoOrientation = lockedVideoOrientation; + public int getMaxFps() { + return maxFps; } - public boolean isTunnelForward() { - return tunnelForward; + public int getLockVideoOrientation() { + return lockVideoOrientation; } - public void setTunnelForward(boolean tunnelForward) { - this.tunnelForward = tunnelForward; + public boolean isTunnelForward() { + return tunnelForward; } public Rect getCrop() { return crop; } - public void setCrop(Rect crop) { - this.crop = crop; + public boolean getControl() { + return control; } - public boolean getSendFrameMeta() { - return sendFrameMeta; + public int getDisplayId() { + return displayId; } - public void setSendFrameMeta(boolean sendFrameMeta) { - this.sendFrameMeta = sendFrameMeta; + public String getCameraId() { + return cameraId; } - public boolean getControl() { - return control; + public Size getCameraSize() { + return cameraSize; } - public void setControl(boolean control) { - this.control = control; + public CameraFacing getCameraFacing() { + return cameraFacing; } - public int getDisplayId() { - return displayId; + public CameraAspectRatio getCameraAspectRatio() { + return cameraAspectRatio; } - public void setDisplayId(int displayId) { - this.displayId = displayId; + public int getCameraFps() { + return cameraFps; } - public boolean getShowTouches() { - return showTouches; + public boolean getCameraHighSpeed() { + return cameraHighSpeed; } - public void setShowTouches(boolean showTouches) { - this.showTouches = showTouches; + public boolean getShowTouches() { + return 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 List getVideoCodecOptions() { + return videoCodecOptions; } - public String getEncoderName() { - return encoderName; + public List getAudioCodecOptions() { + return audioCodecOptions; } - public void setEncoderName(String encoderName) { - this.encoderName = encoderName; + public String getVideoEncoder() { + return videoEncoder; } - public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { - this.powerOffScreenOnClose = powerOffScreenOnClose; + public String getAudioEncoder() { + return audioEncoder; } public boolean getPowerOffScreenOnClose() { @@ -184,4 +224,300 @@ public class Options { + ", listenOnAllInterfaces=" + (this.listenOnAllInterfaces ? "true" : "false") + '}'; } + + public boolean getClipboardAutosync() { + return clipboardAutosync; + } + + public boolean getDownsizeOnError() { + return downsizeOnError; + } + + public boolean getCleanup() { + return cleanup; + } + + public boolean getPowerOn() { + return powerOn; + } + + public boolean getList() { + return listEncoders || listDisplays || listCameras || listCameraSizes; + } + + public boolean getListEncoders() { + return listEncoders; + } + + public boolean getListDisplays() { + return listDisplays; + } + + public boolean getListCameras() { + return listCameras; + } + + public boolean getListCameraSizes() { + return listCameraSizes; + } + + public boolean getSendDeviceMeta() { + return sendDeviceMeta; + } + + public boolean getSendFrameMeta() { + return sendFrameMeta; + } + + public boolean getSendDummyByte() { + return sendDummyByte; + } + + public boolean getSendCodecMeta() { + return sendCodecMeta; + } + + @SuppressWarnings("MethodLength") + public static Options parse(String... args) { + if (args.length < 1) { + throw new IllegalArgumentException("Missing client version"); + } + + String clientVersion = args[0]; + if (!clientVersion.equals(BuildConfig.VERSION_NAME)) { + throw new IllegalArgumentException( + "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); + } + + Options options = new Options(); + + for (int i = 1; i < args.length; ++i) { + String arg = args[i]; + int equalIndex = arg.indexOf('='); + if (equalIndex == -1) { + throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\""); + } + String key = arg.substring(0, equalIndex); + String value = arg.substring(equalIndex + 1); + switch (key) { + case "scid": + int scid = Integer.parseInt(value, 0x10); + if (scid < -1) { + throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid); + } + options.scid = scid; + break; + case "log_level": + options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); + break; + case "video": + options.video = Boolean.parseBoolean(value); + break; + case "audio": + options.audio = Boolean.parseBoolean(value); + break; + case "video_codec": + VideoCodec videoCodec = VideoCodec.findByName(value); + if (videoCodec == null) { + throw new IllegalArgumentException("Video codec " + value + " not supported"); + } + options.videoCodec = videoCodec; + break; + case "audio_codec": + AudioCodec audioCodec = AudioCodec.findByName(value); + if (audioCodec == null) { + throw new IllegalArgumentException("Audio codec " + value + " not supported"); + } + options.audioCodec = audioCodec; + break; + case "video_source": + VideoSource videoSource = VideoSource.findByName(value); + if (videoSource == null) { + throw new IllegalArgumentException("Video source " + value + " not supported"); + } + options.videoSource = videoSource; + break; + case "audio_source": + AudioSource audioSource = AudioSource.findByName(value); + if (audioSource == null) { + throw new IllegalArgumentException("Audio source " + value + " not supported"); + } + options.audioSource = audioSource; + break; + case "max_size": + options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8 + break; + case "video_bit_rate": + options.videoBitRate = Integer.parseInt(value); + break; + case "audio_bit_rate": + options.audioBitRate = Integer.parseInt(value); + break; + case "max_fps": + options.maxFps = Integer.parseInt(value); + break; + case "lock_video_orientation": + options.lockVideoOrientation = Integer.parseInt(value); + break; + case "tunnel_forward": + options.tunnelForward = Boolean.parseBoolean(value); + break; + case "crop": + if (!value.isEmpty()) { + options.crop = parseCrop(value); + } + break; + case "control": + options.control = Boolean.parseBoolean(value); + break; + case "display_id": + options.displayId = Integer.parseInt(value); + break; + case "show_touches": + options.showTouches = Boolean.parseBoolean(value); + break; + case "stay_awake": + options.stayAwake = Boolean.parseBoolean(value); + break; + case "video_codec_options": + options.videoCodecOptions = CodecOption.parse(value); + break; + case "audio_codec_options": + options.audioCodecOptions = CodecOption.parse(value); + break; + case "video_encoder": + if (!value.isEmpty()) { + options.videoEncoder = value; + } + break; + case "audio_encoder": + if (!value.isEmpty()) { + options.audioEncoder = value; + } + case "power_off_on_close": + options.powerOffScreenOnClose = Boolean.parseBoolean(value); + break; + case "clipboard_autosync": + options.clipboardAutosync = Boolean.parseBoolean(value); + break; + case "downsize_on_error": + options.downsizeOnError = Boolean.parseBoolean(value); + break; + case "cleanup": + options.cleanup = Boolean.parseBoolean(value); + break; + case "power_on": + options.powerOn = Boolean.parseBoolean(value); + break; + case "list_encoders": + options.listEncoders = Boolean.parseBoolean(value); + break; + case "list_displays": + options.listDisplays = Boolean.parseBoolean(value); + break; + case "list_cameras": + options.listCameras = Boolean.parseBoolean(value); + break; + case "list_camera_sizes": + options.listCameraSizes = Boolean.parseBoolean(value); + break; + case "camera_id": + if (!value.isEmpty()) { + options.cameraId = value; + } + break; + case "camera_size": + if (!value.isEmpty()) { + options.cameraSize = parseSize(value); + } + break; + case "camera_facing": + if (!value.isEmpty()) { + CameraFacing facing = CameraFacing.findByName(value); + if (facing == null) { + throw new IllegalArgumentException("Camera facing " + value + " not supported"); + } + options.cameraFacing = facing; + } + break; + case "camera_ar": + if (!value.isEmpty()) { + options.cameraAspectRatio = parseCameraAspectRatio(value); + } + break; + case "camera_fps": + options.cameraFps = Integer.parseInt(value); + break; + case "camera_high_speed": + options.cameraHighSpeed = Boolean.parseBoolean(value); + break; + case "send_device_meta": + options.sendDeviceMeta = Boolean.parseBoolean(value); + break; + case "send_frame_meta": + options.sendFrameMeta = Boolean.parseBoolean(value); + break; + case "send_dummy_byte": + options.sendDummyByte = Boolean.parseBoolean(value); + break; + case "send_codec_meta": + options.sendCodecMeta = Boolean.parseBoolean(value); + break; + case "raw_stream": + boolean rawStream = Boolean.parseBoolean(value); + if (rawStream) { + options.sendDeviceMeta = false; + options.sendFrameMeta = false; + options.sendDummyByte = false; + options.sendCodecMeta = false; + } + break; + default: + Ln.w("Unknown server option: " + key); + break; + } + } + + return options; + } + + private static Rect parseCrop(String crop) { + // input format: "width:height:x:y" + String[] tokens = crop.split(":"); + if (tokens.length != 4) { + throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\""); + } + int width = Integer.parseInt(tokens[0]); + int height = Integer.parseInt(tokens[1]); + int x = Integer.parseInt(tokens[2]); + int y = Integer.parseInt(tokens[3]); + return new Rect(x, y, x + width, y + height); + } + + private static Size parseSize(String size) { + // input format: "x" + String[] tokens = size.split("x"); + if (tokens.length != 2) { + throw new IllegalArgumentException("Invalid size format (expected x): \"" + size + "\""); + } + int width = Integer.parseInt(tokens[0]); + int height = Integer.parseInt(tokens[1]); + return new Size(width, height); + } + + private static CameraAspectRatio parseCameraAspectRatio(String ar) { + if ("sensor".equals(ar)) { + return CameraAspectRatio.sensorAspectRatio(); + } + + String[] tokens = ar.split(":"); + if (tokens.length == 2) { + int w = Integer.parseInt(tokens[0]); + int h = Integer.parseInt(tokens[1]); + return CameraAspectRatio.fromFraction(w, h); + } + + float floatAr = Float.parseFloat(tokens[0]); + return CameraAspectRatio.fromFloat(floatAr); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Position.java b/server/src/main/java/com/genymobile/scrcpy/Position.java index e9b6d8a2..2d298645 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Position.java +++ b/server/src/main/java/com/genymobile/scrcpy/Position.java @@ -3,8 +3,8 @@ package com.genymobile.scrcpy; import java.util.Objects; public class Position { - private Point point; - private Size screenSize; + private final Point point; + private final Size screenSize; public Position(Point point, Size screenSize) { this.point = point; diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java new file mode 100644 index 00000000..95214188 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -0,0 +1,113 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ServiceManager; +import com.genymobile.scrcpy.wrappers.SurfaceControl; + +import android.graphics.Rect; +import android.hardware.display.VirtualDisplay; +import android.os.Build; +import android.os.IBinder; +import android.view.Surface; + +public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener { + + private final Device device; + private IBinder display; + private VirtualDisplay virtualDisplay; + + public ScreenCapture(Device device) { + this.device = device; + } + + @Override + public void init() { + device.setRotationListener(this); + device.setFoldListener(this); + } + + @Override + public void start(Surface surface) { + ScreenInfo screenInfo = device.getScreenInfo(); + Rect contentRect = screenInfo.getContentRect(); + + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + int layerStack = device.getLayerStack(); + + if (display != null) { + SurfaceControl.destroyDisplay(display); + display = null; + } + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + try { + display = createDisplay(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + Ln.d("Display: using SurfaceControl API"); + } catch (Exception surfaceControlException) { + Rect videoRect = screenInfo.getVideoSize().toRect(); + try { + virtualDisplay = ServiceManager.getDisplayManager() + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); + Ln.d("Display: using DisplayManager API"); + } catch (Exception displayManagerException) { + Ln.e("Could not create display using SurfaceControl", surfaceControlException); + Ln.e("Could not create display using DisplayManager", displayManagerException); + throw new AssertionError("Could not create display"); + } + } + } + + @Override + public void release() { + device.setRotationListener(null); + device.setFoldListener(null); + if (display != null) { + SurfaceControl.destroyDisplay(display); + } + } + + @Override + public Size getSize() { + return device.getScreenInfo().getVideoSize(); + } + + @Override + public boolean setMaxSize(int maxSize) { + device.setMaxSize(maxSize); + return true; + } + + @Override + public void onFoldChanged(int displayId, boolean folded) { + requestReset(); + } + + @Override + public void onRotationChanged(int rotation) { + requestReset(); + } + + private static IBinder createDisplay() throws Exception { + // 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 setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { + SurfaceControl.openTransaction(); + try { + SurfaceControl.setDisplaySurface(display, surface); + 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 36dbbe37..949e81c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -92,7 +92,6 @@ public final class ScreenInfo { 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 diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 51cafb8d..d949f70a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -6,9 +6,46 @@ import android.media.MediaCodecInfo; import android.os.Build; import java.util.Locale; +import java.util.ArrayList; public final class Server { + public static final String SERVER_PATH; + + static { + String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); + // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath + SERVER_PATH = classPaths[0]; + } + + private static class Completion { + private int running; + private boolean fatalError; + + Completion(int running) { + this.running = running; + } + + synchronized void addCompleted(boolean fatalError) { + --running; + if (fatalError) { + this.fatalError = true; + } + if (running == 0 || this.fatalError) { + notify(); + } + } + + synchronized void await() { + try { + while (running > 0 && !fatalError) { + wait(); + } + } catch (InterruptedException e) { + // ignore + } + } + } private Server() { // not instantiable @@ -145,25 +182,65 @@ public final class Server { } } - public static void main(String... args) throws Exception { - Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - Ln.e("Exception on thread " + t, e); - suggestFix(e); - } + public static void main(String... args) { + int status = 0; + try { + internalMain(args); + } catch (Throwable t) { + Ln.e(t.getMessage(), t); + status = 1; + } finally { + // By default, the Java process exits when all non-daemon threads are terminated. + // The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit. + // So force the process to exit explicitly. + System.exit(status); + } + } + + private static void internalMain(String... args) throws Exception { + Thread.setDefaultUncaughtExceptionHandler((t, e) -> { + Ln.e("Exception on thread " + t, e); }); Options options = new Options(); VideoSettings videoSettings = new VideoSettings(); parseArguments(options, videoSettings, args); + + Ln.disableSystemStreams(); Ln.initLogLevel(options.getLogLevel()); - if (options.getServerType() == Options.TYPE_LOCAL_SOCKET) { - new DesktopConnection(options, videoSettings); - } else if (options.getServerType() == Options.TYPE_WEB_SOCKET) { - WSServer wsServer = new WSServer(options); - wsServer.setReuseAddr(true); - wsServer.run(); + + Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); + + if (options.getList()) { + if (options.getCleanup()) { + CleanUp.unlinkSelf(); + } + + if (options.getListEncoders()) { + Ln.i(LogUtils.buildVideoEncoderListMessage()); + Ln.i(LogUtils.buildAudioEncoderListMessage()); + } + if (options.getListDisplays()) { + Ln.i(LogUtils.buildDisplayListMessage()); + } + if (options.getListCameras() || options.getListCameraSizes()) { + Workarounds.apply(false, true); + Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); + } + // Just print the requested data, do not mirror + return; + } + + try { + if (options.getServerType() == Options.TYPE_LOCAL_SOCKET) { + new DesktopConnection(options, videoSettings); + } else if (options.getServerType() == Options.TYPE_WEB_SOCKET) { + WSServer wsServer = new WSServer(options); + wsServer.setReuseAddr(true); + wsServer.run(); + } + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java new file mode 100644 index 00000000..1b5e5f98 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -0,0 +1,82 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.ContentProvider; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.os.Build; + +import java.io.IOException; + +public final class Settings { + + public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; + public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; + public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; + + private Settings() { + /* not instantiable */ + } + + private static void execSettingsPut(String table, String key, String value) throws SettingsException { + try { + Command.exec("settings", "put", table, key, value); + } catch (IOException | InterruptedException e) { + throw new SettingsException("put", table, key, value, e); + } + } + + private static String execSettingsGet(String table, String key) throws SettingsException { + try { + return Command.execReadLine("settings", "get", table, key); + } catch (IOException | InterruptedException e) { + throw new SettingsException("get", table, key, null, e); + } + } + + public static String getValue(String table, String key) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + return provider.getValue(table, key); + } catch (SettingsException e) { + Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); + } + } + + return execSettingsGet(table, key); + } + + public static void putValue(String table, String key, String value) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + provider.putValue(table, key, value); + } catch (SettingsException e) { + Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); + } + } + + execSettingsPut(table, key, value); + } + + public static String getAndPutValue(String table, String key, String value) throws SettingsException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + // on Android >= 12, it always fails: + try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { + String oldValue = provider.getValue(table, key); + if (!value.equals(oldValue)) { + provider.putValue(table, key, value); + } + return oldValue; + } catch (SettingsException e) { + Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e); + } + } + + 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/SettingsException.java b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java new file mode 100644 index 00000000..36ef63ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/SettingsException.java @@ -0,0 +1,11 @@ +package com.genymobile.scrcpy; + +public class SettingsException extends Exception { + private static String createMessage(String method, String table, String key, String value) { + return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : ""); + } + + public SettingsException(String method, String table, String key, String value, Throwable cause) { + super(createMessage(method, table, key, value), cause); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java new file mode 100644 index 00000000..8b6c9dcc --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -0,0 +1,186 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodec; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +public final class Streamer { + + private static final long PACKET_FLAG_CONFIG = 1L << 63; + private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; + + private final FileDescriptor fd; + private final Codec codec; + private final boolean sendCodecMeta; + private final boolean sendFrameMeta; + + private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); + + public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) { + this.fd = fd; + this.codec = codec; + this.sendCodecMeta = sendCodecMeta; + this.sendFrameMeta = sendFrameMeta; + } + + public Codec getCodec() { + return codec; + } + + public void writeAudioHeader() throws IOException { + if (sendCodecMeta) { + ByteBuffer buffer = ByteBuffer.allocate(4); + buffer.putInt(codec.getId()); + buffer.flip(); + IO.writeFully(fd, buffer); + } + } + + public void writeVideoHeader(Size videoSize) throws IOException { + if (sendCodecMeta) { + ByteBuffer buffer = ByteBuffer.allocate(12); + buffer.putInt(codec.getId()); + buffer.putInt(videoSize.getWidth()); + buffer.putInt(videoSize.getHeight()); + buffer.flip(); + IO.writeFully(fd, buffer); + } + } + + public void writeDisableStream(boolean error) throws IOException { + // Writing a specific code as codec-id means that the device disables the stream + // code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only + // code 1: a configuration error occurred, scrcpy must be stopped + byte[] code = new byte[4]; + if (error) { + code[3] = 1; + } + IO.writeFully(fd, code, 0, code.length); + } + + public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { + if (config) { + if (codec == AudioCodec.OPUS) { + fixOpusConfigPacket(buffer); + } else if (codec == AudioCodec.FLAC) { + fixFlacConfigPacket(buffer); + } + } + + if (sendFrameMeta) { + writeFrameMeta(fd, buffer.remaining(), pts, config, keyFrame); + } + + IO.writeFully(fd, buffer); + } + + public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException { + long pts = bufferInfo.presentationTimeUs; + boolean config = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; + boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; + writePacket(codecBuffer, pts, config, keyFrame); + } + + private void writeFrameMeta(FileDescriptor fd, int packetSize, long pts, boolean config, boolean keyFrame) throws IOException { + headerBuffer.clear(); + + long ptsAndFlags; + if (config) { + ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet + } else { + ptsAndFlags = pts; + if (keyFrame) { + ptsAndFlags |= PACKET_FLAG_KEY_FRAME; + } + } + + headerBuffer.putLong(ptsAndFlags); + headerBuffer.putInt(packetSize); + headerBuffer.flip(); + IO.writeFully(fd, headerBuffer); + } + + private static void fixOpusConfigPacket(ByteBuffer buffer) throws IOException { + // Here is an example of the config packet received for an OPUS stream: + // + // 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........| + // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- + // 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....| + // 00000020 00 00 00 |... | + // ------------------------------------------------------------------------------ + // 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....| + // 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS| + // 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............| + // 00000050 00 00 00 |...| + // + // Each "section" is prefixed by a 64-bit ID and a 64-bit length. + // + // + + if (buffer.remaining() < 16) { + throw new IOException("Not enough data in OPUS config packet"); + } + + final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'}; + byte[] idBuffer = new byte[8]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, opusHeaderId)) { + throw new IOException("OPUS header not found"); + } + + // The size is in native byte-order + long sizeLong = buffer.getLong(); + if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { + throw new IOException("Invalid block size in OPUS header: " + sizeLong); + } + + int size = (int) sizeLong; + if (buffer.remaining() < size) { + throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")"); + } + + // Set the buffer to point to the OPUS header slice + buffer.limit(buffer.position() + size); + } + + private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException { + // 00000000 66 4c 61 43 00 00 00 22 |fLaC..." | + // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- + // 00000000 10 00 10 00 00 00 00 00 | ........| + // 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................| + // 00000020 00 00 00 00 00 00 00 00 00 00 |.......... | + // ------------------------------------------------------------------------------ + // 00000020 84 00 00 28 20 00 | ...( .| + // 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF| + // 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210| + // 00000050 32 32 00 00 00 00 |22....| + // + // + + if (buffer.remaining() < 8) { + throw new IOException("Not enough data in FLAC config packet"); + } + + final byte[] flacHeaderId = {'f', 'L', 'a', 'C'}; + byte[] idBuffer = new byte[4]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, flacHeaderId)) { + throw new IOException("FLAC header not found"); + } + + // The size is in big-endian + buffer.order(ByteOrder.BIG_ENDIAN); + + int size = buffer.getInt(); + if (buffer.remaining() < size) { + throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")"); + } + + // Set the buffer to point to the FLAC header slice + buffer.limit(buffer.position() + size); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java new file mode 100644 index 00000000..e300e4d6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceCapture.java @@ -0,0 +1,71 @@ +package com.genymobile.scrcpy; + +import android.view.Surface; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A video source which can be rendered on a Surface for encoding. + */ +public abstract class SurfaceCapture { + + private final AtomicBoolean resetCapture = new AtomicBoolean(); + + /** + * Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on + * device rotation for example). + */ + protected void requestReset() { + resetCapture.set(true); + } + + /** + * Consume the reset request (intended to be called by the encoder). + * + * @return {@code true} if a reset request was pending, {@code false} otherwise. + */ + public boolean consumeReset() { + return resetCapture.getAndSet(false); + } + + /** + * Called once before the capture starts. + */ + public abstract void init() throws IOException; + + /** + * Called after the capture ends (if and only if {@link #init()} has been called). + */ + public abstract void release(); + + /** + * Start the capture to the target surface. + * + * @param surface the surface which will be encoded + */ + public abstract void start(Surface surface) throws IOException; + + /** + * Return the video size + * + * @return the video size + */ + public abstract Size getSize(); + + /** + * Set the maximum capture size (set by the encoder if it does not support the current size). + * + * @param maxSize Maximum size + */ + public abstract boolean setMaxSize(int maxSize); + + /** + * Indicate if the capture has been closed internally. + * + * @return {@code true} is the capture is closed, {@code false} otherwise. + */ + public boolean isClosed() { + return false; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java new file mode 100644 index 00000000..28435c09 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java @@ -0,0 +1,282 @@ +package com.genymobile.scrcpy; + +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Looper; +import android.os.SystemClock; +import android.view.Surface; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +public class SurfaceEncoder implements AsyncProcessor { + + 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"; + + // Keep the values in descending order + private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; + private static final int MAX_CONSECUTIVE_ERRORS = 3; + + private final SurfaceCapture capture; + private final Streamer streamer; + private final String encoderName; + private final List codecOptions; + private final int videoBitRate; + private final int maxFps; + private final boolean downsizeOnError; + + private boolean firstFrameSent; + private int consecutiveErrors; + + private Thread thread; + private final AtomicBoolean stopped = new AtomicBoolean(); + + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List codecOptions, String encoderName, + boolean downsizeOnError) { + this.capture = capture; + this.streamer = streamer; + this.videoBitRate = videoBitRate; + this.maxFps = maxFps; + this.codecOptions = codecOptions; + this.encoderName = encoderName; + this.downsizeOnError = downsizeOnError; + } + + private void streamScreen() throws IOException, ConfigurationException { + Codec codec = streamer.getCodec(); + MediaCodec mediaCodec = createMediaCodec(codec, encoderName); + MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); + + capture.init(); + + try { + streamer.writeVideoHeader(capture.getSize()); + + boolean alive; + + do { + Size size = capture.getSize(); + format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth()); + format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); + + Surface surface = null; + try { + mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + surface = mediaCodec.createInputSurface(); + + capture.start(surface); + + mediaCodec.start(); + + alive = encode(mediaCodec, streamer); + // do not call stop() on exception, it would trigger an IllegalStateException + mediaCodec.stop(); + } catch (IllegalStateException | IllegalArgumentException e) { + Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); + if (!prepareRetry(size)) { + throw e; + } + Ln.i("Retrying..."); + alive = true; + } finally { + mediaCodec.reset(); + if (surface != null) { + surface.release(); + } + } + } while (alive); + } finally { + mediaCodec.release(); + capture.release(); + } + } + + private boolean prepareRetry(Size currentSize) { + if (firstFrameSent) { + ++consecutiveErrors; + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + // Definitively fail + return false; + } + + // Wait a bit to increase the probability that retrying will fix the problem + SystemClock.sleep(50); + return true; + } + + if (!downsizeOnError) { + // Must fail immediately + return false; + } + + // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) + + int newMaxSize = chooseMaxSizeFallback(currentSize); + if (newMaxSize == 0) { + // Must definitively fail + return false; + } + + boolean accepted = capture.setMaxSize(newMaxSize); + if (!accepted) { + return false; + } + + // Retry with a smaller size + Ln.i("Retrying with -m" + newMaxSize + "..."); + return true; + } + + private static int chooseMaxSizeFallback(Size failedSize) { + int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); + for (int value : MAX_SIZE_FALLBACK) { + if (value < currentMaxSize) { + // We found a smaller value to reduce the video size + return value; + } + } + // No fallback, fail definitively + return 0; + } + + private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { + boolean eof = false; + boolean alive = true; + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + + while (!capture.consumeReset() && !eof) { + if (stopped.get()) { + alive = false; + break; + } + int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); + try { + if (capture.consumeReset()) { + // must restart encoding with new size + break; + } + + eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + if (outputBufferId >= 0) { + ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); + + boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; + if (!isConfig) { + // If this is not a config packet, then it contains a frame + firstFrameSent = true; + consecutiveErrors = 0; + } + + streamer.writePacket(codecBuffer, bufferInfo); + } + } finally { + if (outputBufferId >= 0) { + codec.releaseOutputBuffer(outputBufferId, false); + } + } + } + + if (capture.isClosed()) { + // The capture might have been closed internally (for example if the camera is disconnected) + alive = false; + } + + return !eof && alive; + } + + private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { + if (encoderName != null) { + Ln.d("Creating encoder by name: '" + encoderName + "'"); + try { + return MediaCodec.createByCodecName(encoderName); + } catch (IllegalArgumentException e) { + Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); + throw new ConfigurationException("Unknown encoder: " + encoderName); + } catch (IOException e) { + Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage()); + throw e; + } + } + + try { + MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); + Ln.d("Using video encoder: '" + mediaCodec.getName() + "'"); + return mediaCodec; + } catch (IOException | IllegalArgumentException e) { + Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage()); + throw e; + } + } + + private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List codecOptions) { + MediaFormat format = new MediaFormat(); + format.setString(MediaFormat.KEY_MIME, videoMimeType); + 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, 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) { + // The key existed privately before Android 10: + // + // + format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps); + } + + if (codecOptions != null) { + for (CodecOption option : codecOptions) { + String key = option.getKey(); + Object value = option.getValue(); + CodecUtils.setCodecOption(format, key, value); + Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); + } + } + + return format; + } + + @Override + public void start(TerminationListener listener) { + thread = new Thread(() -> { + // Some devices (Meizu) deadlock if the video encoding thread has no Looper + // + Looper.prepare(); + + try { + streamScreen(); + } catch (ConfigurationException e) { + // Do not print stack trace, a user-friendly error-message has already been logged + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Video encoding error", e); + } + } finally { + Ln.d("Screen streaming stopped"); + listener.onTerminated(true); + } + }, "video"); + thread.start(); + } + + @Override + public void stop() { + if (thread != null) { + stopped.set(true); + } + } + + @Override + public void join() throws InterruptedException { + if (thread != null) { + thread.join(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java new file mode 100644 index 00000000..a39288a5 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/UhidManager.java @@ -0,0 +1,218 @@ +package com.genymobile.scrcpy; + +import android.os.Build; +import android.os.HandlerThread; +import android.os.MessageQueue; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.ArrayMap; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +public final class UhidManager { + + // Linux: include/uapi/linux/uhid.h + private static final int UHID_OUTPUT = 6; + private static final int UHID_CREATE2 = 11; + private static final int UHID_INPUT2 = 12; + + // Linux: include/uapi/linux/input.h + private static final short BUS_VIRTUAL = 0x06; + + private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) + + private final ArrayMap fds = new ArrayMap<>(); + private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); + + private final DeviceMessageSender sender; + private final HandlerThread thread = new HandlerThread("UHidManager"); + private final MessageQueue queue; + + public UhidManager(DeviceMessageSender sender) { + this.sender = sender; + thread.start(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue = thread.getLooper().getQueue(); + } else { + queue = null; + } + } + + public void open(int id, byte[] reportDesc) throws IOException { + try { + FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); + try { + FileDescriptor old = fds.put(id, fd); + if (old != null) { + Ln.w("Duplicate UHID id: " + id); + close(old); + } + + byte[] req = buildUhidCreate2Req(reportDesc); + Os.write(fd, req, 0, req.length); + + registerUhidListener(id, fd); + } catch (Exception e) { + close(fd); + throw e; + } + } catch (ErrnoException e) { + throw new IOException(e); + } + } + + private void registerUhidListener(int id, FileDescriptor fd) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { + try { + buffer.clear(); + int r = Os.read(fd2, buffer); + buffer.flip(); + if (r > 0) { + int type = buffer.getInt(); + if (type == UHID_OUTPUT) { + byte[] data = extractHidOutputData(buffer); + if (data != null) { + DeviceMessage msg = DeviceMessage.createUhidOutput(id, data); + sender.send(msg); + } + } + } + } catch (ErrnoException | InterruptedIOException e) { + Ln.e("Failed to read UHID output", e); + return 0; + } + return events; + }); + } + } + + private static byte[] extractHidOutputData(ByteBuffer buffer) { + /* + * #define UHID_DATA_MAX 4096 + * struct uhid_event { + * uint32_t type; + * union { + * // ... + * struct uhid_output_req { + * __u8 data[UHID_DATA_MAX]; + * __u16 size; + * __u8 rtype; + * }; + * }; + * } __attribute__((__packed__)); + */ + + if (buffer.remaining() < 4099) { + Ln.w("Incomplete HID output"); + return null; + } + int size = buffer.getShort(buffer.position() + 4096) & 0xFFFF; + if (size > 4096) { + Ln.w("Incorrect HID output size: " + size); + return null; + } + byte[] data = new byte[size]; + buffer.get(data); + return data; + } + + public void writeInput(int id, byte[] data) throws IOException { + FileDescriptor fd = fds.get(id); + if (fd == null) { + Ln.w("Unknown UHID id: " + id); + return; + } + + try { + byte[] req = buildUhidInput2Req(data); + Os.write(fd, req, 0, req.length); + } catch (ErrnoException e) { + throw new IOException(e); + } + } + + private static byte[] buildUhidCreate2Req(byte[] reportDesc) { + /* + * struct uhid_event { + * uint32_t type; + * union { + * // ... + * struct uhid_create2_req { + * uint8_t name[128]; + * uint8_t phys[64]; + * uint8_t uniq[64]; + * uint16_t rd_size; + * uint16_t bus; + * uint32_t vendor; + * uint32_t product; + * uint32_t version; + * uint32_t country; + * uint8_t rd_data[HID_MAX_DESCRIPTOR_SIZE]; + * }; + * }; + * } __attribute__((__packed__)); + */ + + byte[] empty = new byte[256]; + ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); + buf.putInt(UHID_CREATE2); + buf.put("scrcpy".getBytes(StandardCharsets.US_ASCII)); + buf.put(empty, 0, 256 - "scrcpy".length()); + buf.putShort((short) reportDesc.length); + buf.putShort(BUS_VIRTUAL); + buf.putInt(0); // vendor id + buf.putInt(0); // product id + buf.putInt(0); // version + buf.putInt(0); // country; + buf.put(reportDesc); + return buf.array(); + } + + private static byte[] buildUhidInput2Req(byte[] data) { + /* + * struct uhid_event { + * uint32_t type; + * union { + * // ... + * struct uhid_input2_req { + * uint16_t size; + * uint8_t data[UHID_DATA_MAX]; + * }; + * }; + * } __attribute__((__packed__)); + */ + + ByteBuffer buf = ByteBuffer.allocate(6 + data.length).order(ByteOrder.nativeOrder()); + buf.putInt(UHID_INPUT2); + buf.putShort((short) data.length); + buf.put(data); + return buf.array(); + } + + public void close(int id) { + FileDescriptor fd = fds.get(id); + assert fd != null; + close(fd); + } + + public void closeAll() { + for (FileDescriptor fd : fds.values()) { + close(fd); + } + } + + private static void close(FileDescriptor fd) { + try { + Os.close(fd); + } catch (ErrnoException e) { + Ln.e("Failed to close uhid: " + e.getMessage()); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java new file mode 100644 index 00000000..fa787a99 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/VideoCodec.java @@ -0,0 +1,50 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.media.MediaFormat; + +public enum VideoCodec implements Codec { + H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC), + H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), + @SuppressLint("InlinedApi") // introduced in API 29 + AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1); + + private final int id; // 4-byte ASCII representation of the name + private final String name; + private final String mimeType; + + VideoCodec(int id, String name, String mimeType) { + this.id = id; + this.name = name; + this.mimeType = mimeType; + } + + @Override + public Type getType() { + return Type.VIDEO; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getMimeType() { + return mimeType; + } + + public static VideoCodec findByName(String name) { + for (VideoCodec codec : values()) { + if (codec.name.equals(name)) { + return codec; + } + } + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/VideoSource.java b/server/src/main/java/com/genymobile/scrcpy/VideoSource.java new file mode 100644 index 00000000..b5a74fbe --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/VideoSource.java @@ -0,0 +1,22 @@ +package com.genymobile.scrcpy; + +public enum VideoSource { + DISPLAY("display"), + CAMERA("camera"); + + private final String name; + + VideoSource(String name) { + this.name = name; + } + + static VideoSource findByName(String name) { + for (VideoSource videoSource : VideoSource.values()) { + if (name.equals(videoSource.name)) { + return videoSource; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 36866ffb..017cf3b3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -1,24 +1,114 @@ package com.genymobile.scrcpy; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Application; -import android.app.Instrumentation; +import android.content.AttributionSource; import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.os.Build; import android.os.Looper; +import android.os.Parcel; +import java.lang.ref.WeakReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") public final class Workarounds { private static boolean looperPrepared = false; + + private static final Class ACTIVITY_THREAD_CLASS; + private static final Object ACTIVITY_THREAD; + + static { + prepareMainLooper(); + + try { + // ActivityThread activityThread = new ActivityThread(); + ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + ACTIVITY_THREAD = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + } catch (Exception e) { + throw new AssertionError(e); + } + } + private Workarounds() { // not instantiable } + public static void apply(boolean audio, boolean camera) { + boolean mustFillConfigurationController = false; + boolean mustFillAppInfo = false; + boolean mustFillAppContext = false; + + if (Build.BRAND.equalsIgnoreCase("meizu")) { + // Workarounds must be applied for Meizu phones: + // - + // - + // - + // + // But only apply when strictly necessary, since workarounds can cause other issues: + // - + // - + mustFillAppInfo = true; + } else if (Build.BRAND.equalsIgnoreCase("honor")) { + // More workarounds must be applied for Honor devices: + // - + // + // The system context must not be set for all devices, because it would cause other problems: + // - + // - + mustFillAppInfo = true; + mustFillAppContext = true; + } + + if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + // Before Android 11, audio is not supported. + // Since Android 12, we can properly set a context on the AudioRecord. + // Only on Android 11 we must fill the application context for the AudioRecord to work. + mustFillAppContext = true; + } + + if (camera) { + mustFillAppInfo = true; + mustFillAppContext = true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), + // which requires a non-null ConfigurationController. + // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. + // + mustFillConfigurationController = true; + } + + if (mustFillConfigurationController) { + // Must be call before fillAppContext() because it is necessary to get a valid system context + fillConfigurationController(); + } + if (mustFillAppInfo) { + fillAppInfo(); + } + if (mustFillAppContext) { + fillAppContext(); + } + } + @SuppressWarnings("deprecation") - public static void prepareMainLooper() { + private 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()" // @@ -34,20 +124,8 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") - public static void fillAppInfo() { + private static void fillAppInfo() { try { - // ActivityThread activityThread = new ActivityThread(); - Class activityThreadClass = Class.forName("android.app.ActivityThread"); - Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); - activityThreadConstructor.setAccessible(true); - Object activityThread = activityThreadConstructor.newInstance(); - - // ActivityThread.sCurrentActivityThread = activityThread; - Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); - sCurrentActivityThreadField.setAccessible(true); - sCurrentActivityThreadField.set(null, activityThread); - // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); @@ -55,7 +133,7 @@ public final class Workarounds { Object appBindData = appBindDataConstructor.newInstance(); ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.packageName = "com.genymobile.scrcpy"; + applicationInfo.packageName = FakeContext.PACKAGE_NAME; // appBindData.appInfo = applicationInfo; Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); @@ -63,23 +141,204 @@ public final class Workarounds { appInfoField.set(appBindData, applicationInfo); // activityThread.mBoundApplication = appBindData; - Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); + Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); - mBoundApplicationField.set(activityThread, appBindData); - - // Context ctx = activityThread.getSystemContext(); - Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); - Context ctx = (Context) getSystemContextMethod.invoke(activityThread); + mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not fill app info: " + throwable.getMessage()); + } + } - Application app = Instrumentation.newApplication(Application.class, ctx); + private static void fillAppContext() { + try { + Application app = new Application(); + Field baseField = ContextWrapper.class.getDeclaredField("mBase"); + baseField.setAccessible(true); + baseField.set(app, FakeContext.get()); // activityThread.mInitialApplication = app; - Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); + Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); mInitialApplicationField.setAccessible(true); - mInitialApplicationField.set(activityThread, app); + mInitialApplicationField.set(ACTIVITY_THREAD, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.d("Could not fill app info: " + throwable.getMessage()); + Ln.d("Could not fill app context: " + throwable.getMessage()); + } + } + + private static void fillConfigurationController() { + try { + Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); + Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); + configurationControllerConstructor.setAccessible(true); + Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + + Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); + configurationControllerField.setAccessible(true); + configurationControllerField.set(ACTIVITY_THREAD, configurationController); + } catch (Throwable throwable) { + Ln.d("Could not fill configuration: " + throwable.getMessage()); + } + } + + static Context getSystemContext() { + try { + Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); + return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD); + } catch (Throwable throwable) { + // this is a workaround, so failing is not an error + Ln.d("Could not get system context: " + throwable.getMessage()); + return null; + } + } + + @TargetApi(Build.VERSION_CODES.R) + @SuppressLint("WrongConstant,MissingPermission") + public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { + // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. + // + // This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses + // reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do). + // As a result, the modified code was not executed. + try { + // AudioRecord audioRecord = new AudioRecord(0L); + Constructor audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class); + audioRecordConstructor.setAccessible(true); + AudioRecord audioRecord = audioRecordConstructor.newInstance(0L); + + // audioRecord.mRecordingState = RECORDSTATE_STOPPED; + Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState"); + mRecordingStateField.setAccessible(true); + mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED); + + Looper looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + + // audioRecord.mInitializationLooper = looper; + Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper"); + mInitializationLooperField.setAccessible(true); + mInitializationLooperField.set(audioRecord, looper); + + // Create `AudioAttributes` with fixed capture preset + int capturePreset = source; + AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder(); + Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class); + setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset); + AudioAttributes attributes = audioAttributesBuilder.build(); + + // audioRecord.mAudioAttributes = attributes; + Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes"); + mAudioAttributesField.setAccessible(true); + mAudioAttributesField.set(audioRecord, attributes); + + // audioRecord.audioParamCheck(capturePreset, sampleRate, encoding); + Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class); + audioParamCheckMethod.setAccessible(true); + audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding); + + // audioRecord.mChannelCount = channels + Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount"); + mChannelCountField.setAccessible(true); + mChannelCountField.set(audioRecord, channels); + + // audioRecord.mChannelMask = channelMask + Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask"); + mChannelMaskField.setAccessible(true); + mChannelMaskField.set(audioRecord, channelMask); + + int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding); + int bufferSizeInBytes = minBufferSize * 8; + + // audioRecord.audioBuffSizeCheck(bufferSizeInBytes) + Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class); + audioBuffSizeCheckMethod.setAccessible(true); + audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes); + + final int channelIndexMask = 0; + + int[] sampleRateArray = new int[]{sampleRate}; + int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; + + int initResult; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + // private native final int native_setup(Object audiorecord_this, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, String opPackageName, + // long nativeRecordInJavaObj); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class, + int.class, int.class, int.class, int[].class, String.class, long.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, sampleRateArray, + channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(), + 0L); + } else { + // Assume `context` is never `null` + AttributionSource attributionSource = FakeContext.get().getAttributionSource(); + + // Assume `attributionSource.getPackageName()` is never null + + // ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState() + Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState"); + asScopedParcelStateMethod.setAccessible(true); + + try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) { + Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); + Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // private native int native_setup(Object audiorecordThis, + // Object /*AudioAttributes*/ attributes, + // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, + // int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource, + // long nativeRecordInJavaObj, int maxSharedAudioHistoryMs); + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, + int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, + sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, + attributionSourceParcel, 0L, 0); + } else { + // Android 14 added a new int parameter "halInputFlags" + // + Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, + int.class, int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class, int.class); + nativeSetupMethod.setAccessible(true); + initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference(audioRecord), attributes, + sampleRateArray, channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, + attributionSourceParcel, 0L, 0, 0); + } + } + } + + if (initResult != AudioRecord.SUCCESS) { + Ln.e("Error code " + initResult + " when initializing native AudioRecord object."); + throw new RuntimeException("Cannot create AudioRecord"); + } + + // mSampleRate = sampleRate[0] + Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate"); + mSampleRateField.setAccessible(true); + mSampleRateField.set(audioRecord, sampleRateArray[0]); + + // audioRecord.mSessionId = session[0] + Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId"); + mSessionIdField.setAccessible(true); + mSessionIdField.set(audioRecord, session[0]); + + // audioRecord.mState = AudioRecord.STATE_INITIALIZED + Field mStateField = AudioRecord.class.getDeclaredField("mState"); + mStateField.setAccessible(true); + mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED); + + return audioRecord; + } catch (Exception e) { + Ln.e("Failed to invoke AudioRecord..", e); + throw new RuntimeException("Cannot create AudioRecord"); } } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index 93ed4528..d4bee165 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,23 +1,44 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Intent; import android.os.Binder; +import android.os.Build; +import android.os.Bundle; 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 { +@SuppressLint("PrivateApi,DiscouragedPrivateApi") +public final class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; + private Method startActivityAsUserMethod; + private Method forceStopPackageMethod; - public ActivityManager(IInterface manager) { + static ActivityManager create() { + 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); + return new ActivityManager(am); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private ActivityManager(IInterface manager) { this.manager = manager; } @@ -42,16 +63,17 @@ public class ActivityManager { return removeContentProviderExternalMethod; } + @TargetApi(Build.VERSION_CODES.Q) 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}; + args = new Object[]{name, FakeContext.ROOT_UID, token, null}; } else { // old version - args = new Object[]{name, ServiceManager.USER_ID, token}; + args = new Object[]{name, FakeContext.ROOT_UID, token}; } // ContentProviderHolder providerHolder = getContentProviderExternal(...); Object providerHolder = method.invoke(manager, args); @@ -66,7 +88,7 @@ public class ActivityManager { return null; } return new ContentProvider(this, provider, name, token); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -76,7 +98,7 @@ public class ActivityManager { try { Method method = getRemoveContentProviderExternalMethod(); method.invoke(manager, name, token); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -84,4 +106,54 @@ public class ActivityManager { public ContentProvider createSettingsProvider() { return getContentProviderExternal("settings", new Binder()); } + + private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { + if (startActivityAsUserMethod == null) { + Class iApplicationThreadClass = Class.forName("android.app.IApplicationThread"); + Class profilerInfo = Class.forName("android.app.ProfilerInfo"); + startActivityAsUserMethod = manager.getClass() + .getMethod("startActivityAsUser", iApplicationThreadClass, String.class, Intent.class, String.class, IBinder.class, String.class, + int.class, int.class, profilerInfo, Bundle.class, int.class); + } + return startActivityAsUserMethod; + } + + @SuppressWarnings("ConstantConditions") + public int startActivity(Intent intent) { + try { + Method method = getStartActivityAsUserMethod(); + return (int) method.invoke( + /* this */ manager, + /* caller */ null, + /* callingPackage */ FakeContext.PACKAGE_NAME, + /* intent */ intent, + /* resolvedType */ null, + /* resultTo */ null, + /* resultWho */ null, + /* requestCode */ 0, + /* startFlags */ 0, + /* profilerInfo */ null, + /* bOptions */ null, + /* userId */ /* UserHandle.USER_CURRENT */ -2); + } catch (Throwable e) { + Ln.e("Could not invoke method", e); + return 0; + } + } + + private Method getForceStopPackageMethod() throws NoSuchMethodException { + if (forceStopPackageMethod == null) { + forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class); + } + return forceStopPackageMethod; + } + + public void forceStopPackage(String packageName) { + try { + Method method = getForceStopPackageMethod(); + method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2); + } catch (Throwable e) { + Ln.e("Could not invoke method", e); + } + } } 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 3b3ead0c..bfa64d80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; import android.content.ClipData; @@ -7,17 +8,30 @@ import android.content.IOnPrimaryClipChangedListener; import android.os.Build; import android.os.IInterface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class ClipboardManager { +public final class ClipboardManager { private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; private Method addPrimaryClipChangedListener; private Method removePrimaryClipChangedListener; + private int getMethodVersion; + private int setMethodVersion; + private int addListenerMethodVersion; - public ClipboardManager(IInterface manager) { + static ClipboardManager create() { + IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); + if (clipboard == null) { + // Some devices have no clipboard manager + // + // + return null; + } + return new ClipboardManager(clipboard); + } + + private ClipboardManager(IInterface manager) { this.manager = manager; } @@ -26,7 +40,36 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); } else { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); + getMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); + getMethodVersion = 1; + } catch (NoSuchMethodException e2) { + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); + getMethodVersion = 2; + } catch (NoSuchMethodException e3) { + try { + getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); + getMethodVersion = 3; + } catch (NoSuchMethodException e4) { + try { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); + getMethodVersion = 4; + } catch (NoSuchMethodException e5) { + getPrimaryClipMethod = manager.getClass() + .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, + boolean.class); + getMethodVersion = 5; + } + } + } + } + } } } return getPrimaryClipMethod; @@ -37,37 +80,83 @@ public class ClipboardManager { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); } else { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); + setMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); + setMethodVersion = 1; + } catch (NoSuchMethodException e2) { + try { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); + setMethodVersion = 2; + } catch (NoSuchMethodException e3) { + setPrimaryClipMethod = manager.getClass() + .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); + setMethodVersion = 3; + } + } + } } } return setPrimaryClipMethod; } - private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { + private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); + } + + switch (methodVersion) { + case 0: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + case 1: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + case 2: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + case 3: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); + case 4: + // The last boolean parameter is "userOperate" + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); + default: + return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); } - return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } - private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) - throws InvocationTargetException, IllegalAccessException { + private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); - } else { - method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); + return; + } + + switch (methodVersion) { + case 0: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + break; + case 1: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + break; + case 2: + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + break; + default: + // The last boolean parameter is "userOperate" + method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); } } public CharSequence getText() { try { Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, manager); + ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -77,20 +166,31 @@ public class ClipboardManager { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, manager, clipData); + setPrimaryClip(method, setMethodVersion, manager, clipData); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } } - private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) - throws InvocationTargetException, IllegalAccessException { + private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) + throws ReflectiveOperationException { 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); + method.invoke(manager, listener, FakeContext.PACKAGE_NAME); + return; + } + + switch (methodVersion) { + case 0: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); + break; + case 1: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); + break; + default: + method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); + break; } } @@ -100,8 +200,23 @@ public class ClipboardManager { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); } else { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + try { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); + addListenerMethodVersion = 0; + } catch (NoSuchMethodException e1) { + try { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, + int.class); + addListenerMethodVersion = 1; + } catch (NoSuchMethodException e2) { + addPrimaryClipChangedListener = manager.getClass() + .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, + int.class, int.class); + addListenerMethodVersion = 2; + } + } } } return addPrimaryClipChangedListener; @@ -110,9 +225,9 @@ public class ClipboardManager { public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { try { Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, manager, listener); + addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException 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 index 387c7a60..a03f824e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,16 +1,19 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Ln; +import com.genymobile.scrcpy.SettingsException; import android.annotation.SuppressLint; +import android.content.AttributionSource; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import java.io.Closeable; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class ContentProvider implements Closeable { +public final class ContentProvider implements Closeable { public static final String TABLE_SYSTEM = "system"; public static final String TABLE_SECURE = "secure"; @@ -38,8 +41,6 @@ public class ContentProvider implements Closeable { private Method callMethod; private int callMethodVersion; - private Object attributionSource; - ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { this.manager = manager; this.provider = provider; @@ -50,11 +51,10 @@ public class ContentProvider implements Closeable { @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); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; - } catch (NoSuchMethodException | ClassNotFoundException e0) { + } else { // old versions try { callMethod = provider.getClass() @@ -74,41 +74,30 @@ public class ContentProvider implements Closeable { return callMethod; } - @SuppressLint("PrivateApi") - private Object getAttributionSource() - throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - if (attributionSource == null) { - Class cl = Class.forName("android.content.AttributionSource$Builder"); - Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID); - cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME); - attributionSource = cl.getDeclaredMethod("build").invoke(builder); - } - - return attributionSource; - } - - private Bundle call(String callMethod, String arg, Bundle extras) { + private Bundle call(String callMethod, String arg, Bundle extras) throws ReflectiveOperationException { 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; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) { + args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; + } else { + switch (callMethodVersion) { + case 1: + args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; + break; + case 2: + args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras}; + break; + default: + args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras}; + break; + } } return (Bundle) method.invoke(provider, args); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); - return null; + throw e; } } @@ -142,30 +131,31 @@ public class ContentProvider implements Closeable { } } - public String getValue(String table, String key) { + public String getValue(String table, String key) throws SettingsException { 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; + arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID); + try { + Bundle bundle = call(method, key, arg); + if (bundle == null) { + return null; + } + return bundle.getString("value"); + } catch (Exception e) { + throw new SettingsException(table, "get", key, null, e); } - return bundle.getString("value"); + } - public void putValue(String table, String key, String value) { + public void putValue(String table, String key, String value) throws SettingsException { String method = getPutMethod(table); Bundle arg = new Bundle(); - arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); + arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID); 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); + try { + call(method, key, arg); + } catch (Exception e) { + throw new SettingsException(table, "put", key, value, e); } - return oldValue; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java new file mode 100644 index 00000000..ba3e9ee0 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -0,0 +1,79 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.IBinder; + +import java.lang.reflect.Method; + +@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public final class DisplayControl { + + private static final Class CLASS; + + static { + Class displayControlClass = null; + try { + Class classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory"); + Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class, + ClassLoader.class, int.class, boolean.class, String.class); + ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null, + ClassLoader.getSystemClassLoader(), 0, true, null); + + displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); + + Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class); + loadMethod.setAccessible(true); + loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers"); + } catch (Throwable e) { + Ln.e("Could not initialize DisplayControl", e); + // Do not throw an exception here, the methods will fail when they are called + } + CLASS = displayControlClass; + } + + private static Method getPhysicalDisplayTokenMethod; + private static Method getPhysicalDisplayIdsMethod; + + private DisplayControl() { + // only static methods + } + + private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { + if (getPhysicalDisplayTokenMethod == null) { + getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + return getPhysicalDisplayTokenMethod; + } + + public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { + try { + Method method = getGetPhysicalDisplayTokenMethod(); + return (IBinder) method.invoke(null, physicalDisplayId); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { + if (getPhysicalDisplayIdsMethod == null) { + getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); + } + return getPhysicalDisplayIdsMethod; + } + + public static long[] getPhysicalDisplayIds() { + try { + Method method = getGetPhysicalDisplayIdsMethod(); + return (long[]) method.invoke(null); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return null; + } + } +} 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 3d347709..ed1c146d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,26 +1,96 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.Command; import com.genymobile.scrcpy.DisplayInfo; import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Size; import android.os.IInterface; +import android.annotation.SuppressLint; +import android.hardware.display.VirtualDisplay; import android.view.Display; +import android.view.Surface; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class DisplayManager { - private final IInterface manager; + private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal + private Method createVirtualDisplayMethod; - public DisplayManager(IInterface manager) { + static DisplayManager create() { + try { + Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); + Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); + Object dmg = getInstanceMethod.invoke(null); + return new DisplayManager(dmg); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private DisplayManager(Object manager) { this.manager = manager; } + // public to call it from unit tests + public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { + Pattern regex = Pattern.compile( + "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " + + "rotation ([0-9]+).*?, layerStack ([0-9]+)", + Pattern.MULTILINE); + Matcher m = regex.matcher(dumpsysDisplayOutput); + if (!m.find()) { + return null; + } + int flags = parseDisplayFlags(m.group(1)); + int width = Integer.parseInt(m.group(2)); + int height = Integer.parseInt(m.group(3)); + int rotation = Integer.parseInt(m.group(4)); + int layerStack = Integer.parseInt(m.group(5)); + + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + } + + private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { + try { + String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display"); + return parseDisplayInfo(dumpsysDisplayOutput, displayId); + } catch (Exception e) { + Ln.e("Could not get display info from \"dumpsys display\" output", e); + return null; + } + } + + private static int parseDisplayFlags(String text) { + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); + if (text == null) { + return 0; + } + + int flags = 0; + Matcher m = regex.matcher(text); + while (m.find()) { + String flagString = m.group(); + try { + Field filed = Display.class.getDeclaredField(flagString); + flags |= filed.getInt(null); + } catch (ReflectiveOperationException e) { + // Silently ignore, some flags reported by "dumpsys display" are @TestApi + } + } + return flags; + } + public DisplayInfo getDisplayInfo(int displayId) { try { Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { - return null; + // fallback when displayInfo is null + return getDisplayInfoFromDumpsysDisplay(displayId); } Class cls = displayInfo.getClass(); // width and height already take the rotation into account @@ -30,7 +100,7 @@ public final class DisplayManager { 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) { + } catch (ReflectiveOperationException e) { throw new AssertionError(e); } } @@ -49,4 +119,17 @@ public final class DisplayManager { throw new AssertionError(e); } } + + private Method getCreateVirtualDisplayMethod() throws NoSuchMethodException { + if (createVirtualDisplayMethod == null) { + createVirtualDisplayMethod = android.hardware.display.DisplayManager.class + .getMethod("createVirtualDisplay", String.class, int.class, int.class, int.class, Surface.class); + } + return createVirtualDisplayMethod; + } + + public VirtualDisplay createVirtualDisplay(String name, int width, int height, int displayIdToMirror, Surface surface) throws Exception { + Method method = getCreateVirtualDisplayMethod(); + return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); + } } 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 e17b5a17..16ecb09f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -2,24 +2,46 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; -import android.os.IInterface; +import android.annotation.SuppressLint; import android.view.InputEvent; +import android.view.MotionEvent; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +@SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final IInterface manager; + private final Object manager; private Method injectInputEventMethod; private static Method setDisplayIdMethod; + private static Method setActionButtonMethod; - public InputManager(IInterface manager) { + static InputManager create() { + try { + Class inputManagerClass = getInputManagerClass(); + Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); + Object im = getInstanceMethod.invoke(null); + return new InputManager(im); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + private static Class getInputManagerClass() { + try { + // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview + return Class.forName("android.hardware.input.InputManagerGlobal"); + } catch (ClassNotFoundException e) { + return android.hardware.input.InputManager.class; + } + } + + private InputManager(Object manager) { this.manager = manager; } @@ -34,7 +56,7 @@ public final class InputManager { try { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } @@ -52,9 +74,27 @@ public final class InputManager { Method method = getSetDisplayIdMethod(); method.invoke(inputEvent, displayId); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Cannot associate a display id to the input event", e); return false; } } + + private static Method getSetActionButtonMethod() throws NoSuchMethodException { + if (setActionButtonMethod == null) { + setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class); + } + return setActionButtonMethod; + } + + public static boolean setActionButton(MotionEvent motionEvent, int actionButton) { + try { + Method method = getSetActionButtonMethod(); + method.invoke(motionEvent, actionButton); + return true; + } catch (ReflectiveOperationException e) { + Ln.e("Cannot set action button on MotionEvent", e); + return false; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 8ff074b3..36d5f1ac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -6,21 +6,25 @@ import android.annotation.SuppressLint; import android.os.Build; import android.os.IInterface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class PowerManager { private final IInterface manager; private Method isScreenOnMethod; - public PowerManager(IInterface manager) { + static PowerManager create() { + IInterface manager = ServiceManager.getService("power", "android.os.IPowerManager"); + return new PowerManager(manager); + } + + private PowerManager(IInterface manager) { this.manager = manager; } private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; @@ -30,7 +34,7 @@ public final class PowerManager { try { Method method = getIsScreenOnMethod(); return (boolean) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", 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 6f4b9c04..a8a56dab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -1,38 +1,45 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; + import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.camera2.CameraManager; import android.os.IBinder; import android.os.IInterface; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class ServiceManager { - public static final String PACKAGE_NAME = "com.android.shell"; - public static final int USER_ID = 0; - - private final Method getServiceMethod; - - private WindowManager windowManager; - private DisplayManager displayManager; - private InputManager inputManager; - private PowerManager powerManager; - private StatusBarManager statusBarManager; - private ClipboardManager clipboardManager; - private ActivityManager activityManager; + private static final Method GET_SERVICE_METHOD; - public ServiceManager() { + static { try { - getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); + GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); } catch (Exception e) { throw new AssertionError(e); } } - private IInterface getService(String service, String type) { + private static WindowManager windowManager; + private static DisplayManager displayManager; + private static InputManager inputManager; + private static PowerManager powerManager; + private static StatusBarManager statusBarManager; + private static ClipboardManager clipboardManager; + private static ActivityManager activityManager; + private static CameraManager cameraManager; + + private ServiceManager() { + /* not instantiable */ + } + + static IInterface getService(String service, String type) { try { - IBinder binder = (IBinder) getServiceMethod.invoke(null, service); + IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); return (IInterface) asInterfaceMethod.invoke(null, binder); } catch (Exception e) { @@ -40,69 +47,65 @@ public final class ServiceManager { } } - public WindowManager getWindowManager() { + public static WindowManager getWindowManager() { if (windowManager == null) { - windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); + windowManager = WindowManager.create(); } return windowManager; } - public DisplayManager getDisplayManager() { + public static DisplayManager getDisplayManager() { if (displayManager == null) { - displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); + displayManager = DisplayManager.create(); } return displayManager; } - public InputManager getInputManager() { + public static InputManager getInputManager() { if (inputManager == null) { - inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager")); + inputManager = InputManager.create(); } return inputManager; } - public PowerManager getPowerManager() { + public static PowerManager getPowerManager() { if (powerManager == null) { - powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); + powerManager = PowerManager.create(); } return powerManager; } - public StatusBarManager getStatusBarManager() { + public static StatusBarManager getStatusBarManager() { if (statusBarManager == null) { - statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); + statusBarManager = StatusBarManager.create(); } return statusBarManager; } - public ClipboardManager getClipboardManager() { + public static ClipboardManager getClipboardManager() { if (clipboardManager == null) { - IInterface clipboard = getService("clipboard", "android.content.IClipboard"); - if (clipboard == null) { - // Some devices have no clipboard manager - // - // - return null; - } - clipboardManager = new ClipboardManager(clipboard); + // May be null, some devices have no clipboard manager + clipboardManager = ClipboardManager.create(); } return clipboardManager; } - public ActivityManager getActivityManager() { + public static ActivityManager getActivityManager() { if (activityManager == null) { + activityManager = ActivityManager.create(); + } + return activityManager; + } + + public static CameraManager getCameraManager() { + if (cameraManager == 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); + Constructor ctor = CameraManager.class.getDeclaredConstructor(Context.class); + cameraManager = ctor.newInstance(FakeContext.get()); } catch (Exception e) { throw new AssertionError(e); } } - - return activityManager; + return cameraManager; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java index 5b1e5f5e..af217da2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java @@ -4,24 +4,35 @@ import com.genymobile.scrcpy.Ln; import android.os.IInterface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -public class StatusBarManager { +public final class StatusBarManager { private final IInterface manager; private Method expandNotificationsPanelMethod; + private boolean expandNotificationPanelMethodCustomVersion; private Method expandSettingsPanelMethod; private boolean expandSettingsPanelMethodNewVersion = true; private Method collapsePanelsMethod; - public StatusBarManager(IInterface manager) { + static StatusBarManager create() { + IInterface manager = ServiceManager.getService("statusbar", "com.android.internal.statusbar.IStatusBarService"); + return new StatusBarManager(manager); + } + + private StatusBarManager(IInterface manager) { this.manager = manager; } private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { - expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + try { + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel"); + } catch (NoSuchMethodException e) { + // Custom version for custom vendor ROM: + expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class); + expandNotificationPanelMethodCustomVersion = true; + } } return expandNotificationsPanelMethod; } @@ -50,8 +61,12 @@ public class StatusBarManager { public void expandNotificationsPanel() { try { Method method = getExpandNotificationsPanelMethod(); - method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + if (expandNotificationPanelMethodCustomVersion) { + method.invoke(manager, 0); + } else { + method.invoke(manager); + } + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -66,7 +81,7 @@ public class StatusBarManager { // old version method.invoke(manager); } - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -75,7 +90,7 @@ public class StatusBarManager { try { Method method = getCollapsePanelsMethod(); method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } 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 8fbb860b..f0e351a2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -8,7 +8,6 @@ import android.os.Build; import android.os.IBinder; import android.view.Surface; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi") @@ -30,6 +29,8 @@ public final class SurfaceControl { private static Method getBuiltInDisplayMethod; private static Method setDisplayPowerModeMethod; + private static Method getPhysicalDisplayTokenMethod; + private static Method getPhysicalDisplayIdsMethod; private SurfaceControl() { // only static methods @@ -76,12 +77,8 @@ public final class SurfaceControl { } } - public static IBinder createDisplay(String name, boolean secure) { - try { - return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); - } catch (Exception e) { - throw new AssertionError(e); - } + public static IBinder createDisplay(String name, boolean secure) throws Exception { + return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure); } private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { @@ -98,7 +95,6 @@ public final class SurfaceControl { } public static IBinder getBuiltInDisplay() { - try { Method method = getGetBuiltInDisplayMethod(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { @@ -108,7 +104,50 @@ public final class SurfaceControl { // call getInternalDisplayToken() return (IBinder) method.invoke(null); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { + if (getPhysicalDisplayTokenMethod == null) { + getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + return getPhysicalDisplayTokenMethod; + } + + public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { + try { + Method method = getGetPhysicalDisplayTokenMethod(); + return (IBinder) method.invoke(null, physicalDisplayId); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { + if (getPhysicalDisplayIdsMethod == null) { + getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); + } + return getPhysicalDisplayIdsMethod; + } + + public static boolean hasPhysicalDisplayIdsMethod() { + try { + getGetPhysicalDisplayIdsMethod(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static long[] getPhysicalDisplayIds() { + try { + Method method = getGetPhysicalDisplayIdsMethod(); + return (long[]) method.invoke(null); + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; } @@ -126,7 +165,7 @@ public final class SurfaceControl { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); return true; - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException 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 368fe4c3..b4488b7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -2,21 +2,30 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; +import android.annotation.TargetApi; import android.os.IInterface; +import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public final class WindowManager { private final IInterface manager; private Method getRotationMethod; private Method freezeRotationMethod; + private Method freezeDisplayRotationMethod; private Method isRotationFrozenMethod; + private Method isDisplayRotationFrozenMethod; private Method thawRotationMethod; private Method removeRotationWatcherMethod; + private Method thawDisplayRotationMethod; - public WindowManager(IInterface manager) { + static WindowManager create() { + IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); + return new WindowManager(manager); + } + + private WindowManager(IInterface manager) { this.manager = manager; } @@ -42,6 +51,15 @@ public final class WindowManager { return freezeRotationMethod; } + // New method added by this commit: + // + private Method getFreezeDisplayRotationMethod() throws NoSuchMethodException { + if (freezeDisplayRotationMethod == null) { + freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class); + } + return freezeDisplayRotationMethod; + } + private Method getIsRotationFrozenMethod() throws NoSuchMethodException { if (isRotationFrozenMethod == null) { isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); @@ -49,6 +67,15 @@ public final class WindowManager { return isRotationFrozenMethod; } + // New method added by this commit: + // + private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException { + if (isDisplayRotationFrozenMethod == null) { + isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class); + } + return isDisplayRotationFrozenMethod; + } + private Method getThawRotationMethod() throws NoSuchMethodException { if (thawRotationMethod == null) { thawRotationMethod = manager.getClass().getMethod("thawRotation"); @@ -62,41 +89,78 @@ public final class WindowManager { } return removeRotationWatcherMethod; } + + // New method added by this commit: + // + private Method getThawDisplayRotationMethod() throws NoSuchMethodException { + if (thawDisplayRotationMethod == null) { + thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class); + } + return thawDisplayRotationMethod; + } public int getRotation() { try { Method method = getGetRotationMethod(); return (int) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return 0; } } - public void freezeRotation(int rotation) { + public void freezeRotation(int displayId, int rotation) { try { - Method method = getFreezeRotationMethod(); - method.invoke(manager, rotation); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + try { + Method method = getFreezeDisplayRotationMethod(); + method.invoke(manager, displayId, rotation); + } catch (ReflectiveOperationException e) { + if (displayId == 0) { + Method method = getFreezeRotationMethod(); + method.invoke(manager, rotation); + } else { + Ln.e("Could not invoke method", e); + } + } + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } - public boolean isRotationFrozen() { + public boolean isRotationFrozen(int displayId) { try { - Method method = getIsRotationFrozenMethod(); - return (boolean) method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + try { + Method method = getIsDisplayRotationFrozenMethod(); + return (boolean) method.invoke(manager, displayId); + } catch (ReflectiveOperationException e) { + if (displayId == 0) { + Method method = getIsRotationFrozenMethod(); + return (boolean) method.invoke(manager); + } else { + Ln.e("Could not invoke method", e); + return false; + } + } + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return false; } } - public void thawRotation() { + public void thawRotation(int displayId) { try { - Method method = getThawRotationMethod(); - method.invoke(manager); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + try { + Method method = getThawDisplayRotationMethod(); + method.invoke(manager, displayId); + } catch (ReflectiveOperationException e) { + if (displayId == 0) { + Method method = getThawRotationMethod(); + method.invoke(manager); + } else { + Ln.e("Could not invoke method", e); + } + } + } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); } } @@ -113,7 +177,17 @@ public final class WindowManager { cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) { - throw new AssertionError(e); + Ln.e("Could not register rotation watcher", e); + } + } + + @TargetApi(29) + public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { + try { + Class cls = manager.getClass(); + cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); + } catch (Exception e) { + Ln.e("Could not register display fold listener", e); } } diff --git a/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java b/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java new file mode 100644 index 00000000..569a2f2c --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java @@ -0,0 +1,42 @@ +package com.genymobile.scrcpy; + +import org.junit.Assert; +import org.junit.Test; + +public class BinaryTest { + + @Test + public void testU16FixedPointToFloat() { + final float delta = 0.0f; // on these values, there MUST be no rounding error + Assert.assertEquals(0.0f, Binary.u16FixedPointToFloat((short) 0), delta); + Assert.assertEquals(0.03125f, Binary.u16FixedPointToFloat((short) 0x800), delta); + Assert.assertEquals(0.0625f, Binary.u16FixedPointToFloat((short) 0x1000), delta); + Assert.assertEquals(0.125f, Binary.u16FixedPointToFloat((short) 0x2000), delta); + Assert.assertEquals(0.25f, Binary.u16FixedPointToFloat((short) 0x4000), delta); + Assert.assertEquals(0.5f, Binary.u16FixedPointToFloat((short) 0x8000), delta); + Assert.assertEquals(0.75f, Binary.u16FixedPointToFloat((short) 0xc000), delta); + Assert.assertEquals(1.0f, Binary.u16FixedPointToFloat((short) 0xffff), delta); + } + + @Test + public void testI16FixedPointToFloat() { + final float delta = 0.0f; // on these values, there MUST be no rounding error + + Assert.assertEquals(0.0f, Binary.i16FixedPointToFloat((short) 0), delta); + Assert.assertEquals(0.03125f, Binary.i16FixedPointToFloat((short) 0x400), delta); + Assert.assertEquals(0.0625f, Binary.i16FixedPointToFloat((short) 0x800), delta); + Assert.assertEquals(0.125f, Binary.i16FixedPointToFloat((short) 0x1000), delta); + Assert.assertEquals(0.25f, Binary.i16FixedPointToFloat((short) 0x2000), delta); + Assert.assertEquals(0.5f, Binary.i16FixedPointToFloat((short) 0x4000), delta); + Assert.assertEquals(0.75f, Binary.i16FixedPointToFloat((short) 0x6000), delta); + Assert.assertEquals(1.0f, Binary.i16FixedPointToFloat((short) 0x7fff), delta); + + Assert.assertEquals(-0.03125f, Binary.i16FixedPointToFloat((short) -0x400), delta); + Assert.assertEquals(-0.0625f, Binary.i16FixedPointToFloat((short) -0x800), delta); + Assert.assertEquals(-0.125f, Binary.i16FixedPointToFloat((short) -0x1000), delta); + Assert.assertEquals(-0.25f, Binary.i16FixedPointToFloat((short) -0x2000), delta); + Assert.assertEquals(-0.5f, Binary.i16FixedPointToFloat((short) -0x4000), delta); + Assert.assertEquals(-0.75f, Binary.i16FixedPointToFloat((short) -0x6000), delta); + Assert.assertEquals(-1.0f, Binary.i16FixedPointToFloat((short) -0x8000), delta); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java new file mode 100644 index 00000000..de996a07 --- /dev/null +++ b/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java @@ -0,0 +1,242 @@ +package com.genymobile.scrcpy; + +import com.genymobile.scrcpy.wrappers.DisplayManager; + +import android.view.Display; +import org.junit.Assert; +import org.junit.Test; + +public class CommandParserTest { + @Test + public void testParseDisplayInfoFromDumpsysDisplay() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + "mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state OFF, " + + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 1440 x 3120, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + + "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 " + + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + + "relativeAddress=null}, removeMode 0}\n" + + " mRequestedMinimalPostProcessing=false\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(1440, displayInfo.getSize().getWidth()); + Assert.assertEquals(3120, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayWithRotation() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + "mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state ON, " + + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + + "FLAG_TRUSTED, real 3120 x 1440, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + + "minimalPostProcessingSupported false, rotation 3, state ON, type INTERNAL, uniqueId \"local:0\", app 3120 x 1440, density 600 " + + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + + "relativeAddress=null}, removeMode 0}\n" + + " mRequestedMinimalPostProcessing=false"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(3, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(3120, displayInfo.getSize().getWidth()); + Assert.assertEquals(1440, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI31() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mPhase=1\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 " + + "Infinity]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, " + + "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, " + + "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mRequestedMinimalPostProcessing=false\n" + + " mFrameRateOverrides=[]\n" + + " mPendingFrameRateOverrideUids={}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported + Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); + Assert.assertEquals(1080, displayInfo.getSize().getWidth()); + Assert.assertEquals(2280, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI31NoFlags() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=1\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mPhase=1\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 " + + "Infinity]}\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + + "real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + + "real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff " + + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + + " mRequestedMinimalPostProcessing=false\n" + + " mFrameRateOverrides=[]\n" + + " mPendingFrameRateOverrideUids={}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(0, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(0, displayInfo.getLayerStack()); + Assert.assertEquals(0, displayInfo.getFlags()); + Assert.assertEquals(1080, displayInfo.getSize().getWidth()); + Assert.assertEquals(2280, displayInfo.getSize().getHeight()); + } + + @Test + public void testParseDisplayInfoFromDumpsysDisplayAPI29WithNoFlags() { + /* @formatter:off */ + String partialOutput = "Logical Displays: size=2\n" + + " Display 0:\n" + + " mDisplayId=0\n" + + " mLayerStack=0\n" + + " mHasContent=true\n" + + " mAllowedDisplayModes=[1]\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=Built-in Screen\n" + + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + + "real 3664 x 1920, largest app 3664 x 1920, smallest app 3664 x 1920, mode 61, defaultMode 61, modes [" + + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + + "real 3664 x 1920, largest app 3664 x 3620, smallest app 1920 x 1876, mode 61, defaultMode 61, modes [" + + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + + " Display 31:\n" + + " mDisplayId=31\n" + + " mLayerStack=31\n" + + " mHasContent=true\n" + + " mAllowedDisplayModes=[92]\n" + + " mRequestedColorMode=0\n" + + " mDisplayOffset=(0, 0)\n" + + " mDisplayScalingDisabled=false\n" + + " mPrimaryDisplayDevice=PanelLayer-#main\n" + + " mBaseDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId " + + "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 110, smallest app 800 x " + + "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + + "type VIRTUAL, state ON, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n" + + " mOverrideDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId " + + "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 800, smallest app 110 x " + + "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + + "type VIRTUAL, state OFF, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n"; + DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 31); + Assert.assertNotNull(displayInfo); + Assert.assertEquals(31, displayInfo.getDisplayId()); + Assert.assertEquals(0, displayInfo.getRotation()); + Assert.assertEquals(31, displayInfo.getLayerStack()); + Assert.assertEquals(0, displayInfo.getFlags()); + Assert.assertEquals(800, displayInfo.getSize().getWidth()); + Assert.assertEquals(110, displayInfo.getSize().getHeight()); + } +} diff --git a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java index da568486..0c8086f7 100644 --- a/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java @@ -2,7 +2,6 @@ package com.genymobile.scrcpy; import android.view.KeyEvent; import android.view.MotionEvent; - import org.junit.Assert; import org.junit.Test; @@ -13,7 +12,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; - public class ControlMessageReaderTest { @Test @@ -95,7 +93,8 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0xffff); // pressure - dos.writeInt(MotionEvent.BUTTON_PRIMARY); + dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button + dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons byte[] packet = bos.toByteArray(); @@ -113,6 +112,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact + Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons()); } @@ -127,8 +127,9 @@ public class ControlMessageReaderTest { dos.writeInt(1026); dos.writeShort(1080); dos.writeShort(1920); + dos.writeShort(0); // 0.0f encoded as i16 + dos.writeShort(0x8000); // -1.0f encoded as i16 dos.writeInt(1); - dos.writeInt(-1); byte[] packet = bos.toByteArray(); @@ -143,8 +144,9 @@ public class ControlMessageReaderTest { Assert.assertEquals(1026, event.getPosition().getPoint().getY()); Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); - Assert.assertEquals(1, event.getHScroll()); - Assert.assertEquals(-1, event.getVScroll()); + Assert.assertEquals(0f, event.getHScroll(), 0f); + Assert.assertEquals(-1f, event.getVScroll(), 0f); + Assert.assertEquals(1, event.getButtons()); } @Test @@ -220,6 +222,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD); + dos.writeByte(ControlMessage.COPY_KEY_COPY); byte[] packet = bos.toByteArray(); @@ -227,6 +230,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); + Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); } @Test @@ -236,6 +240,7 @@ public class ControlMessageReaderTest { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); + dos.writeLong(0x0102030405060708L); // sequence dos.writeByte(1); // paste byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeInt(text.length); @@ -247,6 +252,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(0x0102030405060708L, event.getSequence()); Assert.assertEquals("testé", event.getText()); Assert.assertTrue(event.getPaste()); } @@ -260,6 +266,7 @@ public class ControlMessageReaderTest { dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; + dos.writeLong(0x0807060504030201L); // sequence dos.writeByte(1); // paste Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); @@ -273,6 +280,7 @@ public class ControlMessageReaderTest { ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); + Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); Assert.assertTrue(event.getPaste()); } @@ -314,6 +322,66 @@ public class ControlMessageReaderTest { Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); } + @Test + public void testParseUhidCreate() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_CREATE); + dos.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + dos.writeShort(data.length); // size + dos.write(data); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); + Assert.assertEquals(42, event.getId()); + Assert.assertArrayEquals(data, event.getData()); + } + + @Test + public void testParseUhidInput() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_UHID_INPUT); + dos.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5}; + dos.writeShort(data.length); // size + dos.write(data); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_UHID_INPUT, event.getType()); + Assert.assertEquals(42, event.getId()); + Assert.assertArrayEquals(data, event.getData()); + } + + @Test + public void testParseOpenHardKeyboardSettings() throws IOException { + ControlMessageReader reader = new ControlMessageReader(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS); + + byte[] packet = bos.toByteArray(); + + reader.readFrom(new ByteArrayInputStream(packet)); + ControlMessage event = reader.next(); + + Assert.assertEquals(ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS, event.getType()); + } + @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); diff --git a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java index 88bf2af9..d7f926ba 100644 --- a/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java @@ -32,4 +32,47 @@ public class DeviceMessageWriterTest { Assert.assertArrayEquals(expected, actual); } + + @Test + public void testSerializeAckSetClipboard() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD); + dos.writeLong(0x0102030405060708L); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } + + @Test + public void testSerializeUhidOutput() throws IOException { + DeviceMessageWriter writer = new DeviceMessageWriter(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(DeviceMessage.TYPE_UHID_OUTPUT); + dos.writeShort(42); // id + byte[] data = {1, 2, 3, 4, 5}; + dos.writeShort(data.length); + dos.write(data); + + byte[] expected = bos.toByteArray(); + + DeviceMessage msg = DeviceMessage.createUhidOutput(42, data); + bos = new ByteArrayOutputStream(); + writer.writeTo(msg, bos); + + byte[] actual = bos.toByteArray(); + + Assert.assertArrayEquals(expected, actual); + } }