pax_global_header00006660000000000000000000000064141512413600014507gustar00rootroot0000000000000052 comment=003e7381064f2c0e8ce1095d1a65522fab4e118b scrcpy-1.21/000077500000000000000000000000001415124136000127355ustar00rootroot00000000000000scrcpy-1.21/.github/000077500000000000000000000000001415124136000142755ustar00rootroot00000000000000scrcpy-1.21/.github/ISSUE_TEMPLATE/000077500000000000000000000000001415124136000164605ustar00rootroot00000000000000scrcpy-1.21/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014421415124136000211530ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- - [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md). - [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues). **Environment** - OS: [e.g. Debian, Windows, macOS...] - scrcpy version: [e.g. 1.12.1] - installation method: [e.g. manual build, apt, snap, brew, Windows release...] - device model: - Android version: [e.g. 10] **Describe the bug** A clear and concise description of what the bug is. On errors, please provide the output of the console (and `adb logcat` if relevant). ``` Please paste terminal output in a code block. ``` Please do not post screenshots of your terminal, just post the content as text instead. scrcpy-1.21/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000014051415124136000222050ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- - [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist. **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. scrcpy-1.21/.gitignore000066400000000000000000000001231415124136000147210ustar00rootroot00000000000000build/ /dist/ /build-*/ /build_*/ /release-*/ .idea/ .gradle/ /x/ local.properties scrcpy-1.21/BUILD.md000066400000000000000000000156211415124136000141230ustar00rootroot00000000000000# Build scrcpy 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 \ 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 ``` ## 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. If you want to contribute code, please base your commits on the latest `dev` branch. ## Requirements You need [adb]. It is available in the [Android SDK platform tools][platform-tools], or packaged in your distribution (`adb`). On Windows, download the [platform-tools][platform-tools-windows] and extract the following files to a directory accessible from your `PATH`: - `adb.exe` - `AdbWinApi.dll` - `AdbWinUsbApi.dll` The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions. [adb]: https://developer.android.com/studio/command-line/adb.html [platform-tools]: https://developer.android.com/studio/releases/platform-tools.html [platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip [ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg [LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer ## System-specific steps ### Linux Install the required packages from your package manager. #### Debian/Ubuntu ```bash # runtime dependencies 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 \ libusb-1.0-0-dev # server build dependencies sudo apt install openjdk-11-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install it from `pip3`: ```bash sudo apt install python3-pip pip3 install meson ``` #### Fedora ```bash # enable RPM fusion free 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 libusb-devel meson gcc make # server build dependencies sudo dnf install java-devel ``` ### Windows #### Cross-compile from Linux This is the preferred method (and the way the release is built). From _Debian_, install _mingw_: ```bash sudo apt install mingw-w64 mingw-w64-tools ``` You also need the JDK to build the server: ```bash sudo apt install openjdk-11-jdk ``` Then generate the releases: ```bash ./release.sh ``` It will generate win32 and win64 releases into `dist/`. #### In MSYS2 From Windows, you need [MSYS2] to build the project. From an MSYS2 terminal, install the required packages: [MSYS2]: http://www.msys2.org/ ```bash # runtime dependencies pacman -S mingw-w64-x86_64-SDL2 \ mingw-w64-x86_64-ffmpeg # client build dependencies pacman -S mingw-w64-x86_64-make \ mingw-w64-x86_64-gcc \ mingw-w64-x86_64-pkg-config \ mingw-w64-x86_64-meson ``` For a 32 bits version, replace `x86_64` by `i686`: ```bash # runtime dependencies pacman -S mingw-w64-i686-SDL2 \ mingw-w64-i686-ffmpeg # client build dependencies pacman -S mingw-w64-i686-make \ mingw-w64-i686-gcc \ mingw-w64-i686-pkg-config \ mingw-w64-i686-meson ``` Java (>= 7) is not available in MSYS2, so if you plan to build the server, install it manually and make it available from the `PATH`: ```bash export PATH="$JAVA_HOME/bin:$PATH" ``` ### Mac OS Install the packages with [Homebrew]: [Homebrew]: https://brew.sh/ ```bash # runtime dependencies brew install sdl2 ffmpeg # client build dependencies brew install pkg-config meson ``` Additionally, if you want to build the server, install Java 8 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)" export PATH="$JAVA_HOME/bin:$PATH" ``` ### Docker See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker). ## Common steps **As a non-root user**, clone the project: ```bash git clone https://github.com/Genymobile/scrcpy cd scrcpy ``` ### Build You may want to build only the client: the server binary, which will be pushed to the Android device, does not depend on your system and architecture. In that case, use the [prebuilt server] (so you will not need Java or the Android SDK). [prebuilt server]: #option-2-use-prebuilt-server #### Option 1: Build everything from sources Install the [Android SDK] (_Android Studio_), and set `ANDROID_SDK_ROOT` to its directory. For example: [Android SDK]: https://developer.android.com/studio/index.html ```bash # Linux export ANDROID_SDK_ROOT=~/Android/Sdk # Mac export ANDROID_SDK_ROOT=~/Library/Android/sdk # Windows set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk ``` Then, build: ```bash meson x --buildtype release --strip -Db_lto=true ninja -Cx # DO NOT RUN AS ROOT ``` _Note: `ninja` [must][ninja-user] be run as a non-root user (only `ninja install` must be run as root)._ [ninja-user]: https://github.com/Genymobile/scrcpy/commit/4c49b27e9f6be02b8e63b508b60535426bd0291a #### Option 2: Use prebuilt server - [`scrcpy-server-v1.20`][direct-scrcpy-server] _(SHA-256: b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34)_ [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20 Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash meson x --buildtype release --strip -Db_lto=true \ -Dprebuilt_server=/path/to/scrcpy-server ninja -Cx # DO NOT RUN AS ROOT ``` The server only works with a matching client version (this server works with the `master` branch). ### Run without installing: ```bash ./run x [options] ``` ### Install After a successful build, you can install _scrcpy_ on the system: ```bash sudo ninja -Cx install # without sudo on Windows ``` This installs three files: - `/usr/local/bin/scrcpy` - `/usr/local/share/scrcpy/scrcpy-server` - `/usr/local/share/man/man1/scrcpy.1` You can then [run](README.md#run) _scrcpy_. ### Uninstall ```bash sudo ninja -Cx uninstall # without sudo on Windows ``` scrcpy-1.21/DEVELOP.md000066400000000000000000000311221415124136000143540ustar00rootroot00000000000000# 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_. scrcpy-1.21/FAQ.it.md000066400000000000000000000177301415124136000143110ustar00rootroot00000000000000_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 ### Problema con Wayland Per impostazione predefinita, SDL utilizza x11 su Linux. Il [video driver] può essere cambiato attraversio la variabile d'ambiente `SDL_VIDEODRIVER`: [video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver ```bash export SDL_VIDEODRIVER=wayland scrcpy ``` Su alcune distribuzioni (almeno Fedora), il pacchetto `libdecor` deve essere installato manualmente. Vedi le issues [#2554] e [#2559]. [#2554]: https://github.com/Genymobile/scrcpy/issues/2554 [#2559]: https://github.com/Genymobile/scrcpy/issues/2559 ### Crash del compositore KWin In Plasma Desktop, il compositore è disabilitato mentre _scrcpy_ è in esecuzione. Come soluzione alternativa, [disattiva la "composizione dei blocchi"][kwin] (link in inglese). [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 scrcpy-1.21/FAQ.ko.md000066400000000000000000000055061415124136000143040ustar00rootroot00000000000000# 자주하는 질문 (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 ``` scrcpy-1.21/FAQ.md000066400000000000000000000177721415124136000137040ustar00rootroot00000000000000# Frequently Asked Questions [Read in another language](#translations) Here are the common reported problems and their status. ## `adb` 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 You need `adb` accessible from your `PATH`. 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 Check that you correctly enabled [adb debugging][enable-adb]. If your device is not detected, you may need some [drivers] (on Windows). [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html ### 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 the identifier of the device you want to mirror must be provided: ```bash scrcpy -s 01234567890abcdef ``` Note that if your device is connected over TCP/IP, you'll get this message: > adb: error: more than one device/emulator > ERROR: "adb reverse" returned with value 1 > WARN: 'adb reverse' failed, fallback to 'adb forward' This is expected (due to a bug on old Android versions, see [#5]), but in that case, scrcpy fallbacks to a different method, which should work. [#5]: https://github.com/Genymobile/scrcpy/issues/5 ### Conflicts between adb versions > adb server version (41) doesn't match this client (39); killing... This error occurs when you use several `adb` versions simultaneously. You must find the program using a different `adb` version, and use the same `adb` version everywhere. 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 scrcpy ``` ### Device disconnected If _scrcpy_ stops itself with the warning "Device disconnected", then the `adb` connection has been closed. Try with another USB cable or plug it into another USB port. See [#281] and [#283]. [#281]: https://github.com/Genymobile/scrcpy/issues/281 [#283]: https://github.com/Genymobile/scrcpy/issues/283 ## Control issues ### Mouse and keyboard do not work On some devices, you may need to enable an option to allow [simulating input]. In developer options, enable: > **USB debugging (Security settings)** > _Allow granting permissions and simulating input via USB debugging_ [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 ### Special characters do not work The default text injection method is [limited to ASCII characters][text-input]. A trick allows to also inject some [accented characters][accented-characters], but that's all. See [#37]. Since scrcpy v1.20 on Linux, it is possible to simulate a [physical keyboard][hid] (HID). [text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [#37]: https://github.com/Genymobile/scrcpy/issues/37 [hid]: README.md#physical-keyboard-simulation-hid ## Client issues ### 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 `SDL_VIDEODRIVER` environment variable: [video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver ```bash export SDL_VIDEODRIVER=wayland scrcpy ``` On some distributions (at least Fedora), the package `libdecor` must be installed manually. See issues [#2554] and [#2559]. [#2554]: https://github.com/Genymobile/scrcpy/issues/2554 [#2559]: https://github.com/Genymobile/scrcpy/issues/2559 ### KWin compositor crashes On Plasma Desktop, compositor is disabled while _scrcpy_ is running. As a workaround, [disable "Block compositing"][kwin]. [kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613 ## Crashes ### 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: > ``` > 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). If you encounter this exception on Android 12, then just upgrade to scrcpy >= 1.18 (see [#2129]): ``` > ERROR: Exception on thread Thread[main,5,main] java.lang.AssertionError: java.lang.reflect.InvocationTargetException at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75) ... Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Method.invoke(Native Method) at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73) ... 7 more Caused by: java.lang.IllegalArgumentException: displayToken must not be null at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067) at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147) ... 9 more ``` [#2129]: https://github.com/Genymobile/scrcpy/issues/2129 ## Command line on Windows Some Windows users are not familiar with the command line. Here is how to open a terminal and run `scrcpy` with arguments: 1. Press Windows+r, this opens a dialog box. 2. Type `cmd` and press Enter, this opens a terminal. 3. Go to your _scrcpy_ directory, by typing (adapt the path): ```bat cd C:\Users\user\Downloads\scrcpy-win64-xxx ``` and press Enter 4. Type your command. For example: ```bat scrcpy --record file.mkv ``` If you plan to always use the same arguments, create a file `myscrcpy.bat` (enable [show file extensions] to avoid confusion) in the `scrcpy` directory, containing your command. For example: ```bat scrcpy --prefer-text --turn-screen-off --stay-awake ``` Then just double-click on that file. You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs` to add some arguments. [show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/ ## Translations This FAQ is available in other languages: - [Italiano (Italiano, `it`) - v1.19](FAQ.it.md) - [한국어 (Korean, `ko`) - v1.11](FAQ.ko.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.18](FAQ.zh-Hans.md) scrcpy-1.21/FAQ.zh-Hans.md000066400000000000000000000157261415124136000152100ustar00rootroot00000000000000只有原版的[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/ scrcpy-1.21/LICENSE000066400000000000000000000261731415124136000137530ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 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-1.21/README.id.md000066400000000000000000000504241415124136000146140ustar00rootroot00000000000000_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/ scrcpy-1.21/README.it.md000066400000000000000000000623641415124136000146420ustar00rootroot00000000000000_Apri il [README](README.md) originale e sempre aggiornato._ # scrcpy (v1.19) Questa applicazione fornisce la visualizzazione e il controllo dei dispositivi Android collegati via USB (o [via TCP/IP][article-tcpip]). Non richiede alcun accesso _root_. Funziona su _GNU/Linux_, _Windows_ e _macOS_. ![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 # orientamento iniziale (corrente) scrcpy --lock-video-orientation=0 # orientamento naturale scrcpy --lock-video-orientation=1 # 90° antiorario scrcpy --lock-video-orientation=2 # 180° scrcpy --lock-video-orientation=3 # 90° orario ``` Questo influisce sull'orientamento della registrazione. 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 _ ``` ### Cattura #### 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 #### v4l2loopback Su Linux è possibile inviare il flusso video ad un dispositivo v4l2 loopback, cosicchè un dispositivo Android possa essere aperto come una webcam da qualsiasi strumento compatibile con v4l2. Il modulo `v4l2loopback` deve essere installato: ```bash sudo apt install v4l2loopback-dkms ``` Per creare un dispositvo v4l2: ```bash sudo modprobe v4l2loopback ``` Questo creerà un nuovo dispositivo video in `/dev/videoN` dove `N` è un intero (più [opzioni](https://github.com/umlaeute/v4l2loopback#options) sono disponibili per crere più dispositivi o dispositivi con ID specifici). Per elencare i dispositvi attivati: ```bash # necessita del pacchetto v4l-utils v4l2-ctl --list-devices # semplice ma potrebbe essere sufficiente ls /dev/video* ``` Per avviare scrcpy utilizzando un v4l2 sink: ```bash scrcpy --v4l2-sink=/dev/videoN scrcpy --v4l2-sink=/dev/videoN --no-display # disabilita la finestra di trasmissione scrcpy --v4l2-sink=/dev/videoN -N # versione corta ``` (sostituisci `N` con l'ID del dispositivo, controlla con `ls /dev/video*`) Una volta abilitato, puoi aprire il tuo flusso video con uno strumento compatibile con v4l2: ```bash ffplay -i /dev/videoN vlc v4l2:///dev/videoN # VLC potrebbe aggiungere del ritardo per il buffer ``` Per esempio potresti catturare il video in [OBS]. [OBS]: https://obsproject.com/ #### Buffering È possibile aggiungere del buffer. Questo aumenta la latenza ma riduce il jitter (vedi [#2464]). [#2464]: https://github.com/Genymobile/scrcpy/issues/2464 L'opzione è disponibile per il buffer della visualizzazione: ```bash scrcpy --display-buffer=50 # aggiungi 50 ms di buffer per la visualizzazione ``` e per il V4L2 sink: ```bash scrcpy --v4l2-buffer=500 # aggiungi 50 ms di buffer per il v4l2 sink ``` ### Connessione #### Wireless _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 ``` #### 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/Download` del dispositivo trascina e rilascia un file (non APK) nella finestra di _scrcpy_. Non c'è alcuna risposta visiva, un log è stampato nella console. La cartella di destinazione può essere cambiata all'avvio: ```bash scrcpy --push-target=/sdcard/Movies/ ``` ### 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 sinistro¹_ | Premi il tasto `HOME` | MOD+h \| _Click centrale_ | Premi il tasto `BACK` | MOD+b \| _Click destro²_ | Premi il tasto `APP_SWITCH` | MOD+s \| _4° click³_ | Premi il tasto `MENU` (sblocca lo schermo) | MOD+m | Premi il tasto `VOLUME_UP` | MOD+ _(su)_ | Premi il tasto `VOLUME_DOWN` | MOD+ _(giù)_ | 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 \| _5° click³_ | Espandi il pannello delle impostazioni | MOD+n+n \| _Doppio 5° click³_ | Chiudi pannelli | MOD+Shift+n | Copia negli appunti⁴ | MOD+c | Taglia negli appunti⁴ | MOD+x | Sincronizza gli appunti e incolla⁴ | MOD+v | Inietta il testo degli appunti del computer | MOD+Shift+v | Abilita/Disabilita il contatore FPS (su stdout) | MOD+i | Pizzica per zoomare | Ctrl+_click e trascina_ _¹Doppio click sui bordi neri per rimuoverli._ _²Il tasto destro accende lo schermo se era spento, preme BACK in caso contrario._ _³4° e 5° pulsante del mouse, se il tuo mouse ne dispone._ _⁴Solo in Android >= 7._ Le scorciatoie con pulsanti ripetuti sono eseguite rilasciando e premendo il pulsante una seconda volta. Per esempio, per eseguire "Espandi il pannello delle impostazioni": 1. Premi e tieni premuto MOD. 2. Poi premi due volte n. 3. Infine rilascia MOD. Tutte le scorciatoie Ctrl+_tasto_ sono inoltrate al dispositivo, così sono gestite dall'applicazione attiva. ## 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/ scrcpy-1.21/README.jp.md000066400000000000000000000716161415124136000146370ustar00rootroot00000000000000_Only the original [README](README.md) is guaranteed to be up-to-date._ # scrcpy (v1.19) このアプリケーションはUSB(もしくは[TCP/IP経由][article-tcpip])で接続されたAndroidデバイスの表示と制御を提供します。このアプリケーションは _root_ でのアクセスを必要としません。このアプリケーションは _GNU/Linux_ 、 _Windows_ そして _macOS_ 上で動作します。 ![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 brew install android-platform-tools ``` `adb`は[MacPorts]からでもインストールできます。 ```bash sudo port install scrcpy ``` [MacPorts]: https://www.macports.org/ また、[アプリケーションをビルド][BUILD]することも可能です。 ## 実行 Androidデバイスを接続し、実行: ```bash scrcpy ``` 次のコマンドでリストされるコマンドライン引数も受け付けます: ```bash scrcpy --help ``` ## 機能 ### キャプチャ構成 #### サイズ削減 Androidデバイスを低解像度でミラーリングする場合、パフォーマンス向上に便利な場合があります。 幅と高さをある値(例:1024)に制限するには: ```bash scrcpy --max-size 1024 scrcpy -m 1024 # 短縮版 ``` 一方のサイズはデバイスのアスペクト比が維持されるように計算されます。この方法では、1920x1080のデバイスでは1024x576にミラーリングされます。 #### ビットレート変更 ビットレートの初期値は8Mbpsです。ビットレートを変更するには(例:2Mbpsに変更): ```bash scrcpy --bit-rate 2M scrcpy -b 2M # 短縮版 ``` #### フレームレート制限 キャプチャするフレームレートを制限できます: ```bash scrcpy --max-fps 15 ``` この機能はAndroid 10からオフィシャルサポートとなっていますが、以前のバージョンでも動作する可能性があります。 #### トリミング デバイスの画面は、画面の一部のみをミラーリングするようにトリミングできます。 これは、例えばOculus Goの片方の目をミラーリングする場合に便利です。: ```bash scrcpy --crop 1224:1440:0:0 # オフセット位置(0,0)で1224x1440 ``` もし`--max-size`も指定されている場合、トリミング後にサイズ変更が適用されます。 #### ビデオの向きをロックする ミラーリングの向きをロックするには: ```bash scrcpy --lock-video-orientation # 現在の向き scrcpy --lock-video-orientation=0 # 自然な向き scrcpy --lock-video-orientation=1 # 90°反時計回り scrcpy --lock-video-orientation=2 # 180° scrcpy --lock-video-orientation=3 # 90°時計回り ``` この設定は録画の向きに影響します。 [ウィンドウは独立して回転することもできます](#回転)。 #### エンコーダ いくつかのデバイスでは一つ以上のエンコーダを持ちます。それらのいくつかは、問題やクラッシュを引き起こします。別のエンコーダを選択することが可能です: ```bash scrcpy --encoder OMX.qcom.video.encoder.avc ``` 利用可能なエンコーダをリストするために、無効なエンコーダ名を渡すことができます。エラー表示で利用可能なエンコーダを提供します。 ```bash scrcpy --encoder _ ``` ### キャプチャ #### 録画 ミラーリング中に画面の録画をすることが可能です: ```bash scrcpy --record file.mp4 scrcpy -r file.mkv ``` 録画中にミラーリングを無効にするには: ```bash scrcpy --no-display --record file.mp4 scrcpy -Nr file.mkv # Ctrl+Cで録画を中断する ``` "スキップされたフレーム"は(パフォーマンス上の理由で)リアルタイムで表示されなくても録画されます。 フレームはデバイス上で _タイムスタンプされる_ ため [パケット遅延のバリエーション] は録画されたファイルに影響を与えません。 [パケット遅延のバリエーション]: https://en.wikipedia.org/wiki/Packet_delay_variation #### v4l2loopback Linuxでは、ビデオストリームをv4l2ループバックデバイスに送信することができます。 v4l2loopbackのデバイスにビデオストリームを送信することで、Androidデバイスをウェブカメラのようにv4l2対応ツールで開くこともできます。 `v4l2loopback` モジュールのインストールが必要です。 ```bash sudo apt install v4l2loopback-dkms ``` v4l2デバイスを作成する。 ```bash sudo modprobe v4l2loopback ``` これにより、新しいビデオデバイスが `/dev/videoN` に作成されます。(`N` は整数) (複数のデバイスや特定のIDのデバイスを作成するために、より多くの[オプション](https://github.com/umlaeute/v4l2loopback#options)が利用可能です。 多くの[オプション]()が利用可能で複数のデバイスや特定のIDのデバイスを作成できます。 有効なデバイスを一覧表示する: ```bash # v4l-utilsパッケージが必要 v4l2-ctl --list-devices # シンプルですが十分これで確認できます ls /dev/video* ``` v4l2シンクを使用してscrcpyを起動する。 ```bash scrcpy --v4l2-sink=/dev/videoN scrcpy --v4l2-sink=/dev/videoN --no-display # ミラーリングウィンドウを無効化する scrcpy --v4l2-sink=/dev/videoN -N # 短縮版 ``` (`N` をデバイス ID に置き換えて、`ls /dev/video*` で確認してください) 有効にすると、v4l2対応のツールでビデオストリームを開けます。 ```bash ffplay -i /dev/videoN vlc v4l2:///dev/videoN # VLCではバッファリングの遅延が発生する場合があります ``` 例えばですが [OBS]の中にこの映像を取り込めことができます。 [OBS]: https://obsproject.com/ #### Buffering バッファリングを追加することも可能です。これによりレイテンシーは増加しますが、ジッターは減少します。(参照 [#2464]) [#2464]: https://github.com/Genymobile/scrcpy/issues/2464 このオプションでディスプレイバッファリングを設定できます。 ```bash scrcpy --display-buffer=50 # ディスプレイに50msのバッファリングを追加する ``` V4L2の場合はこちらのオプションで設定できます。 ```bash scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink ``` ### 接続 #### ワイヤレス _Scrcpy_ はデバイスとの通信に`adb`を使用します。そして`adb`はTCP/IPを介しデバイスに[接続]することができます: 1. あなたのコンピュータと同じWi-Fiに接続します。 2. あなたのIPアドレスを取得します。設定 → 端末情報 → ステータス情報、もしくは、このコマンドを実行します: ```bash adb shell ip route | awk '{print $9}' ``` 3. あなたのデバイスでTCP/IPを介したadbを有効にします: `adb tcpip 5555` 4. あなたのデバイスの接続を外します。 5. あなたのデバイスに接続します: `adb connect DEVICE_IP:5555` _(`DEVICE_IP`は置き換える)_ 6. 通常通り`scrcpy`を実行します。 この方法はビットレートと解像度を減らすのにおそらく有用です: ```bash scrcpy --bit-rate 2M --max-size 800 scrcpy -b2M -m800 # 短縮版 ``` [接続]: https://developer.android.com/studio/command-line/adb.html#wireless #### マルチデバイス もし`adb devices`でいくつかのデバイスがリストされる場合、 _シリアルナンバー_ を指定する必要があります: ```bash scrcpy --serial 0123456789abcdef scrcpy -s 0123456789abcdef # 短縮版 ``` デバイスがTCP/IPを介して接続されている場合: ```bash scrcpy --serial 192.168.0.1:5555 scrcpy -s 192.168.0.1:5555 # 短縮版 ``` 複数のデバイスに対して、複数の _scrcpy_ インスタンスを開始することができます。 #### デバイス接続での自動起動 [AutoAdb]を使用可能です: ```bash autoadb scrcpy -s '{}' ``` [AutoAdb]: https://github.com/rom1v/autoadb #### SSHトンネル リモートデバイスに接続するため、ローカル`adb`クライアントからリモート`adb`サーバーへ接続することが可能です(同じバージョンの _adb_ プロトコルを使用している場合): ```bash adb kill-server # 5037ポートのローカルadbサーバーを終了する ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer # オープンしたままにする ``` 他の端末から: ```bash scrcpy ``` リモートポート転送の有効化を回避するためには、代わりに転送接続を強制することができます(`-R`の代わりに`-L`を使用することに注意): ```bash adb kill-server # 5037ポートのローカルadbサーバーを終了する ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer # オープンしたままにする ``` 他の端末から: ```bash scrcpy --force-adb-forward ``` ワイヤレス接続と同様に、クオリティを下げると便利な場合があります: ``` scrcpy -b2M -m800 --max-fps 15 ``` ### ウィンドウ構成 #### タイトル ウィンドウのタイトルはデバイスモデルが初期値です。これは変更できます: ```bash scrcpy --window-title 'My device' ``` #### 位置とサイズ ウィンドウの位置とサイズの初期値を指定できます: ```bash scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 ``` #### ボーダーレス ウィンドウの装飾を無効化するには: ```bash scrcpy --window-borderless ``` #### 常に画面のトップ scrcpyの画面を常にトップにするには: ```bash scrcpy --always-on-top ``` #### フルスクリーン アプリケーションを直接フルスクリーンで開始できます: ```bash scrcpy --fullscreen scrcpy -f # 短縮版 ``` フルスクリーンは、次のコマンドで動的に切り替えることができます 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 ``` #### タッチを表示 プレゼンテーションの場合(物理デバイス上で)物理的なタッチを表示すると便利な場合があります。 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/Download`ディレクトリにファイルを送るには、(APKではない)ファイルを _scrcpy_ の画面にドラッグ&ドロップします。 見た目のフィードバックはありません。コンソールにログが出力されます。 転送先ディレクトリを起動時に変更することができます: ```bash scrcpy --push-target=/sdcard/Movies/ ``` ### 音声転送 音声は _scrcpy_ では転送されません。[sndcpy]を使用します。 [issue #14]も参照ください。 [sndcpy]: https://github.com/rom1v/sndcpy [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 ## ショートカット 次のリストでは、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 \| _4クリック³_ | `MENU` (画面のアンロック)をクリック | MOD+m | `VOLUME_UP`をクリック | MOD+ _(上)_ | `VOLUME_DOWN`をクリック | MOD+ _(下)_ | `POWER`をクリック | MOD+p | 電源オン | _右クリック²_ | デバイス画面をオフにする(ミラーリングしたまま) | MOD+o | デバイス画面をオンにする | MOD+Shift+o | デバイス画面を回転する | MOD+r | 通知パネルを展開する | MOD+n \| _5ボタンクリック³_ | 設定パネルを展開する | MOD+n+n \| _5ダブルクリック³_ | 通知パネルを折りたたむ | MOD+Shift+n | クリップボードへのコピー³ | MOD+c | クリップボードへのカット³ | MOD+x | クリップボードの同期とペースト³ | MOD+v | コンピュータのクリップボードテキストの挿入 | MOD+Shift+v | FPSカウンタ有効/無効(標準入出力上) | MOD+i | ピンチしてズームする | Ctrl+_クリック&移動_ _¹黒い境界線を削除するため、境界線上でダブルクリック_ _²もしスクリーンがオフの場合、右クリックでスクリーンをオンする。それ以外の場合はBackを押します._ _³4と5はマウスのボタンです、もしあなたのマウスにボタンがあれば使えます._ _⁴Android 7以上のみ._ キーを繰り返すショートカットはキーを離して2回目を押したら実行されます。例えば「設定パネルを展開する」を実行する場合は以下のように操作する。 1. MOD キーを押し、押したままにする. 2. その後に nキーを2回押す. 3. 最後に MODキーを離す. 全ての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/ scrcpy-1.21/README.ko.md000066400000000000000000000405651415124136000146360ustar00rootroot00000000000000_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/ scrcpy-1.21/README.md000066400000000000000000000674111415124136000142250ustar00rootroot00000000000000# scrcpy (v1.20) scrcpy [Read in another language](#translations) This application provides display and control of Android devices connected via USB (or [over TCP/IP](#tcpip-wireless)). It does not require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) It focuses on: - **lightness**: native, displays only the device screen - **performance**: 30~120fps, depending on the device - **quality**: 1920×1080 or above - **low latency**: [35~70ms][lowlatency] - **low startup time**: ~1 second to display the first image - **non-intrusiveness**: nothing is left installed on the device - **user benefits**: no account, no ads, no internet required - **freedom**: free and open source software [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 Its features include: - [recording](#recording) - mirroring with [device screen off](#turn-screen-off) - [copy-paste](#copy-paste) in both directions - [configurable quality](#capture-configuration) - device screen [as a webcam (V4L2)](#v4l2loopback) (Linux-only) - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) (Linux-only) - and more… ## Requirements 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 and Ubuntu: ``` 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.20.zip`][direct-win64] _(SHA-256: 548532b616288bcaeceff6881ad5e6f0928e5ae2b48c380385f03627401cfdba)_ [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-win64-v1.20.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 The capture frame rate can be limited: ```bash scrcpy --max-fps 15 ``` This is officially supported since Android 10, but may work on earlier versions. #### Crop The device screen may be cropped to mirror only part of the screen. 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]. [OBS]: https://obsproject.com/ #### Buffering It is possible to add buffering. This increases latency but reduces jitter (see [#2464]). [#2464]: https://github.com/Genymobile/scrcpy/issues/2464 The option is available for display buffering: ```bash scrcpy --display-buffer=50 # add 50 ms buffering for display ``` and V4L2 sink: ```bash scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink ``` ### Connection #### 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. ##### Automatic An option `--tcpip` allows to configure the connection automatically. There are two variants. 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 ``` 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, enable TCP/IP mode, then connect to the device before starting. ##### Manual Alternatively, it is possible to enable the TCP/IP connection manually using `adb`: 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: ```bash adb shell ip route | awk '{print $9}' ``` 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 #### Tunnels 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 ADB server are unencrypted.** Suppose that this server is accessible at 192.168.1.2. Then, from another terminal, run scrcpy: ```bash export 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 a SSH tunnel. First, make sure the ADB server is running on the remote computer: ```bash adb start-server ``` Then, establish a 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 export 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 export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` Like for wireless connections, it may be useful to reduce quality: ``` scrcpy -b2M -m800 --max-fps 15 ``` ### 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 ``` #### Power off on close To turn the device screen off when closing scrcpy: ```bash scrcpy --power-off-on-close ``` #### 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). To disable automatic clipboard synchronization, use `--no-clipboard-autosync`. #### 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. #### Physical keyboard simulation (HID) By default, scrcpy uses Android key or text injection: it works everywhere, but is limited to ASCII. On Linux, scrcpy can simulate a physical USB keyboard on Android to provide a better input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard is disabled and it works for all characters and IME. [hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support However, it only works if the device is connected by USB, and is currently only supported on Linux. To enable this mode: ```bash scrcpy --hid-keyboard scrcpy -K # short version ``` If it fails for some reason (for example because the device is not connected via USB), it automatically fallbacks to the default mode (with a log in the console). This allows to use the same command line options when connected over USB and TCP/IP. In this mode, raw key events (scancodes) are sent to the device, independently of the host key mapping. Therefore, if your keyboard layout does not match, it must be configured on the Android device, in Settings → System → Languages and input → [Physical keyboard]. This settings page can be started directly: ```bash adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS ``` However, the option is only available when the HID keyboard is enabled (or when a physical keyboard is connected). [Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 #### 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) On the contrary, you could force to always inject raw key events: ```bash scrcpy --raw-key-events ``` These options have no effect on HID keyboard (all key events are sent as scancodes in this mode). [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 #### 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 ``` This option has no effect on HID keyboard (key repeat is handled by Android directly in this mode). #### 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_ | Drag & drop APK file | Install APK from computer | Drag & drop non-APK file | [Push file to device](#push-file-to-device) _¹Double-click on black borders to remove them._ _²Right-click turns the screen on if it was off, presses BACK otherwise._ _³4th and 5th mouse buttons, if your mouse has them._ _⁴Only on Android >= 7._ Shortcuts with repeated keys are executted by releasing and pressing the key a second time. For example, to execute "Expand settings panel": 1. Press and keep pressing MOD. 2. Then double-press n. 3. Finally, release MOD. All Ctrl+_key_ shortcuts are forwarded to the device, so they are handled by the active application. ## Custom paths To use a specific _adb_ binary, configure its path in the environment variable `ADB`: ```bash ADB=/path/to/adb scrcpy ``` To override the path of the `scrcpy-server` file, configure its path in `SCRCPY_SERVER_PATH`. To override the icon, configure its path in `SCRCPY_ICON_PATH`. ## Why _scrcpy_? A colleague challenged me to find a name as unpronounceable as [gnirehtet]. [`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een. [gnirehtet]: https://github.com/Genymobile/gnirehtet [`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html ## How to build? See [BUILD]. ## Common issues See the [FAQ](FAQ.md). ## Developers Read the [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. ## 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.19](README.it.md) - [日本語 (Japanese, `jp`) - v1.19](README.jp.md) - [한국어 (Korean, `ko`) - v1.11](README.ko.md) - [Português Brasileiro (Brazilian Portuguese, `pt-BR`) - v1.19](README.pt-br.md) - [Español (Spanish, `sp`) - v1.17](README.sp.md) - [简体中文 (Simplified Chinese, `zh-Hans`) - v1.20](README.zh-Hans.md) - [繁體中文 (Traditional Chinese, `zh-Hant`) - v1.15](README.zh-Hant.md) - [Turkish (Turkish, `tr`) - v1.18](README.tr.md) Only this README file is guaranteed to be up-to-date. scrcpy-1.21/README.pt-br.md000066400000000000000000000603241415124136000152440ustar00rootroot00000000000000_Apenas o [README](README.md) original é garantido estar atualizado._ # scrcpy (v1.19) Esta aplicação fornece exibição e controle de dispositivos Android conectados via USB (ou [via TCP/IP][article-tcpip]). Não requer nenhum acesso _root_. Funciona em _GNU/Linux_, _Windows_ e _macOS_. ![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 ### Sumário - Linux: `apt install scrcpy` - Windows: [baixar][direct-win64] - macOS: `brew install scrcpy` Compilar pelos arquivos fontes: [BUILD] ([processo simplificado][BUILD_simple]) [BUILD]: BUILD.md [BUILD_simple]: BUILD.md#simple ### Linux No Debian (_testing_ e _sid_ por enquanto) e Ubuntu (20.04): ``` apt install scrcpy ``` Um pacote [Snap] está disponível: [`scrcpy`][snap-link]. [snap-link]: https://snapstats.org/snaps/scrcpy [snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager) Para Fedora, um pacote [COPR] está disponível: [`scrcpy`][copr-link]. [COPR]: https://fedoraproject.org/wiki/Category:Copr [copr-link]: https://copr.fedorainfracloud.org/coprs/zeno/scrcpy/ Para Arch Linux, um pacote [AUR] está disponível: [`scrcpy`][aur-link]. [AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository [aur-link]: https://aur.archlinux.org/packages/scrcpy/ Para Gentoo, uma [Ebuild] está disponível: [`scrcpy/`][ebuild-link]. [Ebuild]: https://wiki.gentoo.org/wiki/Ebuild [ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy Você também pode [compilar o app manualmente][BUILD] ([processo simplificado][BUILD_simple]). ### Windows Para Windows, por simplicidade, um arquivo pré-compilado com todas as dependências (incluindo `adb`) está disponível: - [README](README.md#windows) Também está disponível em [Chocolatey]: [Chocolatey]: https://chocolatey.org/ ```bash choco install scrcpy choco install adb # se você ainda não o tem ``` E no [Scoop]: ```bash scoop install scrcpy scoop install adb # se você ainda não o tem ``` [Scoop]: https://scoop.sh Você também pode [compilar o app manualmente][BUILD]. ### macOS A aplicação está disponível em [Homebrew]. Apenas instale-a: [Homebrew]: https://brew.sh/ ```bash brew install scrcpy ``` Você precisa do `adb`, acessível pelo seu `PATH`. Se você ainda não o tem: ```bash brew install android-platform-tools ``` Está também disponivel em [MacPorts], que prepara o adb para você: ```bash sudo port install scrcpy ``` [MacPorts]: https://www.macports.org/ Você também pode [compilar o app manualmente][BUILD]. ## Executar Conecte um dispositivo Android e execute: ```bash scrcpy ``` Também aceita argumentos de linha de comando, listados por: ```bash scrcpy --help ``` ## Funcionalidades ### Configuração de captura #### Reduzir tamanho Algumas vezes, é útil espelhar um dispositivo Android em uma resolução menor para aumentar a performance. Para limitar ambos (largura e altura) para algum valor (ex: 1024): ```bash scrcpy --max-size 1024 scrcpy -m 1024 # versão curta ``` A outra dimensão é calculada para que a proporção do dispositivo seja preservada. Dessa forma, um dispositivo de 1920x1080 será espelhado em 1024x576. #### Mudar bit-rate O bit-rate padrão é 8 Mbps. Para mudar o bit-rate do vídeo (ex: para 2 Mbps): ```bash scrcpy --bit-rate 2M scrcpy -b 2M # versão curta ``` #### Limitar frame rate O frame rate de captura pode ser limitado: ```bash scrcpy --max-fps 15 ``` Isso é oficialmente suportado desde o Android 10, mas pode funcionar em versões anteriores. #### Cortar A tela do dispositivo pode ser cortada para espelhar apenas uma parte da tela. Isso é útil por exemplo, para espelhar apenas um olho do Oculus Go: ```bash scrcpy --crop 1224:1440:0:0 # 1224x1440 no deslocamento (0,0) ``` Se `--max-size` também for especificado, o redimensionamento é aplicado após o corte. #### Travar orientação do vídeo Para travar a orientação do espelhamento: ```bash scrcpy --lock-video-orientation # orientação inicial (Atual) scrcpy --lock-video-orientation=0 # orientação natural scrcpy --lock-video-orientation=1 # 90° sentido anti-horário scrcpy --lock-video-orientation=2 # 180° scrcpy --lock-video-orientation=3 # 90° sentido horário ``` Isso afeta a orientação de gravação. A [janela também pode ser rotacionada](#rotação) independentemente. #### Encoder Alguns dispositivos têm mais de um encoder, e alguns deles podem causar problemas ou travar. É possível selecionar um encoder diferente: ```bash scrcpy --encoder OMX.qcom.video.encoder.avc ``` Para listar os encoders disponíveis, você pode passar um nome de encoder inválido, o erro dará os encoders disponíveis: ```bash scrcpy --encoder _ ``` ### Captura #### Gravando É possível gravar a tela enquanto ocorre o espelhamento: ```bash scrcpy --record file.mp4 scrcpy -r file.mkv ``` Para desativar o espelhamento durante a gravação: ```bash scrcpy --no-display --record file.mp4 scrcpy -Nr file.mkv # interrompa a gravação com Ctrl+C ``` "Frames pulados" são gravados, mesmo que não sejam exibidos em tempo real (por motivos de performance). Frames têm seu _horário carimbado_ no dispositivo, então [variação de atraso nos pacotes][packet delay variation] não impacta o arquivo gravado. [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation #### v4l2loopback Em Linux, é possível enviar a transmissão do video para um disposiivo v4l2 loopback, assim o dispositivo Android pode ser aberto como uma webcam por qualquer ferramneta capaz de v4l2 The module `v4l2loopback` must be installed: ```bash sudo apt install v4l2loopback-dkms ``` Para criar um dispositivo v4l2: ```bash sudo modprobe v4l2loopback ``` Isso criara um novo dispositivo de vídeo em `/dev/videoN`, onde `N` é uma integer (mais [opções](https://github.com/umlaeute/v4l2loopback#options) estão disponiveis para criar varios dispositivos ou dispositivos com IDs específicas). Para listar os dispositivos disponíveis: ```bash # requer o pacote v4l-utils v4l2-ctl --list-devices # simples, mas pode ser suficiente ls /dev/video* ``` Para iniciar o scrcpy usando o coletor v4l2 (sink): ```bash scrcpy --v4l2-sink=/dev/videoN scrcpy --v4l2-sink=/dev/videoN --no-display # desativa a janela espelhada scrcpy --v4l2-sink=/dev/videoN -N # versão curta ``` (troque `N` pelo ID do dipositivo, verifique com `ls /dev/video*`) Uma vez ativado, você pode abrir suas trasmissões de videos com uma ferramenta capaz de v4l2: ```bash ffplay -i /dev/videoN vlc v4l2:///dev/videoN # VLC pode adicionar um pouco de atraso de buffering ``` Por exemplo, você pode capturar o video dentro do [OBS]. [OBS]: https://obsproject.com/ #### Buffering É possivel adicionar buffering. Isso aumenta a latência, mas reduz a tenção (jitter) (veja [#2464]). [#2464]: https://github.com/Genymobile/scrcpy/issues/2464 A opção éta disponivel para buffering de exibição: ```bash scrcpy --display-buffer=50 # adiciona 50 ms de buffering para a exibição ``` e coletor V4L2: ```bash scrcpy --v4l2-buffer=500 # adiciona 500 ms de buffering para coletor V4L2 ``` , ### Conexão #### Sem fio _Scrcpy_ usa `adb` para se comunicar com o dispositivo, e `adb` pode [conectar-se][connect] a um dispositivo via TCP/IP: 1. Conecte o dispositivo no mesmo Wi-Fi do seu computador. 2. Pegue o endereço IP do seu dispositivo, em Configurações → Sobre o telefone → Status, ou executando este comando: ```bash adb shell ip route | awk '{print $9}' ``` 3. Ative o adb via TCP/IP no seu dispositivo: `adb tcpip 5555`. 4. Desconecte seu dispositivo. 5. Conecte-se ao seu dispositivo: `adb connect DEVICE_IP:5555` _(substitua `DEVICE_IP`)_. 6. Execute `scrcpy` como de costume. Pode ser útil diminuir o bit-rate e a resolução: ```bash scrcpy --bit-rate 2M --max-size 800 scrcpy -b2M -m800 # versão curta ``` [connect]: https://developer.android.com/studio/command-line/adb.html#wireless #### Múltiplos dispositivos Se vários dispositivos são listados em `adb devices`, você deve especificar o _serial_: ```bash scrcpy --serial 0123456789abcdef scrcpy -s 0123456789abcdef # versão curta ``` Se o dispositivo está conectado via TCP/IP: ```bash scrcpy --serial 192.168.0.1:5555 scrcpy -s 192.168.0.1:5555 # versão curta ``` Você pode iniciar várias instâncias do _scrcpy_ para vários dispositivos. #### Iniciar automaticamente quando dispositivo é conectado Você pode usar [AutoAdb]: ```bash autoadb scrcpy -s '{}' ``` [AutoAdb]: https://github.com/rom1v/autoadb #### Túnel SSH Para conectar-se a um dispositivo remoto, é possível conectar um cliente `adb` local a um servidor `adb` remoto (contanto que eles usem a mesma versão do protocolo _adb_): ```bash adb kill-server # encerra o servidor adb local em 5037 ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 your_remote_computer # mantenha isso aberto ``` De outro terminal: ```bash scrcpy ``` Para evitar ativar o encaminhamento de porta remota, você pode forçar uma conexão de encaminhamento (note o `-L` em vez de `-R`): ```bash adb kill-server # encerra o servidor adb local em 5037 ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 your_remote_computer # mantenha isso aberto ``` De outro terminal: ```bash scrcpy --force-adb-forward ``` Igual a conexões sem fio, pode ser útil reduzir a qualidade: ``` scrcpy -b2M -m800 --max-fps 15 ``` ### Configuração de janela #### Título Por padrão, o título da janela é o modelo do dispositivo. Isso pode ser mudado: ```bash scrcpy --window-title 'Meu dispositivo' ``` #### Posição e tamanho A posição e tamanho iniciais da janela podem ser especificados: ```bash scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600 ``` #### Sem bordas Para desativar decorações de janela: ```bash scrcpy --window-borderless ``` #### Sempre no topo Para manter a janela do scrcpy sempre no topo: ```bash scrcpy --always-on-top ``` #### Tela cheia A aplicação pode ser iniciada diretamente em tela cheia: ```bash scrcpy --fullscreen scrcpy -f # versão curta ``` Tela cheia pode ser alternada dinamicamente com 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 ``` #### 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/Download/` no dispositivo, arraste e solte um arquivo (não-APK) para a janela do _scrcpy_. Não existe feedback visual, um log é imprimido no console. O diretório alvo pode ser mudado ao iniciar: ```bash scrcpy --push-target /sdcard/foo/bar/ ``` ### Encaminhamento de áudio Áudio não é encaminhado pelo _scrcpy_. Use [sndcpy]. Também veja [issue #14]. [sndcpy]: https://github.com/rom1v/sndcpy [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 ## Atalhos Na lista a seguir, 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-perfeito) | MOD+g | Redimensionar janela para remover bordas pretas | MOD+w \| _Clique-duplo-esquerdo¹_ | Clicar em `HOME` | MOD+h \| _Clique-do-meio_ | Clicar em `BACK` | MOD+b \| _Clique-direito²_ | Clicar em `APP_SWITCH` | MOD+s \| _Clique-do-4.°³_ | Clicar em `MENU` (desbloquear tela) | MOD+m | Clicar em `VOLUME_UP` | MOD+ _(cima)_ | Clicar em `VOLUME_DOWN` | MOD+ _(baixo)_ | Clicar em `POWER` | MOD+p | 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 \| _Clique-do-5.°³_ | Expandir painel de configurção | MOD+n+n \| _Clique-duplo-do-5.°³_ | Colapsar paineis | MOD+Shift+n | Copiar para área de transferência⁴ | MOD+c | Recortar para área de transferência⁴ | MOD+x | Sincronizar áreas de transferência e colar⁴ | MOD+v | Injetar texto da área de transferência do computador | MOD+Shift+v | Ativar/desativar contador de FPS (em stdout) | MOD+i | Pinçar para dar zoom | Ctrl+_Clicar-e-mover_ _¹Clique-duplo-esquerdo na borda preta para remove-la._ _²Clique-direito liga a tela caso esteja desligada, pressione BACK caso contrário._ _³4.° and 5.° botões do mouse, caso o mouse possua._ _⁴Apenas em Android >= 7._ Atalhos com teclas reptidas são executados soltando e precionando a tecla uma segunda vez. Por exemplo, para executar "Expandir painel de Configurção": 1. Mantenha pressionado MOD. 2. Depois click duas vezes n. 3. Finalmente, solte MOD. Todos os atalhos Ctrl+_tecla_ são encaminhados para o dispositivo, para que eles sejam tratados pela aplicação ativa. ## Caminhos personalizados Para usar um binário _adb_ específico, configure seu caminho na variável de ambiente `ADB`: ```bash ADB=/caminho/para/adb scrcpy ``` Para sobrepor o caminho do arquivo `scrcpy-server`, configure seu caminho em `SCRCPY_SERVER_PATH`. [useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345 ## Por quê _scrcpy_? Um colega me desafiou a encontrar um nome tão impronunciável quanto [gnirehtet]. [`strcpy`] copia uma **str**ing; `scrcpy` copia uma **scr**een. [gnirehtet]: https://github.com/Genymobile/gnirehtet [`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html ## Como compilar? Veja [BUILD]. ## Problemas comuns Veja o [FAQ](FAQ.md). ## Desenvolvedores Leia a [página dos desenvolvedores][developers page]. [developers page]: DEVELOP.md ## Licença Copyright (C) 2018 Genymobile Copyright (C) 2018-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/ scrcpy-1.21/README.sp.md000066400000000000000000000545031415124136000146440ustar00rootroot00000000000000Solo 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/ scrcpy-1.21/README.tr.md000066400000000000000000000603261415124136000146470ustar00rootroot00000000000000# 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/ scrcpy-1.21/README.zh-Hans.md000066400000000000000000000600011415124136000155200ustar00rootroot00000000000000_Only the original [README](README.md) is guaranteed to be up-to-date._ 只有原版的[README](README.md)会保持最新。 Current version is based on [65b023a] 本文根据[65b023a]进行翻译。 [65b023a]: https://github.com/Genymobile/scrcpy/blob/65b023ac6d586593193fd5290f65e25603b68e02/README.md # scrcpy (v1.20) scrcpy 本应用程序可以显示并控制通过 USB (或 [TCP/IP][article-tcpip]) 连接的安卓设备,且不需要任何 _root_ 权限。本程序支持 _GNU/Linux_, _Windows_ 和 _macOS_。 ![screenshot](assets/screenshot-debian-600.jpg) 本应用专注于: - **轻量**: 原生,仅显示设备屏幕 - **性能**: 30~120fps,取决于设备 - **质量**: 分辨率可达 1920×1080 或更高 - **低延迟**: [35~70ms][lowlatency] - **快速启动**: 最快 1 秒内即可显示第一帧 - **无侵入性**: 不会在设备上遗留任何程序 - **用户利益**: 无需帐号,无广告,无需联网 - **自由**: 自由和开源软件 [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 功能: - [屏幕录制](#屏幕录制) - 镜像时[关闭设备屏幕](#关闭设备屏幕) - 双向[复制粘贴](#复制粘贴) - [可配置显示质量](#采集设置) - 以设备屏幕[作为摄像头(V4L2)](#v4l2loopback) (仅限 Linux) - [模拟物理键盘 (HID)](#物理键盘模拟-hid) (仅限 Linux) - 更多 …… ## 系统要求 安卓设备最低需要支持 API 21 (Android 5.0)。 确保设备已[开启 adb 调试][enable-adb]。 [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling 在某些设备上,还需要开启[额外的选项][control]以使用鼠标和键盘进行控制。 [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 ## 获取本程序 Packaging status ### 概要 - Linux: `apt install scrcpy` - Windows: [下载][direct-win64] - macOS: `brew install scrcpy` 从源代码编译: [构建][BUILD] ([简化过程][BUILD_simple]) [BUILD]: BUILD.md [BUILD_simple]: BUILD.md#simple ### Linux 在 Debian (目前仅支持 _testing_ 和 _sid_ 分支) 和Ubuntu (20.04) 上: ``` 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] ([简化过程][BUILD_simple])。 ### Windows 在 Windows 上,为简便起见,我们提供包含了所有依赖 (包括 `adb`) 的预编译包。 - [README](README.md#windows) 也可以使用 [Chocolatey]: [Chocolatey]: https://chocolatey.org/ ```bash choco install scrcpy choco install adb # 如果还没有 adb ``` 或者 [Scoop]: ```bash scoop install scrcpy scoop install adb # 如果还没有 adb ``` [Scoop]: https://scoop.sh 您也可以[自行构建][BUILD]。 ### macOS 本程序已发布到 [Homebrew]。直接安装即可: [Homebrew]: https://brew.sh/ ```bash brew install scrcpy ``` 你还需要在 `PATH` 内有 `adb`。如果还没有: ```bash brew install android-platform-tools ``` 或者通过 [MacPorts],该方法同时设置好 adb: ```bash sudo port install scrcpy ``` [MacPorts]: https://www.macports.org/ 您也可以[自行构建][BUILD]。 ## 运行 连接安卓设备,然后执行: ```bash scrcpy ``` 本程序支持命令行参数,查看参数列表: ```bash scrcpy --help ``` ## 功能介绍 ### 采集设置 #### 降低分辨率 有时候,可以通过降低镜像的分辨率来提高性能。 要同时限制宽度和高度到某个值 (例如 1024): ```bash scrcpy --max-size 1024 scrcpy -m 1024 # 简写 ``` 另一边会被按比例缩小以保持设备的显示比例。这样,1920×1080 分辨率的设备会以 1024×576 的分辨率进行镜像。 #### 修改码率 默认码率是 8 Mbps。改变视频码率 (例如改为 2 Mbps): ```bash scrcpy --bit-rate 2M scrcpy -b 2M # 简写 ``` #### 限制帧率 要限制采集的帧率: ```bash scrcpy --max-fps 15 ``` 本功能从 Android 10 开始才被官方支持,但在一些旧版本中也能生效。 #### 画面裁剪 可以对设备屏幕进行裁剪,只镜像屏幕的一部分。 例如可以只镜像 Oculus Go 的一只眼睛。 ```bash scrcpy --crop 1224:1440:0:0 # 以 (0,0) 为原点的 1224x1440 像素 ``` 如果同时指定了 `--max-size`,会先进行裁剪,再进行缩放。 #### 锁定屏幕方向 要锁定镜像画面的方向: ```bash scrcpy --lock-video-orientation # 初始(目前)方向 scrcpy --lock-video-orientation=0 # 自然方向 scrcpy --lock-video-orientation=1 # 逆时针旋转 90° scrcpy --lock-video-orientation=2 # 180° scrcpy --lock-video-orientation=3 # 顺时针旋转 90° ``` 只影响录制的方向。 [窗口可以独立旋转](#旋转)。 #### 编码器 一些设备内置了多种编码器,但是有的编码器会导致问题或崩溃。可以手动选择其它编码器: ```bash scrcpy --encoder OMX.qcom.video.encoder.avc ``` 要列出可用的编码器,可以指定一个不存在的编码器名称,错误信息中会包含所有的编码器: ```bash scrcpy --encoder _ ``` ### 采集 #### 屏幕录制 可以在镜像的同时录制视频: ```bash scrcpy --record file.mp4 scrcpy -r file.mkv ``` 仅录制,不显示镜像: ```bash scrcpy --no-display --record file.mp4 scrcpy -Nr file.mkv # 按 Ctrl+C 停止录制 ``` 录制时会包含“被跳过的帧”,即使它们由于性能原因没有实时显示。设备会为每一帧打上 _时间戳_ ,所以 [包时延抖动][packet delay variation] 不会影响录制的文件。 [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation #### v4l2loopback 在 Linux 上,可以将视频流发送至 v4l2 回环 (loopback) 设备,因此可以使用任何 v4l2 工具像摄像头一样打开安卓设备。 需安装 `v4l2loopback` 模块: ```bash sudo apt install v4l2loopback-dkms ``` 创建一个 v4l2 设备: ```bash sudo modprobe v4l2loopback ``` 这样会在 `/dev/videoN` 创建一个新的视频设备,其中 `N` 是整数。 ([更多选项](https://github.com/umlaeute/v4l2loopback#options) 可以用来创建多个设备或者特定 ID 的设备)。 列出已启用的设备: ```bash # 需要 v4l-utils 包 v4l2-ctl --list-devices # 简单但或许足够 ls /dev/video* ``` 使用一个 v4l2 漏开启 scrcpy: ```bash scrcpy --v4l2-sink=/dev/videoN scrcpy --v4l2-sink=/dev/videoN --no-display # 禁用窗口镜像 scrcpy --v4l2-sink=/dev/videoN -N # 简写 ``` (将 `N` 替换为设备 ID,使用 `ls /dev/video*` 命令查看) 启用之后,可以使用 v4l2 工具打开视频流: ```bash ffplay -i /dev/videoN vlc v4l2:///dev/videoN # VLC 可能存在一些缓冲延迟 ``` 例如,可以在 [OBS] 中采集视频。 [OBS]: https://obsproject.com/ #### 缓冲 可以加入缓冲,会增加延迟,但可以减少抖动 (见 [#2464])。 [#2464]: https://github.com/Genymobile/scrcpy/issues/2464 对于显示缓冲: ```bash scrcpy --display-buffer=50 # 为显示增加 50 毫秒的缓冲 ``` 对于 V4L2 漏: ```bash scrcpy --v4l2-buffer=500 # 为 v4l2 漏增加 500 毫秒的缓冲 ``` ### 连接 #### 无线 _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 ``` scrcpy 关闭时会恢复设备原来的设置。 #### 关闭设备屏幕 可以通过以下的命令行参数在关闭设备屏幕的状态下进行镜像: ```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 --power-off-on-close ``` #### 显示触摸 在演示时,可能会需要显示 (在物理设备上的) 物理触摸点。 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_ 会在关于屏幕中心对称的位置上用“虚拟手指”发出触摸事件。 #### 物理键盘模拟 (HID) 默认情况下,scrcpy 使用安卓按键或文本注入,这在任何情况都可以使用,但仅限于ASCII字符。 在 Linux 上,scrcpy 可以模拟为 Android 上的物理 USB 键盘,以提供更好地输入体验 (使用 [USB HID over AOAv2][hid-aoav2]):禁用虚拟键盘,并适用于任何字符和输入法。 [hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support 不过,这种方法仅支持 USB 连接以及 Linux平台。 启用 HID 模式: ```bash scrcpy --hid-keyboard scrcpy -K # 简写 ``` 如果失败了 (如设备未通过 USB 连接),则自动回退至默认模式 (终端中会输出日志)。这即允许通过 USB 和 TCP/IP 连接时使用相同的命令行参数。 在这种模式下,原始按键事件 (扫描码) 被发送给设备,而与宿主机按键映射无关。因此,若键盘布局不匹配,需要在 Android 设备上进行配置,具体为 设置 → 系统 → 语言和输入法 → [实体键盘]。 [Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915 #### 文本注入偏好 打字的时候,系统会产生两种[事件][textevents]: - _按键事件_ ,代表一个按键被按下或松开。 - _文本事件_ ,代表一个字符被输入。 程序默认使用按键事件来输入字母。只有这样,键盘才会在游戏中正常运作 (例如 WASD 键)。 但这也有可能[造成一些问题][prefertext]。如果您遇到了问题,可以通过以下方式避免: ```bash scrcpy --prefer-text ``` (这会导致键盘在游戏中工作不正常) 该选项不影响 HID 键盘 (该模式下,所有按键都发送为扫描码)。 [textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input [prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343 #### 按键重复 默认状态下,按住一个按键不放会生成多个重复按键事件。在某些游戏中这通常没有实际用途,且可能会导致性能问题。 避免转发重复按键事件: ```bash scrcpy --no-key-repeat ``` 该选项不影响 HID 键盘 (该模式下,按键重复由 Android 直接管理)。 #### 右键和中键 默认状态下,右键会触发返回键 (或电源键开启),中键会触发 HOME 键。要禁用这些快捷键并把所有点击转发到设备: ```bash scrcpy --forward-all-clicks ``` ### 文件拖放 #### 安装APK 将 APK 文件 (文件名以 `.apk` 结尾) 拖放到 _scrcpy_ 窗口来安装。 不会有视觉反馈,终端会输出一条日志。 #### 将文件推送至设备 要推送文件到设备的 `/sdcard/Download/`,将 (非 APK) 文件拖放至 _scrcpy_ 窗口。 不会有视觉反馈,终端会输出一条日志。 在启动时可以修改目标目录: ```bash scrcpy --push-target=/sdcard/Movies/ ``` ### 音频转发 _Scrcpy_ 不支持音频。请使用 [sndcpy]。 另见 [issue #14]。 [sndcpy]: https://github.com/rom1v/sndcpy [issue #14]: https://github.com/Genymobile/scrcpy/issues/14 ## 快捷键 在以下列表中, 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 \| _第4键³_ | 点按 `菜单` (解锁屏幕) | MOD+m | 点按 `音量+` | MOD+ _(上箭头)_ | 点按 `音量-` | MOD+ _(下箭头)_ | 点按 `电源` | MOD+p | 打开屏幕 | _鼠标右键²_ | 关闭设备屏幕 (但继续在电脑上显示) | MOD+o | 打开设备屏幕 | MOD+Shift+o | 旋转设备屏幕 | MOD+r | 展开通知面板 | MOD+n \| _第5键³_ | 展开设置面板 | MOD+n+n \| _双击第5键³_ | 收起通知面板 | MOD+Shift+n | 复制到剪贴板⁴ | MOD+c | 剪切到剪贴板⁴ | MOD+x | 同步剪贴板并粘贴⁴ | MOD+v | 注入电脑剪贴板文本 | MOD+Shift+v | 打开/关闭FPS显示 (至标准输出) | MOD+i | 捏拉缩放 | Ctrl+_按住并移动鼠标_ | 拖放 APK 文件 | 从电脑安装 APK 文件 | 拖放非 APK 文件 | [将文件推送至设备](#push-file-to-device) _¹双击黑边可以去除黑边。_ _²点击鼠标右键将在屏幕熄灭时点亮屏幕,其余情况则视为按下返回键 。_ _³鼠标的第4键和第5键。_ _⁴需要安卓版本 Android >= 7。_ 有重复按键的快捷键通过松开再按下一个按键来进行,如“展开设置面板”: 1. 按下 MOD 不放。 2. 双击 n。 3. 松开 MOD。 所有的 Ctrl+_按键_ 的快捷键都会被转发到设备,所以会由当前应用程序进行处理。 ## 自定义路径 要使用指定的 _adb_ 二进制文件,可以设置环境变量 `ADB`: ```bash ADB=/path/to/adb scrcpy ``` 要覆盖 `scrcpy-server` 的路径,可以设置 `SCRCPY_SERVER_PATH`。 要覆盖图标,可以设置其路径至 `SCRCPY_ICON_PATH`。 ## 为什么叫 _scrcpy_ ? 一个同事让我找出一个和 [gnirehtet] 一样难以发音的名字。 [`strcpy`] 复制一个 **str**ing (字符串); `scrcpy` 复制一个 **scr**een (屏幕)。 [gnirehtet]: https://github.com/Genymobile/gnirehtet [`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html ## 如何构建? 请查看 [BUILD]。 ## 常见问题 请查看 [FAQ](FAQ.md)。 ## 开发者 请查看[开发者页面]。 [开发者页面]: DEVELOP.md ## 许可协议 Copyright (C) 2018 Genymobile Copyright (C) 2018-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/ scrcpy-1.21/README.zh-Hant.md000066400000000000000000000461741415124136000155400ustar00rootroot00000000000000_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/ scrcpy-1.21/app/000077500000000000000000000000001415124136000135155ustar00rootroot00000000000000scrcpy-1.21/app/meson.build000066400000000000000000000162011415124136000156570ustar00rootroot00000000000000src = [ 'src/main.c', 'src/adb.c', 'src/adb_parser.c', 'src/adb_tunnel.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', 'src/device_msg.c', 'src/icon.c', 'src/file_handler.c', 'src/fps_counter.c', 'src/frame_buffer.c', 'src/input_manager.c', 'src/keyboard_inject.c', 'src/mouse_inject.c', 'src/opengl.c', 'src/options.c', 'src/receiver.c', 'src/recorder.c', 'src/scrcpy.c', 'src/screen.c', 'src/server.c', 'src/stream.c', 'src/video_buffer.c', 'src/util/acksync.c', 'src/util/file.c', 'src/util/intmap.c', 'src/util/intr.c', 'src/util/log.c', 'src/util/net.c', 'src/util/net_intr.c', 'src/util/process.c', 'src/util/process_intr.c', 'src/util/strbuf.c', 'src/util/str.c', 'src/util/term.c', 'src/util/thread.c', 'src/util/tick.c', ] 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/file.c', 'src/sys/win/process.c', ] conf.set('_WIN32_WINNT', '0x0600') conf.set('WINVER', '0x0600') else 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' if v4l2_support src += [ 'src/v4l2_sink.c' ] endif aoa_hid_support = host_machine.system() == 'linux' if aoa_hid_support src += [ 'src/aoa_hid.c', 'src/hid_keyboard.c', ] endif cc = meson.get_compiler('c') if not get_option('crossbuild_windows') # native build dependencies = [ dependency('libavformat'), dependency('libavcodec'), dependency('libavutil'), dependency('sdl2'), ] if v4l2_support dependencies += dependency('libavdevice') endif if aoa_hid_support dependencies += dependency('libusb-1.0') endif else # cross-compile mingw32 build (from Linux to Windows) prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' 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') ] endif if host_machine.system() == 'windows' dependencies += cc.find_library('ws2_32') endif check_functions = [ 'strdup', 'asprintf', 'vasprintf', ] foreach f : check_functions if cc.has_function(f) define = 'HAVE_' + f.underscorify().to_upper() conf.set(define, true) 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()) # the prefix used during configuration (meson --prefix=PREFIX) conf.set_quoted('PREFIX', get_option('prefix')) # build a "portable" version (with scrcpy-server accessible from the same # directory as the executable) conf.set('PORTABLE', get_option('portable')) # the default client TCP port range for the "adb reverse" tunnel # overridden by option --port 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')) # select the debugger method ('old' for Android < 9, 'new' for Android >= 9) conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) # enable HID over AOA support (linux only) conf.set('HAVE_AOA_HID', aoa_hid_support) configure_file(configuration: conf, output: 'config.h') src_dir = include_directories('src') executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true, c_args: []) install_man('scrcpy.1') install_data('../data/icon.png', rename: 'scrcpy.png', install_dir: 'share/icons/hicolor/256x256/apps') ### TESTS # do not build tests in release (assertions would not be executed at all) if get_option('buildtype') == 'debug' tests = [ ['test_adb_parser', [ 'tests/test_adb_parser.c', 'src/adb_parser.c', 'src/util/str.c', 'src/util/strbuf.c', ]], ['test_buffer_util', [ 'tests/test_buffer_util.c', ]], ['test_cbuf', [ 'tests/test_cbuf.c', ]], ['test_cli', [ 'tests/test_cli.c', 'src/cli.c', 'src/options.c', 'src/util/net.c', 'src/util/str.c', 'src/util/strbuf.c', 'src/util/term.c', ]], ['test_clock', [ 'tests/test_clock.c', 'src/clock.c', ]], ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', 'src/util/str.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_strbuf', [ 'tests/test_strbuf.c', 'src/util/strbuf.c', ]], ['test_str', [ 'tests/test_str.c', 'src/util/str.c', 'src/util/strbuf.c', ]], ] foreach t : tests exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies, c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST']) test(t[0], exe) endforeach endif scrcpy-1.21/app/scrcpy.1000066400000000000000000000254121415124136000151060ustar00rootroot00000000000000.TH "scrcpy" "1" .SH NAME scrcpy \- Display and control your Android device .SH SYNOPSIS .B scrcpy .RI [ options ] .SH DESCRIPTION .B scrcpy provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access. .SH OPTIONS .TP .B \-\-always\-on\-top 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). Default is 8000000. .TP .BI "\-\-codec\-options " key[:type]=value[,...] Set a list of comma-separated key:type=value options for the device encoder. The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. The list of possible codec options is available in the Android documentation .UR https://d.android.com/reference/android/media/MediaFormat .UE . .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any .B \-\-max\-size value is computed on the cropped size. .TP .BI "\-\-disable-screensaver" Disable screensaver while scrcpy is running. .TP .BI "\-\-display " id Specify the display id to mirror. The list of possible display ids can be listed by "adb shell dumpsys display" (search "mDisplayId=" in the output). Default is 0. .TP .BI "\-\-display\-buffer ms Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. Default is 0 (no buffering). .TP .BI "\-\-encoder " name Use a specific MediaCodec encoder (must be a H.264 encoder). .TP .B \-\-force\-adb\-forward Do not attempt to use "adb reverse" to connect to the device. .TP .B \-\-forward\-all\-clicks By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead. .TP .B \-f, \-\-fullscreen Start in fullscreen. .TP .B \-h, \-\-help Print this help. .TP .B \-K, \-\-hid\-keyboard Simulate a physical keyboard by using HID over AOAv2. This provides a better experience for IME users, and allows to generate non-ASCII characters, contrary to the default injection method. It may only work over USB, and is currently only supported on Linux. 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: adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS However, the option is only available when the HID keyboard is enabled (or a physical keyboard is connected). .TP .B \-\-legacy\-paste Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v). This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. .TP .BI "\-\-lock\-video\-orientation[=value] Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. Default is "unlocked". Passing the option without argument is equivalent to passing "initial". .TP .BI "\-\-max\-fps " value Limit the framerate of screen capture (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. Default is 0 (unlimited). .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 \-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). .TP .B \-\-no\-key\-repeat Do not forward repeated key events when a key is held down. .TP .B \-\-no\-mipmaps If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps. .TP .BI "\-p, \-\-port " port[:port] Set the TCP port (range) used by the client to listen. Default is 27183:27199. .TP .B \-\-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. This avoids issues when combining multiple keys to enter special characters, but breaks the expected behavior of alpha keys in games (typically WASD). .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". Default is "/sdcard/Download/". .TP .B \-\-raw\-key\-events Inject key events for all input keys, and ignore text events. .TP .BI "\-r, \-\-record " file Record screen to .IR file . The format is determined by the .B \-\-record\-format option if set, or by the file extension (.mp4 or .mkv). .TP .BI "\-\-record\-format " format Force recording format (either mp4 or mkv). .TP .BI "\-\-render\-driver " name Request SDL to use the given render driver (this is just a hint). Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". .UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER .UE .TP .BI "\-\-rotation " value Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. .TP .BI "\-\-shortcut\-mod " key[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','. For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper". Default is "lalt,lsuper" (left-Alt or left-Super). .TP .BI "\-\-tcpip[=ip[:port]] Configure and reconnect the device over TCP/IP. If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). 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. .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 "\-\-tunnel\-host " ip Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. 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 --force-adb-forward. Default is 0 (not forced): the local port used for establishing the tunnel will be used. .TP .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). .TP .BI "\-\-v4l2-buffer " ms Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. Default is 0 (no buffering). .TP .BI "\-V, \-\-verbosity " value Set the log level ("verbose", "debug", "info", "warn" or "error"). Default is "info" for release builds, "debug" for debug builds. .TP .B \-v, \-\-version Print the version of scrcpy. .TP .B \-w, \-\-stay-awake Keep the device on while scrcpy is running, when the device is plugged in. .TP .B \-\-window\-borderless Disable window decorations (display borderless window). .TP .BI "\-\-window\-title " text Set a custom window title. .TP .BI "\-\-window\-x " value Set the initial window horizontal position. Default is "auto". .TP .BI "\-\-window\-y " value Set the initial window vertical position. Default is "auto". .TP .BI "\-\-window\-width " value Set the initial window width. Default is 0 (automatic). .TP .BI "\-\-window\-height " value Set the initial window height. Default is 0 (automatic). .SH SHORTCUTS In the following list, MOD is the shortcut modifier. By default, it's (left) Alt or (left) Super, but it can be configured by \fB\-\-shortcut\-mod\fR (see above). .TP .B MOD+f Switch fullscreen mode .TP .B MOD+Left Rotate display left .TP .B MOD+Right Rotate display right .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) .TP .B MOD+w, Double\-click on black borders Resize window to remove black borders .TP .B MOD+h, Home, Middle\-click Click on HOME .TP .B MOD+b, MOD+Backspace, Right\-click (when screen is on) Click on BACK .TP .B MOD+s Click on APP_SWITCH .TP .B MOD+m Click on MENU .TP .B MOD+Up Click on VOLUME_UP .TP .B MOD+Down Click on VOLUME_DOWN .TP .B MOD+p Click on POWER (turn screen on/off) .TP .B Right\-click (when screen is off) Turn screen on .TP .B MOD+o Turn device screen off (keep mirroring) .TP .B MOD+Shift+o Turn device screen on .TP .B MOD+r Rotate device screen .TP .B MOD+n Expand notification panel .TP .B MOD+Shift+n Collapse notification panel .TP .B Mod+c Copy to clipboard (inject COPY keycode, Android >= 7 only) .TP .B Mod+x Cut to clipboard (inject CUT keycode, Android >= 7 only) .TP .B MOD+v Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only) .TP .B MOD+Shift+v Inject computer clipboard text as a sequence of key events .TP .B MOD+i Enable/disable FPS counter (print frames/second in logs) .TP .B Ctrl+click-and-move Pinch-to-zoom from the center of the screen .TP .B Drag & drop APK file Install APK from computer .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. .TP .B SCRCPY_SERVER_PATH Specify the path to server binary. .SH AUTHORS .B scrcpy is written by Romain Vimont. This manual page was written by .MT mmyangfl@gmail.com Yangfl .ME for the Debian Project (and may be used by others). .SH "REPORTING BUGS" Report bugs to .UR https://github.com/Genymobile/scrcpy/issues .UE . .SH COPYRIGHT Copyright \(co 2018 Genymobile .UR https://www.genymobile.com Genymobile .UE Copyright \(co 2018\-2020 .MT rom@rom1v.com Romain Vimont .ME Licensed under the Apache License, Version 2.0. .SH WWW .UR https://github.com/Genymobile/scrcpy .UE scrcpy-1.21/app/src/000077500000000000000000000000001415124136000143045ustar00rootroot00000000000000scrcpy-1.21/app/src/adb.c000066400000000000000000000325101415124136000151770ustar00rootroot00000000000000#include "adb.h" #include #include #include #include #include "adb_parser.h" #include "util/file.h" #include "util/log.h" #include "util/process_intr.h" #include "util/str.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 (sc_file_executable_exists(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 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 (!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); sc_intr_set_process(intr, SC_PROCESS_NONE); // Close separately sc_process_close(pid); return ret; } static const char ** adb_create_argv(const char *serial, const char *const adb_cmd[], size_t len) { const char **argv = malloc((len + 4) * sizeof(*argv)); if (!argv) { LOG_OOM(); return NULL; } argv[0] = get_adb_command(); int i; 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; return argv; } static sc_pid adb_execute_p(const char *serial, const char *const adb_cmd[], size_t len, unsigned flags, sc_pipe *pout) { const char **argv = adb_create_argv(serial, adb_cmd, len); if (!argv) { return SC_PROCESS_NONE; } 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; } free(argv); return pid; } sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len, unsigned flags) { return adb_execute_p(serial, adb_cmd, len, flags, NULL); } bool 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 sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"forward", local, remote}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb forward", flags); } bool adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); const char *const adb_cmd[] = {"forward", "--remove", local}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb forward --remove", flags); } bool 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 sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", remote, local}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb reverse", flags); } bool adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name, unsigned flags) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); const char *const adb_cmd[] = {"reverse", "--remove", remote}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb reverse --remove", flags); } bool 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 const char *const adb_cmd[] = {"push", local, remote}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ free((void *) remote); free((void *) local); #endif return process_check_success_intr(intr, pid, "adb push", flags); } bool 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 const char *const adb_cmd[] = {"install", "-r", local}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); #ifdef __WINDOWS__ free((void *) local); #endif return process_check_success_intr(intr, pid, "adb install", flags); } bool adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags) { char port_string[5 + 1]; sprintf(port_string, "%" PRIu16, port); const char *const adb_cmd[] = {"tcpip", port_string}; sc_pid pid = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd), flags); return process_check_success_intr(intr, pid, "adb tcpip", flags); } bool adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const adb_cmd[] = {"connect", ip_port}; sc_pipe pout; sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), 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)); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb connect", flags); if (!ok) { return false; } if (r == -1) { return false; } 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. sc_str_truncate(buf, r, "\r\n"); fprintf(stderr, "%s\n", buf); } return ok; } bool adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const adb_cmd[] = {"disconnect", ip_port}; size_t len = ip_port ? ARRAY_LEN(adb_cmd) : ARRAY_LEN(adb_cmd) - 1; sc_pid pid = adb_execute(NULL, adb_cmd, len, flags); return process_check_success_intr(intr, pid, "adb disconnect", flags); } char * adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags) { const char *const adb_cmd[] = {"shell", "getprop", prop}; sc_pipe pout; sc_pid pid = adb_execute_p(serial, adb_cmd, ARRAY_LEN(adb_cmd), 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)); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb getprop", flags); if (!ok) { return NULL; } if (r == -1) { return NULL; } sc_str_truncate(buf, r, " \r\n"); return strdup(buf); } char * adb_get_serialno(struct sc_intr *intr, unsigned flags) { const char *const adb_cmd[] = {"get-serialno"}; sc_pipe pout; sc_pid pid = adb_execute_p(NULL, adb_cmd, ARRAY_LEN(adb_cmd), flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb get-serialno\""); return NULL; } char buf[128]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf)); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb get-serialno", flags); if (!ok) { return NULL; } if (r == -1) { return false; } sc_str_truncate(buf, r, " \r\n"); return strdup(buf); } char * adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { const char *const cmd[] = {"shell", "ip", "route"}; sc_pipe pout; sc_pid pid = adb_execute_p(serial, cmd, ARRAY_LEN(cmd), 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)); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "ip route", flags); if (!ok) { return NULL; } if (r == -1) { return false; } assert((size_t) r <= sizeof(buf)); if (r == sizeof(buf) && buf[sizeof(buf) - 1] != '\0') { // 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.\n"); return NULL; } return sc_adb_parse_device_ip_from_output(buf, r); } scrcpy-1.21/app/src/adb.h000066400000000000000000000043701415124136000152070ustar00rootroot00000000000000#ifndef SC_ADB_H #define SC_ADB_H #include "common.h" #include #include #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) sc_pid adb_execute(const char *serial, const char *const adb_cmd[], size_t len, unsigned flags); bool adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags); bool adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port, unsigned flags); bool adb_reverse(struct sc_intr *intr, const char *serial, const char *device_socket_name, uint16_t local_port, unsigned flags); bool adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name, unsigned flags); bool adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote, unsigned flags); bool adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); /** * Execute `adb tcpip ` */ bool adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags); /** * Execute `adb connect ` * * `ip_port` may not be NULL. */ bool 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 adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); /** * Execute `adb getprop ` */ char * adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags); /** * Execute `adb get-serialno` * * Return the result, to be freed by the caller, or NULL on error. */ char * adb_get_serialno(struct sc_intr *intr, 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 * adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); #endif scrcpy-1.21/app/src/adb_parser.c000066400000000000000000000036231415124136000165560ustar00rootroot00000000000000#include "adb_parser.h" #include #include #include "util/log.h" #include "util/str.h" static char * sc_adb_parse_device_ip_from_line(char *line, size_t len) { // One line from "ip route" looks lile: // "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]; sc_str_truncate(dev_name, len - idx_dev_name + 1, " \t"); char *ip = &line[idx_ip]; sc_str_truncate(ip, len - idx_ip + 1, " \t"); // 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_from_output(char *buf, size_t buf_len) { size_t idx_line = 0; while (idx_line < buf_len && buf[idx_line] != '\0') { char *line = &buf[idx_line]; size_t len = sc_str_truncate(line, buf_len - idx_line, "\n"); // The same, but without any trailing '\r' size_t line_len = sc_str_remove_trailing_cr(line, len); char *ip = sc_adb_parse_device_ip_from_line(line, line_len); if (ip) { // Found return ip; } // The next line starts after the '\n' (replaced by `\0`) idx_line += len + 1; } return NULL; } scrcpy-1.21/app/src/adb_parser.h000066400000000000000000000003511415124136000165560ustar00rootroot00000000000000#ifndef SC_ADB_PARSER_H #define SC_ADB_PARSER_H #include "common.h" #include "stddef.h" /** * Parse the ip from the output of `adb shell ip route` */ char * sc_adb_parse_device_ip_from_output(char *buf, size_t buf_len); #endif scrcpy-1.21/app/src/adb_tunnel.c000066400000000000000000000124741415124136000165730ustar00rootroot00000000000000#include "adb_tunnel.h" #include #include "adb.h" #include "util/log.h" #include "util/net_intr.h" #include "util/process_intr.h" #define SC_SOCKET_NAME "scrcpy" static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1); } static bool enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, const char *serial, struct sc_port_range port_range) { uint16_t port = port_range.first; for (;;) { if (!adb_reverse(intr, serial, SC_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 (!adb_reverse_remove(intr, serial, SC_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, struct sc_port_range port_range) { tunnel->forward = true; uint16_t port = port_range.first; for (;;) { if (adb_forward(intr, serial, port, SC_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, struct sc_port_range port_range, bool force_adb_forward) { assert(!tunnel->enabled); if (!force_adb_forward) { // Attempt to use "adb reverse" if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) { return true; } // if "adb reverse" does not work (e.g. over "adb connect"), it // fallbacks to "adb forward", so the app socket is the client LOGW("'adb reverse' failed, fallback to 'adb forward'"); } return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range); } bool sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, const char *serial) { assert(tunnel->enabled); bool ret; if (tunnel->forward) { ret = adb_forward_remove(intr, serial, tunnel->local_port, SC_ADB_NO_STDOUT); } else { ret = adb_reverse_remove(intr, serial, SC_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; } scrcpy-1.21/app/src/adb_tunnel.h000066400000000000000000000021351415124136000165710ustar00rootroot00000000000000#ifndef SC_ADB_TUNNEL_H #define SC_ADB_TUNNEL_H #include "common.h" #include #include #include "options.h" #include "util/intr.h" #include "util/net.h" struct sc_adb_tunnel { bool enabled; bool forward; // use "adb forward" instead of "adb reverse" sc_socket server_socket; // only used if !forward uint16_t local_port; }; /** * Initialize the adb tunnel struct to default values */ void sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel); /** * Open a tunnel * * Blocking calls may be interrupted asynchronously via `intr`. * * If `force_adb_forward` is not set, then attempts to set up an "adb reverse" * tunnel first. Only if it fails (typical on old Android version connected via * TCP/IP), use "adb forward". */ bool sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, const char *serial, struct sc_port_range port_range, bool force_adb_forward); /** * Close the tunnel */ bool sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr, const char *serial); #endif scrcpy-1.21/app/src/android/000077500000000000000000000000001415124136000157245ustar00rootroot00000000000000scrcpy-1.21/app/src/android/input.h000066400000000000000000000765711415124136000172540ustar00rootroot00000000000000// copied from // blob 08299899b6305a0fe74d7d2b8471b7cd0af49dc7 // (and modified) /* * Copyright (C) 2010 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. */ #ifndef _ANDROID_INPUT_H #define _ANDROID_INPUT_H /** * Meta key / modifier state. */ enum android_metastate { /** No meta keys are pressed. */ AMETA_NONE = 0, /** This mask is used to check whether one of the ALT meta keys is pressed. */ AMETA_ALT_ON = 0x02, /** This mask is used to check whether the left ALT meta key is pressed. */ AMETA_ALT_LEFT_ON = 0x10, /** This mask is used to check whether the right ALT meta key is pressed. */ AMETA_ALT_RIGHT_ON = 0x20, /** This mask is used to check whether one of the SHIFT meta keys is pressed. */ AMETA_SHIFT_ON = 0x01, /** This mask is used to check whether the left SHIFT meta key is pressed. */ AMETA_SHIFT_LEFT_ON = 0x40, /** This mask is used to check whether the right SHIFT meta key is pressed. */ AMETA_SHIFT_RIGHT_ON = 0x80, /** This mask is used to check whether the SYM meta key is pressed. */ AMETA_SYM_ON = 0x04, /** This mask is used to check whether the FUNCTION meta key is pressed. */ AMETA_FUNCTION_ON = 0x08, /** This mask is used to check whether one of the CTRL meta keys is pressed. */ AMETA_CTRL_ON = 0x1000, /** This mask is used to check whether the left CTRL meta key is pressed. */ AMETA_CTRL_LEFT_ON = 0x2000, /** This mask is used to check whether the right CTRL meta key is pressed. */ AMETA_CTRL_RIGHT_ON = 0x4000, /** This mask is used to check whether one of the META meta keys is pressed. */ AMETA_META_ON = 0x10000, /** This mask is used to check whether the left META meta key is pressed. */ AMETA_META_LEFT_ON = 0x20000, /** This mask is used to check whether the right META meta key is pressed. */ AMETA_META_RIGHT_ON = 0x40000, /** This mask is used to check whether the CAPS LOCK meta key is on. */ AMETA_CAPS_LOCK_ON = 0x100000, /** This mask is used to check whether the NUM LOCK meta key is on. */ AMETA_NUM_LOCK_ON = 0x200000, /** This mask is used to check whether the SCROLL LOCK meta key is on. */ AMETA_SCROLL_LOCK_ON = 0x400000, }; /** * Input event types. */ enum android_input_event_type { /** Indicates that the input event is a key event. */ AINPUT_EVENT_TYPE_KEY = 1, /** Indicates that the input event is a motion event. */ AINPUT_EVENT_TYPE_MOTION = 2 }; /** * Key event actions. */ enum android_keyevent_action { /** The key has been pressed down. */ AKEY_EVENT_ACTION_DOWN = 0, /** The key has been released. */ AKEY_EVENT_ACTION_UP = 1, /** * Multiple duplicate key events have occurred in a row, or a * complex string is being delivered. The repeat_count property * of the key event contains the number of times the given key * code should be executed. */ AKEY_EVENT_ACTION_MULTIPLE = 2 }; /** * Key event flags. */ enum android_keyevent_flags { /** This mask is set if the device woke because of this key event. */ AKEY_EVENT_FLAG_WOKE_HERE = 0x1, /** This mask is set if the key event was generated by a software keyboard. */ AKEY_EVENT_FLAG_SOFT_KEYBOARD = 0x2, /** This mask is set if we don't want the key event to cause us to leave touch mode. */ AKEY_EVENT_FLAG_KEEP_TOUCH_MODE = 0x4, /** * This mask is set if an event was known to come from a trusted * part of the system. That is, the event is known to come from * the user, and could not have been spoofed by a third party * component. */ AKEY_EVENT_FLAG_FROM_SYSTEM = 0x8, /** * This mask is used for compatibility, to identify enter keys that are * coming from an IME whose enter key has been auto-labelled "next" or * "done". This allows TextView to dispatch these as normal enter keys * for old applications, but still do the appropriate action when * receiving them. */ AKEY_EVENT_FLAG_EDITOR_ACTION = 0x10, /** * When associated with up key events, this indicates that the key press * has been canceled. Typically this is used with virtual touch screen * keys, where the user can slide from the virtual key area on to the * display: in that case, the application will receive a canceled up * event and should not perform the action normally associated with the * key. Note that for this to work, the application can not perform an * action for a key until it receives an up or the long press timeout has * expired. */ AKEY_EVENT_FLAG_CANCELED = 0x20, /** * This key event was generated by a virtual (on-screen) hard key area. * Typically this is an area of the touchscreen, outside of the regular * display, dedicated to "hardware" buttons. */ AKEY_EVENT_FLAG_VIRTUAL_HARD_KEY = 0x40, /** * This flag is set for the first key repeat that occurs after the * long press timeout. */ AKEY_EVENT_FLAG_LONG_PRESS = 0x80, /** * Set when a key event has AKEY_EVENT_FLAG_CANCELED set because a long * press action was executed while it was down. */ AKEY_EVENT_FLAG_CANCELED_LONG_PRESS = 0x100, /** * Set for AKEY_EVENT_ACTION_UP when this event's key code is still being * tracked from its initial down. That is, somebody requested that tracking * started on the key down and a long press has not caused * the tracking to be canceled. */ AKEY_EVENT_FLAG_TRACKING = 0x200, /** * Set when a key event has been synthesized to implement default behavior * for an event that the application did not handle. * Fallback key events are generated by unhandled trackball motions * (to emulate a directional keypad) and by certain unhandled key presses * that are declared in the key map (such as special function numeric keypad * keys when numlock is off). */ AKEY_EVENT_FLAG_FALLBACK = 0x400, }; /** * Bit shift for the action bits holding the pointer index as * defined by AMOTION_EVENT_ACTION_POINTER_INDEX_MASK. */ #define AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT 8 /** Motion event actions */ enum android_motionevent_action { /** Bit mask of the parts of the action code that are the action itself. */ AMOTION_EVENT_ACTION_MASK = 0xff, /** * Bits in the action code that represent a pointer index, used with * AMOTION_EVENT_ACTION_POINTER_DOWN and AMOTION_EVENT_ACTION_POINTER_UP. Shifting * down by AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT provides the actual pointer * index where the data for the pointer going up or down can be found. */ AMOTION_EVENT_ACTION_POINTER_INDEX_MASK = 0xff00, /** A pressed gesture has started, the motion contains the initial starting location. */ AMOTION_EVENT_ACTION_DOWN = 0, /** * A pressed gesture has finished, the motion contains the final release location * as well as any intermediate points since the last down or move event. */ AMOTION_EVENT_ACTION_UP = 1, /** * A change has happened during a press gesture (between AMOTION_EVENT_ACTION_DOWN and * AMOTION_EVENT_ACTION_UP). The motion contains the most recent point, as well as * any intermediate points since the last down or move event. */ AMOTION_EVENT_ACTION_MOVE = 2, /** * The current gesture has been aborted. * You will not receive any more points in it. You should treat this as * an up event, but not perform any action that you normally would. */ AMOTION_EVENT_ACTION_CANCEL = 3, /** * A movement has happened outside of the normal bounds of the UI element. * This does not provide a full gesture, but only the initial location of the movement/touch. */ AMOTION_EVENT_ACTION_OUTSIDE = 4, /** * A non-primary pointer has gone down. * The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed. */ AMOTION_EVENT_ACTION_POINTER_DOWN = 5, /** * A non-primary pointer has gone up. * The bits in AMOTION_EVENT_ACTION_POINTER_INDEX_MASK indicate which pointer changed. */ AMOTION_EVENT_ACTION_POINTER_UP = 6, /** * A change happened but the pointer is not down (unlike AMOTION_EVENT_ACTION_MOVE). * The motion contains the most recent point, as well as any intermediate points since * the last hover move event. */ AMOTION_EVENT_ACTION_HOVER_MOVE = 7, /** * The motion event contains relative vertical and/or horizontal scroll offsets. * Use getAxisValue to retrieve the information from AMOTION_EVENT_AXIS_VSCROLL * and AMOTION_EVENT_AXIS_HSCROLL. * The pointer may or may not be down when this event is dispatched. * This action is always delivered to the winder under the pointer, which * may not be the window currently touched. */ AMOTION_EVENT_ACTION_SCROLL = 8, /** The pointer is not down but has entered the boundaries of a window or view. */ AMOTION_EVENT_ACTION_HOVER_ENTER = 9, /** The pointer is not down but has exited the boundaries of a window or view. */ AMOTION_EVENT_ACTION_HOVER_EXIT = 10, /* One or more buttons have been pressed. */ AMOTION_EVENT_ACTION_BUTTON_PRESS = 11, /* One or more buttons have been released. */ AMOTION_EVENT_ACTION_BUTTON_RELEASE = 12, }; /** * Motion event flags. */ enum android_motionevent_flags { /** * This flag indicates that the window that received this motion event is partly * or wholly obscured by another visible window above it. This flag is set to true * even if the event did not directly pass through the obscured area. * A security sensitive application can check this flag to identify situations in which * a malicious application may have covered up part of its content for the purpose * of misleading the user or hijacking touches. An appropriate response might be * to drop the suspect touches or to take additional precautions to confirm the user's * actual intent. */ AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED = 0x1, }; /** * Motion event edge touch flags. */ enum android_motionevent_edge_touch_flags { /** No edges intersected. */ AMOTION_EVENT_EDGE_FLAG_NONE = 0, /** Flag indicating the motion event intersected the top edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_TOP = 0x01, /** Flag indicating the motion event intersected the bottom edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_BOTTOM = 0x02, /** Flag indicating the motion event intersected the left edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_LEFT = 0x04, /** Flag indicating the motion event intersected the right edge of the screen. */ AMOTION_EVENT_EDGE_FLAG_RIGHT = 0x08 }; /** * Constants that identify each individual axis of a motion event. * @anchor AMOTION_EVENT_AXIS */ enum android_motionevent_axis { /** * Axis constant: X axis of a motion event. * * - For a touch screen, reports the absolute X screen position of the center of * the touch contact area. The units are display pixels. * - For a touch pad, reports the absolute X surface position of the center of the touch * contact area. The units are device-dependent. * - For a mouse, reports the absolute X screen position of the mouse pointer. * The units are display pixels. * - For a trackball, reports the relative horizontal displacement of the trackball. * The value is normalized to a range from -1.0 (left) to 1.0 (right). * - For a joystick, reports the absolute X position of the joystick. * The value is normalized to a range from -1.0 (left) to 1.0 (right). */ AMOTION_EVENT_AXIS_X = 0, /** * Axis constant: Y axis of a motion event. * * - For a touch screen, reports the absolute Y screen position of the center of * the touch contact area. The units are display pixels. * - For a touch pad, reports the absolute Y surface position of the center of the touch * contact area. The units are device-dependent. * - For a mouse, reports the absolute Y screen position of the mouse pointer. * The units are display pixels. * - For a trackball, reports the relative vertical displacement of the trackball. * The value is normalized to a range from -1.0 (up) to 1.0 (down). * - For a joystick, reports the absolute Y position of the joystick. * The value is normalized to a range from -1.0 (up or far) to 1.0 (down or near). */ AMOTION_EVENT_AXIS_Y = 1, /** * Axis constant: Pressure axis of a motion event. * * - For a touch screen or touch pad, reports the approximate pressure applied to the surface * by a finger or other tool. The value is normalized to a range from * 0 (no pressure at all) to 1 (normal pressure), although values higher than 1 * may be generated depending on the calibration of the input device. * - For a trackball, the value is set to 1 if the trackball button is pressed * or 0 otherwise. * - For a mouse, the value is set to 1 if the primary mouse button is pressed * or 0 otherwise. */ AMOTION_EVENT_AXIS_PRESSURE = 2, /** * Axis constant: Size axis of a motion event. * * - For a touch screen or touch pad, reports the approximate size of the contact area in * relation to the maximum detectable size for the device. The value is normalized * to a range from 0 (smallest detectable size) to 1 (largest detectable size), * although it is not a linear scale. This value is of limited use. * To obtain calibrated size information, see * {@link AMOTION_EVENT_AXIS_TOUCH_MAJOR} or {@link AMOTION_EVENT_AXIS_TOOL_MAJOR}. */ AMOTION_EVENT_AXIS_SIZE = 3, /** * Axis constant: TouchMajor axis of a motion event. * * - For a touch screen, reports the length of the major axis of an ellipse that * represents the touch area at the point of contact. * The units are display pixels. * - For a touch pad, reports the length of the major axis of an ellipse that * represents the touch area at the point of contact. * The units are device-dependent. */ AMOTION_EVENT_AXIS_TOUCH_MAJOR = 4, /** * Axis constant: TouchMinor axis of a motion event. * * - For a touch screen, reports the length of the minor axis of an ellipse that * represents the touch area at the point of contact. * The units are display pixels. * - For a touch pad, reports the length of the minor axis of an ellipse that * represents the touch area at the point of contact. * The units are device-dependent. * * When the touch is circular, the major and minor axis lengths will be equal to one another. */ AMOTION_EVENT_AXIS_TOUCH_MINOR = 5, /** * Axis constant: ToolMajor axis of a motion event. * * - For a touch screen, reports the length of the major axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * - For a touch pad, reports the length of the major axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * The units are device-dependent. * * When the touch is circular, the major and minor axis lengths will be equal to one another. * * The tool size may be larger than the touch size since the tool may not be fully * in contact with the touch sensor. */ AMOTION_EVENT_AXIS_TOOL_MAJOR = 6, /** * Axis constant: ToolMinor axis of a motion event. * * - For a touch screen, reports the length of the minor axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * - For a touch pad, reports the length of the minor axis of an ellipse that * represents the size of the approaching finger or tool used to make contact. * The units are device-dependent. * * When the touch is circular, the major and minor axis lengths will be equal to one another. * * The tool size may be larger than the touch size since the tool may not be fully * in contact with the touch sensor. */ AMOTION_EVENT_AXIS_TOOL_MINOR = 7, /** * Axis constant: Orientation axis of a motion event. * * - For a touch screen or touch pad, reports the orientation of the finger * or tool in radians relative to the vertical plane of the device. * An angle of 0 radians indicates that the major axis of contact is oriented * upwards, is perfectly circular or is of unknown orientation. A positive angle * indicates that the major axis of contact is oriented to the right. A negative angle * indicates that the major axis of contact is oriented to the left. * The full range is from -PI/2 radians (finger pointing fully left) to PI/2 radians * (finger pointing fully right). * - For a stylus, the orientation indicates the direction in which the stylus * is pointing in relation to the vertical axis of the current orientation of the screen. * The range is from -PI radians to PI radians, where 0 is pointing up, * -PI/2 radians is pointing left, -PI or PI radians is pointing down, and PI/2 radians * is pointing right. See also {@link AMOTION_EVENT_AXIS_TILT}. */ AMOTION_EVENT_AXIS_ORIENTATION = 8, /** * Axis constant: Vertical Scroll axis of a motion event. * * - For a mouse, reports the relative movement of the vertical scroll wheel. * The value is normalized to a range from -1.0 (down) to 1.0 (up). * * This axis should be used to scroll views vertically. */ AMOTION_EVENT_AXIS_VSCROLL = 9, /** * Axis constant: Horizontal Scroll axis of a motion event. * * - For a mouse, reports the relative movement of the horizontal scroll wheel. * The value is normalized to a range from -1.0 (left) to 1.0 (right). * * This axis should be used to scroll views horizontally. */ AMOTION_EVENT_AXIS_HSCROLL = 10, /** * Axis constant: Z axis of a motion event. * * - For a joystick, reports the absolute Z position of the joystick. * The value is normalized to a range from -1.0 (high) to 1.0 (low). * On game pads with two analog joysticks, this axis is often reinterpreted * to report the absolute X position of the second joystick instead. */ AMOTION_EVENT_AXIS_Z = 11, /** * Axis constant: X Rotation axis of a motion event. * * - For a joystick, reports the absolute rotation angle about the X axis. * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). */ AMOTION_EVENT_AXIS_RX = 12, /** * Axis constant: Y Rotation axis of a motion event. * * - For a joystick, reports the absolute rotation angle about the Y axis. * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). */ AMOTION_EVENT_AXIS_RY = 13, /** * Axis constant: Z Rotation axis of a motion event. * * - For a joystick, reports the absolute rotation angle about the Z axis. * The value is normalized to a range from -1.0 (counter-clockwise) to 1.0 (clockwise). * On game pads with two analog joysticks, this axis is often reinterpreted * to report the absolute Y position of the second joystick instead. */ AMOTION_EVENT_AXIS_RZ = 14, /** * Axis constant: Hat X axis of a motion event. * * - For a joystick, reports the absolute X position of the directional hat control. * The value is normalized to a range from -1.0 (left) to 1.0 (right). */ AMOTION_EVENT_AXIS_HAT_X = 15, /** * Axis constant: Hat Y axis of a motion event. * * - For a joystick, reports the absolute Y position of the directional hat control. * The value is normalized to a range from -1.0 (up) to 1.0 (down). */ AMOTION_EVENT_AXIS_HAT_Y = 16, /** * Axis constant: Left Trigger axis of a motion event. * * - For a joystick, reports the absolute position of the left trigger control. * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed). */ AMOTION_EVENT_AXIS_LTRIGGER = 17, /** * Axis constant: Right Trigger axis of a motion event. * * - For a joystick, reports the absolute position of the right trigger control. * The value is normalized to a range from 0.0 (released) to 1.0 (fully pressed). */ AMOTION_EVENT_AXIS_RTRIGGER = 18, /** * Axis constant: Throttle axis of a motion event. * * - For a joystick, reports the absolute position of the throttle control. * The value is normalized to a range from 0.0 (fully open) to 1.0 (fully closed). */ AMOTION_EVENT_AXIS_THROTTLE = 19, /** * Axis constant: Rudder axis of a motion event. * * - For a joystick, reports the absolute position of the rudder control. * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right). */ AMOTION_EVENT_AXIS_RUDDER = 20, /** * Axis constant: Wheel axis of a motion event. * * - For a joystick, reports the absolute position of the steering wheel control. * The value is normalized to a range from -1.0 (turn left) to 1.0 (turn right). */ AMOTION_EVENT_AXIS_WHEEL = 21, /** * Axis constant: Gas axis of a motion event. * * - For a joystick, reports the absolute position of the gas (accelerator) control. * The value is normalized to a range from 0.0 (no acceleration) * to 1.0 (maximum acceleration). */ AMOTION_EVENT_AXIS_GAS = 22, /** * Axis constant: Brake axis of a motion event. * * - For a joystick, reports the absolute position of the brake control. * The value is normalized to a range from 0.0 (no braking) to 1.0 (maximum braking). */ AMOTION_EVENT_AXIS_BRAKE = 23, /** * Axis constant: Distance axis of a motion event. * * - For a stylus, reports the distance of the stylus from the screen. * A value of 0.0 indicates direct contact and larger values indicate increasing * distance from the surface. */ AMOTION_EVENT_AXIS_DISTANCE = 24, /** * Axis constant: Tilt axis of a motion event. * * - For a stylus, reports the tilt angle of the stylus in radians where * 0 radians indicates that the stylus is being held perpendicular to the * surface, and PI/2 radians indicates that the stylus is being held flat * against the surface. */ AMOTION_EVENT_AXIS_TILT = 25, /** * Axis constant: Generic scroll axis of a motion event. * * - This is used for scroll axis motion events that can't be classified as strictly * vertical or horizontal. The movement of a rotating scroller is an example of this. */ AMOTION_EVENT_AXIS_SCROLL = 26, /** * Axis constant: The movement of x position of a motion event. * * - For a mouse, reports a difference of x position between the previous position. * This is useful when pointer is captured, in that case the mouse pointer doesn't * change the location but this axis reports the difference which allows the app * to see how the mouse is moved. */ AMOTION_EVENT_AXIS_RELATIVE_X = 27, /** * Axis constant: The movement of y position of a motion event. * * Same as {@link RELATIVE_X}, but for y position. */ AMOTION_EVENT_AXIS_RELATIVE_Y = 28, /** * Axis constant: Generic 1 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_1 = 32, /** * Axis constant: Generic 2 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_2 = 33, /** * Axis constant: Generic 3 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_3 = 34, /** * Axis constant: Generic 4 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_4 = 35, /** * Axis constant: Generic 5 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_5 = 36, /** * Axis constant: Generic 6 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_6 = 37, /** * Axis constant: Generic 7 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_7 = 38, /** * Axis constant: Generic 8 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_8 = 39, /** * Axis constant: Generic 9 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_9 = 40, /** * Axis constant: Generic 10 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_10 = 41, /** * Axis constant: Generic 11 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_11 = 42, /** * Axis constant: Generic 12 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_12 = 43, /** * Axis constant: Generic 13 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_13 = 44, /** * Axis constant: Generic 14 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_14 = 45, /** * Axis constant: Generic 15 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_15 = 46, /** * Axis constant: Generic 16 axis of a motion event. * The interpretation of a generic axis is device-specific. */ AMOTION_EVENT_AXIS_GENERIC_16 = 47, // NOTE: If you add a new axis here you must also add it to several other files. // Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list. }; /** * Constants that identify buttons that are associated with motion events. * Refer to the documentation on the MotionEvent class for descriptions of each button. */ enum android_motionevent_buttons { /** primary */ AMOTION_EVENT_BUTTON_PRIMARY = 1 << 0, /** secondary */ AMOTION_EVENT_BUTTON_SECONDARY = 1 << 1, /** tertiary */ AMOTION_EVENT_BUTTON_TERTIARY = 1 << 2, /** back */ AMOTION_EVENT_BUTTON_BACK = 1 << 3, /** forward */ AMOTION_EVENT_BUTTON_FORWARD = 1 << 4, AMOTION_EVENT_BUTTON_STYLUS_PRIMARY = 1 << 5, AMOTION_EVENT_BUTTON_STYLUS_SECONDARY = 1 << 6, }; /** * Constants that identify tool types. * Refer to the documentation on the MotionEvent class for descriptions of each tool type. */ enum android_motionevent_tool_type { /** unknown */ AMOTION_EVENT_TOOL_TYPE_UNKNOWN = 0, /** finger */ AMOTION_EVENT_TOOL_TYPE_FINGER = 1, /** stylus */ AMOTION_EVENT_TOOL_TYPE_STYLUS = 2, /** mouse */ AMOTION_EVENT_TOOL_TYPE_MOUSE = 3, /** eraser */ AMOTION_EVENT_TOOL_TYPE_ERASER = 4, }; /** * Input source masks. * * Refer to the documentation on android.view.InputDevice for more details about input sources * and their correct interpretation. */ enum android_input_source_class { /** mask */ AINPUT_SOURCE_CLASS_MASK = 0x000000ff, /** none */ AINPUT_SOURCE_CLASS_NONE = 0x00000000, /** button */ AINPUT_SOURCE_CLASS_BUTTON = 0x00000001, /** pointer */ AINPUT_SOURCE_CLASS_POINTER = 0x00000002, /** navigation */ AINPUT_SOURCE_CLASS_NAVIGATION = 0x00000004, /** position */ AINPUT_SOURCE_CLASS_POSITION = 0x00000008, /** joystick */ AINPUT_SOURCE_CLASS_JOYSTICK = 0x00000010, }; /** * Input sources. */ enum android_input_source { /** unknown */ AINPUT_SOURCE_UNKNOWN = 0x00000000, /** keyboard */ AINPUT_SOURCE_KEYBOARD = 0x00000100 | AINPUT_SOURCE_CLASS_BUTTON, /** dpad */ AINPUT_SOURCE_DPAD = 0x00000200 | AINPUT_SOURCE_CLASS_BUTTON, /** gamepad */ AINPUT_SOURCE_GAMEPAD = 0x00000400 | AINPUT_SOURCE_CLASS_BUTTON, /** touchscreen */ AINPUT_SOURCE_TOUCHSCREEN = 0x00001000 | AINPUT_SOURCE_CLASS_POINTER, /** mouse */ AINPUT_SOURCE_MOUSE = 0x00002000 | AINPUT_SOURCE_CLASS_POINTER, /** stylus */ AINPUT_SOURCE_STYLUS = 0x00004000 | AINPUT_SOURCE_CLASS_POINTER, /** bluetooth stylus */ AINPUT_SOURCE_BLUETOOTH_STYLUS = 0x00008000 | AINPUT_SOURCE_STYLUS, /** trackball */ AINPUT_SOURCE_TRACKBALL = 0x00010000 | AINPUT_SOURCE_CLASS_NAVIGATION, /** mouse relative */ AINPUT_SOURCE_MOUSE_RELATIVE = 0x00020000 | AINPUT_SOURCE_CLASS_NAVIGATION, /** touchpad */ AINPUT_SOURCE_TOUCHPAD = 0x00100000 | AINPUT_SOURCE_CLASS_POSITION, /** navigation */ AINPUT_SOURCE_TOUCH_NAVIGATION = 0x00200000 | AINPUT_SOURCE_CLASS_NONE, /** joystick */ AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK, /** rotary encoder */ AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE, }; /** * Keyboard types. * * Refer to the documentation on android.view.InputDevice for more details. */ enum android_keyboard_type { /** none */ AINPUT_KEYBOARD_TYPE_NONE = 0, /** non alphabetic */ AINPUT_KEYBOARD_TYPE_NON_ALPHABETIC = 1, /** alphabetic */ AINPUT_KEYBOARD_TYPE_ALPHABETIC = 2, }; /** * Constants used to retrieve information about the range of motion for a particular * coordinate of a motion event. * * Refer to the documentation on android.view.InputDevice for more details about input sources * and their correct interpretation. * * @deprecated These constants are deprecated. Use {@link AMOTION_EVENT_AXIS AMOTION_EVENT_AXIS_*} constants instead. */ enum android_motion_range { /** x */ AINPUT_MOTION_RANGE_X = AMOTION_EVENT_AXIS_X, /** y */ AINPUT_MOTION_RANGE_Y = AMOTION_EVENT_AXIS_Y, /** pressure */ AINPUT_MOTION_RANGE_PRESSURE = AMOTION_EVENT_AXIS_PRESSURE, /** size */ AINPUT_MOTION_RANGE_SIZE = AMOTION_EVENT_AXIS_SIZE, /** touch major */ AINPUT_MOTION_RANGE_TOUCH_MAJOR = AMOTION_EVENT_AXIS_TOUCH_MAJOR, /** touch minor */ AINPUT_MOTION_RANGE_TOUCH_MINOR = AMOTION_EVENT_AXIS_TOUCH_MINOR, /** tool major */ AINPUT_MOTION_RANGE_TOOL_MAJOR = AMOTION_EVENT_AXIS_TOOL_MAJOR, /** tool minor */ AINPUT_MOTION_RANGE_TOOL_MINOR = AMOTION_EVENT_AXIS_TOOL_MINOR, /** orientation */ AINPUT_MOTION_RANGE_ORIENTATION = AMOTION_EVENT_AXIS_ORIENTATION, }; #endif // _ANDROID_INPUT_H scrcpy-1.21/app/src/android/keycodes.h000066400000000000000000000672601415124136000177160ustar00rootroot00000000000000// copied from // blob 2164d6163e1646c22825e364cad4f3c47638effd // (and modified) /* * Copyright (C) 2010 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. */ #ifndef _ANDROID_KEYCODES_H #define _ANDROID_KEYCODES_H /** * Key codes. */ enum android_keycode { /** Unknown key code. */ AKEYCODE_UNKNOWN = 0, /** Soft Left key. * Usually situated below the display on phones and used as a multi-function * feature key for selecting a software defined function shown on the bottom left * of the display. */ AKEYCODE_SOFT_LEFT = 1, /** Soft Right key. * Usually situated below the display on phones and used as a multi-function * feature key for selecting a software defined function shown on the bottom right * of the display. */ AKEYCODE_SOFT_RIGHT = 2, /** Home key. * This key is handled by the framework and is never delivered to applications. */ AKEYCODE_HOME = 3, /** Back key. */ AKEYCODE_BACK = 4, /** Call key. */ AKEYCODE_CALL = 5, /** End Call key. */ AKEYCODE_ENDCALL = 6, /** '0' key. */ AKEYCODE_0 = 7, /** '1' key. */ AKEYCODE_1 = 8, /** '2' key. */ AKEYCODE_2 = 9, /** '3' key. */ AKEYCODE_3 = 10, /** '4' key. */ AKEYCODE_4 = 11, /** '5' key. */ AKEYCODE_5 = 12, /** '6' key. */ AKEYCODE_6 = 13, /** '7' key. */ AKEYCODE_7 = 14, /** '8' key. */ AKEYCODE_8 = 15, /** '9' key. */ AKEYCODE_9 = 16, /** '*' key. */ AKEYCODE_STAR = 17, /** '#' key. */ AKEYCODE_POUND = 18, /** Directional Pad Up key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_UP = 19, /** Directional Pad Down key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_DOWN = 20, /** Directional Pad Left key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_LEFT = 21, /** Directional Pad Right key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_RIGHT = 22, /** Directional Pad Center key. * May also be synthesized from trackball motions. */ AKEYCODE_DPAD_CENTER = 23, /** Volume Up key. * Adjusts the speaker volume up. */ AKEYCODE_VOLUME_UP = 24, /** Volume Down key. * Adjusts the speaker volume down. */ AKEYCODE_VOLUME_DOWN = 25, /** Power key. */ AKEYCODE_POWER = 26, /** Camera key. * Used to launch a camera application or take pictures. */ AKEYCODE_CAMERA = 27, /** Clear key. */ AKEYCODE_CLEAR = 28, /** 'A' key. */ AKEYCODE_A = 29, /** 'B' key. */ AKEYCODE_B = 30, /** 'C' key. */ AKEYCODE_C = 31, /** 'D' key. */ AKEYCODE_D = 32, /** 'E' key. */ AKEYCODE_E = 33, /** 'F' key. */ AKEYCODE_F = 34, /** 'G' key. */ AKEYCODE_G = 35, /** 'H' key. */ AKEYCODE_H = 36, /** 'I' key. */ AKEYCODE_I = 37, /** 'J' key. */ AKEYCODE_J = 38, /** 'K' key. */ AKEYCODE_K = 39, /** 'L' key. */ AKEYCODE_L = 40, /** 'M' key. */ AKEYCODE_M = 41, /** 'N' key. */ AKEYCODE_N = 42, /** 'O' key. */ AKEYCODE_O = 43, /** 'P' key. */ AKEYCODE_P = 44, /** 'Q' key. */ AKEYCODE_Q = 45, /** 'R' key. */ AKEYCODE_R = 46, /** 'S' key. */ AKEYCODE_S = 47, /** 'T' key. */ AKEYCODE_T = 48, /** 'U' key. */ AKEYCODE_U = 49, /** 'V' key. */ AKEYCODE_V = 50, /** 'W' key. */ AKEYCODE_W = 51, /** 'X' key. */ AKEYCODE_X = 52, /** 'Y' key. */ AKEYCODE_Y = 53, /** 'Z' key. */ AKEYCODE_Z = 54, /** ',' key. */ AKEYCODE_COMMA = 55, /** '.' key. */ AKEYCODE_PERIOD = 56, /** Left Alt modifier key. */ AKEYCODE_ALT_LEFT = 57, /** Right Alt modifier key. */ AKEYCODE_ALT_RIGHT = 58, /** Left Shift modifier key. */ AKEYCODE_SHIFT_LEFT = 59, /** Right Shift modifier key. */ AKEYCODE_SHIFT_RIGHT = 60, /** Tab key. */ AKEYCODE_TAB = 61, /** Space key. */ AKEYCODE_SPACE = 62, /** Symbol modifier key. * Used to enter alternate symbols. */ AKEYCODE_SYM = 63, /** Explorer special function key. * Used to launch a browser application. */ AKEYCODE_EXPLORER = 64, /** Envelope special function key. * Used to launch a mail application. */ AKEYCODE_ENVELOPE = 65, /** Enter key. */ AKEYCODE_ENTER = 66, /** Backspace key. * Deletes characters before the insertion point, unlike {@link AKEYCODE_FORWARD_DEL}. */ AKEYCODE_DEL = 67, /** '`' (backtick) key. */ AKEYCODE_GRAVE = 68, /** '-'. */ AKEYCODE_MINUS = 69, /** '=' key. */ AKEYCODE_EQUALS = 70, /** '[' key. */ AKEYCODE_LEFT_BRACKET = 71, /** ']' key. */ AKEYCODE_RIGHT_BRACKET = 72, /** '\' key. */ AKEYCODE_BACKSLASH = 73, /** ';' key. */ AKEYCODE_SEMICOLON = 74, /** ''' (apostrophe) key. */ AKEYCODE_APOSTROPHE = 75, /** '/' key. */ AKEYCODE_SLASH = 76, /** '@' key. */ AKEYCODE_AT = 77, /** Number modifier key. * Used to enter numeric symbols. * This key is not {@link AKEYCODE_NUM_LOCK}; it is more like {@link AKEYCODE_ALT_LEFT}. */ AKEYCODE_NUM = 78, /** Headset Hook key. * Used to hang up calls and stop media. */ AKEYCODE_HEADSETHOOK = 79, /** Camera Focus key. * Used to focus the camera. */ AKEYCODE_FOCUS = 80, /** '+' key. */ AKEYCODE_PLUS = 81, /** Menu key. */ AKEYCODE_MENU = 82, /** Notification key. */ AKEYCODE_NOTIFICATION = 83, /** Search key. */ AKEYCODE_SEARCH = 84, /** Play/Pause media key. */ AKEYCODE_MEDIA_PLAY_PAUSE= 85, /** Stop media key. */ AKEYCODE_MEDIA_STOP = 86, /** Play Next media key. */ AKEYCODE_MEDIA_NEXT = 87, /** Play Previous media key. */ AKEYCODE_MEDIA_PREVIOUS = 88, /** Rewind media key. */ AKEYCODE_MEDIA_REWIND = 89, /** Fast Forward media key. */ AKEYCODE_MEDIA_FAST_FORWARD = 90, /** Mute key. * Mutes the microphone, unlike {@link AKEYCODE_VOLUME_MUTE}. */ AKEYCODE_MUTE = 91, /** Page Up key. */ AKEYCODE_PAGE_UP = 92, /** Page Down key. */ AKEYCODE_PAGE_DOWN = 93, /** Picture Symbols modifier key. * Used to switch symbol sets (Emoji, Kao-moji). */ AKEYCODE_PICTSYMBOLS = 94, /** Switch Charset modifier key. * Used to switch character sets (Kanji, Katakana). */ AKEYCODE_SWITCH_CHARSET = 95, /** A Button key. * On a game controller, the A button should be either the button labeled A * or the first button on the bottom row of controller buttons. */ AKEYCODE_BUTTON_A = 96, /** B Button key. * On a game controller, the B button should be either the button labeled B * or the second button on the bottom row of controller buttons. */ AKEYCODE_BUTTON_B = 97, /** C Button key. * On a game controller, the C button should be either the button labeled C * or the third button on the bottom row of controller buttons. */ AKEYCODE_BUTTON_C = 98, /** X Button key. * On a game controller, the X button should be either the button labeled X * or the first button on the upper row of controller buttons. */ AKEYCODE_BUTTON_X = 99, /** Y Button key. * On a game controller, the Y button should be either the button labeled Y * or the second button on the upper row of controller buttons. */ AKEYCODE_BUTTON_Y = 100, /** Z Button key. * On a game controller, the Z button should be either the button labeled Z * or the third button on the upper row of controller buttons. */ AKEYCODE_BUTTON_Z = 101, /** L1 Button key. * On a game controller, the L1 button should be either the button labeled L1 (or L) * or the top left trigger button. */ AKEYCODE_BUTTON_L1 = 102, /** R1 Button key. * On a game controller, the R1 button should be either the button labeled R1 (or R) * or the top right trigger button. */ AKEYCODE_BUTTON_R1 = 103, /** L2 Button key. * On a game controller, the L2 button should be either the button labeled L2 * or the bottom left trigger button. */ AKEYCODE_BUTTON_L2 = 104, /** R2 Button key. * On a game controller, the R2 button should be either the button labeled R2 * or the bottom right trigger button. */ AKEYCODE_BUTTON_R2 = 105, /** Left Thumb Button key. * On a game controller, the left thumb button indicates that the left (or only) * joystick is pressed. */ AKEYCODE_BUTTON_THUMBL = 106, /** Right Thumb Button key. * On a game controller, the right thumb button indicates that the right * joystick is pressed. */ AKEYCODE_BUTTON_THUMBR = 107, /** Start Button key. * On a game controller, the button labeled Start. */ AKEYCODE_BUTTON_START = 108, /** Select Button key. * On a game controller, the button labeled Select. */ AKEYCODE_BUTTON_SELECT = 109, /** Mode Button key. * On a game controller, the button labeled Mode. */ AKEYCODE_BUTTON_MODE = 110, /** Escape key. */ AKEYCODE_ESCAPE = 111, /** Forward Delete key. * Deletes characters ahead of the insertion point, unlike {@link AKEYCODE_DEL}. */ AKEYCODE_FORWARD_DEL = 112, /** Left Control modifier key. */ AKEYCODE_CTRL_LEFT = 113, /** Right Control modifier key. */ AKEYCODE_CTRL_RIGHT = 114, /** Caps Lock key. */ AKEYCODE_CAPS_LOCK = 115, /** Scroll Lock key. */ AKEYCODE_SCROLL_LOCK = 116, /** Left Meta modifier key. */ AKEYCODE_META_LEFT = 117, /** Right Meta modifier key. */ AKEYCODE_META_RIGHT = 118, /** Function modifier key. */ AKEYCODE_FUNCTION = 119, /** System Request / Print Screen key. */ AKEYCODE_SYSRQ = 120, /** Break / Pause key. */ AKEYCODE_BREAK = 121, /** Home Movement key. * Used for scrolling or moving the cursor around to the start of a line * or to the top of a list. */ AKEYCODE_MOVE_HOME = 122, /** End Movement key. * Used for scrolling or moving the cursor around to the end of a line * or to the bottom of a list. */ AKEYCODE_MOVE_END = 123, /** Insert key. * Toggles insert / overwrite edit mode. */ AKEYCODE_INSERT = 124, /** Forward key. * Navigates forward in the history stack. Complement of {@link AKEYCODE_BACK}. */ AKEYCODE_FORWARD = 125, /** Play media key. */ AKEYCODE_MEDIA_PLAY = 126, /** Pause media key. */ AKEYCODE_MEDIA_PAUSE = 127, /** Close media key. * May be used to close a CD tray, for example. */ AKEYCODE_MEDIA_CLOSE = 128, /** Eject media key. * May be used to eject a CD tray, for example. */ AKEYCODE_MEDIA_EJECT = 129, /** Record media key. */ AKEYCODE_MEDIA_RECORD = 130, /** F1 key. */ AKEYCODE_F1 = 131, /** F2 key. */ AKEYCODE_F2 = 132, /** F3 key. */ AKEYCODE_F3 = 133, /** F4 key. */ AKEYCODE_F4 = 134, /** F5 key. */ AKEYCODE_F5 = 135, /** F6 key. */ AKEYCODE_F6 = 136, /** F7 key. */ AKEYCODE_F7 = 137, /** F8 key. */ AKEYCODE_F8 = 138, /** F9 key. */ AKEYCODE_F9 = 139, /** F10 key. */ AKEYCODE_F10 = 140, /** F11 key. */ AKEYCODE_F11 = 141, /** F12 key. */ AKEYCODE_F12 = 142, /** Num Lock key. * This is the Num Lock key; it is different from {@link AKEYCODE_NUM}. * This key alters the behavior of other keys on the numeric keypad. */ AKEYCODE_NUM_LOCK = 143, /** Numeric keypad '0' key. */ AKEYCODE_NUMPAD_0 = 144, /** Numeric keypad '1' key. */ AKEYCODE_NUMPAD_1 = 145, /** Numeric keypad '2' key. */ AKEYCODE_NUMPAD_2 = 146, /** Numeric keypad '3' key. */ AKEYCODE_NUMPAD_3 = 147, /** Numeric keypad '4' key. */ AKEYCODE_NUMPAD_4 = 148, /** Numeric keypad '5' key. */ AKEYCODE_NUMPAD_5 = 149, /** Numeric keypad '6' key. */ AKEYCODE_NUMPAD_6 = 150, /** Numeric keypad '7' key. */ AKEYCODE_NUMPAD_7 = 151, /** Numeric keypad '8' key. */ AKEYCODE_NUMPAD_8 = 152, /** Numeric keypad '9' key. */ AKEYCODE_NUMPAD_9 = 153, /** Numeric keypad '/' key (for division). */ AKEYCODE_NUMPAD_DIVIDE = 154, /** Numeric keypad '*' key (for multiplication). */ AKEYCODE_NUMPAD_MULTIPLY = 155, /** Numeric keypad '-' key (for subtraction). */ AKEYCODE_NUMPAD_SUBTRACT = 156, /** Numeric keypad '+' key (for addition). */ AKEYCODE_NUMPAD_ADD = 157, /** Numeric keypad '.' key (for decimals or digit grouping). */ AKEYCODE_NUMPAD_DOT = 158, /** Numeric keypad ',' key (for decimals or digit grouping). */ AKEYCODE_NUMPAD_COMMA = 159, /** Numeric keypad Enter key. */ AKEYCODE_NUMPAD_ENTER = 160, /** Numeric keypad '=' key. */ AKEYCODE_NUMPAD_EQUALS = 161, /** Numeric keypad '(' key. */ AKEYCODE_NUMPAD_LEFT_PAREN = 162, /** Numeric keypad ')' key. */ AKEYCODE_NUMPAD_RIGHT_PAREN = 163, /** Volume Mute key. * Mutes the speaker, unlike {@link AKEYCODE_MUTE}. * This key should normally be implemented as a toggle such that the first press * mutes the speaker and the second press restores the original volume. */ AKEYCODE_VOLUME_MUTE = 164, /** Info key. * Common on TV remotes to show additional information related to what is * currently being viewed. */ AKEYCODE_INFO = 165, /** Channel up key. * On TV remotes, increments the television channel. */ AKEYCODE_CHANNEL_UP = 166, /** Channel down key. * On TV remotes, decrements the television channel. */ AKEYCODE_CHANNEL_DOWN = 167, /** Zoom in key. */ AKEYCODE_ZOOM_IN = 168, /** Zoom out key. */ AKEYCODE_ZOOM_OUT = 169, /** TV key. * On TV remotes, switches to viewing live TV. */ AKEYCODE_TV = 170, /** Window key. * On TV remotes, toggles picture-in-picture mode or other windowing functions. */ AKEYCODE_WINDOW = 171, /** Guide key. * On TV remotes, shows a programming guide. */ AKEYCODE_GUIDE = 172, /** DVR key. * On some TV remotes, switches to a DVR mode for recorded shows. */ AKEYCODE_DVR = 173, /** Bookmark key. * On some TV remotes, bookmarks content or web pages. */ AKEYCODE_BOOKMARK = 174, /** Toggle captions key. * Switches the mode for closed-captioning text, for example during television shows. */ AKEYCODE_CAPTIONS = 175, /** Settings key. * Starts the system settings activity. */ AKEYCODE_SETTINGS = 176, /** TV power key. * On TV remotes, toggles the power on a television screen. */ AKEYCODE_TV_POWER = 177, /** TV input key. * On TV remotes, switches the input on a television screen. */ AKEYCODE_TV_INPUT = 178, /** Set-top-box power key. * On TV remotes, toggles the power on an external Set-top-box. */ AKEYCODE_STB_POWER = 179, /** Set-top-box input key. * On TV remotes, switches the input mode on an external Set-top-box. */ AKEYCODE_STB_INPUT = 180, /** A/V Receiver power key. * On TV remotes, toggles the power on an external A/V Receiver. */ AKEYCODE_AVR_POWER = 181, /** A/V Receiver input key. * On TV remotes, switches the input mode on an external A/V Receiver. */ AKEYCODE_AVR_INPUT = 182, /** Red "programmable" key. * On TV remotes, acts as a contextual/programmable key. */ AKEYCODE_PROG_RED = 183, /** Green "programmable" key. * On TV remotes, actsas a contextual/programmable key. */ AKEYCODE_PROG_GREEN = 184, /** Yellow "programmable" key. * On TV remotes, acts as a contextual/programmable key. */ AKEYCODE_PROG_YELLOW = 185, /** Blue "programmable" key. * On TV remotes, acts as a contextual/programmable key. */ AKEYCODE_PROG_BLUE = 186, /** App switch key. * Should bring up the application switcher dialog. */ AKEYCODE_APP_SWITCH = 187, /** Generic Game Pad Button #1.*/ AKEYCODE_BUTTON_1 = 188, /** Generic Game Pad Button #2.*/ AKEYCODE_BUTTON_2 = 189, /** Generic Game Pad Button #3.*/ AKEYCODE_BUTTON_3 = 190, /** Generic Game Pad Button #4.*/ AKEYCODE_BUTTON_4 = 191, /** Generic Game Pad Button #5.*/ AKEYCODE_BUTTON_5 = 192, /** Generic Game Pad Button #6.*/ AKEYCODE_BUTTON_6 = 193, /** Generic Game Pad Button #7.*/ AKEYCODE_BUTTON_7 = 194, /** Generic Game Pad Button #8.*/ AKEYCODE_BUTTON_8 = 195, /** Generic Game Pad Button #9.*/ AKEYCODE_BUTTON_9 = 196, /** Generic Game Pad Button #10.*/ AKEYCODE_BUTTON_10 = 197, /** Generic Game Pad Button #11.*/ AKEYCODE_BUTTON_11 = 198, /** Generic Game Pad Button #12.*/ AKEYCODE_BUTTON_12 = 199, /** Generic Game Pad Button #13.*/ AKEYCODE_BUTTON_13 = 200, /** Generic Game Pad Button #14.*/ AKEYCODE_BUTTON_14 = 201, /** Generic Game Pad Button #15.*/ AKEYCODE_BUTTON_15 = 202, /** Generic Game Pad Button #16.*/ AKEYCODE_BUTTON_16 = 203, /** Language Switch key. * Toggles the current input language such as switching between English and Japanese on * a QWERTY keyboard. On some devices, the same function may be performed by * pressing Shift+Spacebar. */ AKEYCODE_LANGUAGE_SWITCH = 204, /** Manner Mode key. * Toggles silent or vibrate mode on and off to make the device behave more politely * in certain settings such as on a crowded train. On some devices, the key may only * operate when long-pressed. */ AKEYCODE_MANNER_MODE = 205, /** 3D Mode key. * Toggles the display between 2D and 3D mode. */ AKEYCODE_3D_MODE = 206, /** Contacts special function key. * Used to launch an address book application. */ AKEYCODE_CONTACTS = 207, /** Calendar special function key. * Used to launch a calendar application. */ AKEYCODE_CALENDAR = 208, /** Music special function key. * Used to launch a music player application. */ AKEYCODE_MUSIC = 209, /** Calculator special function key. * Used to launch a calculator application. */ AKEYCODE_CALCULATOR = 210, /** Japanese full-width / half-width key. */ AKEYCODE_ZENKAKU_HANKAKU = 211, /** Japanese alphanumeric key. */ AKEYCODE_EISU = 212, /** Japanese non-conversion key. */ AKEYCODE_MUHENKAN = 213, /** Japanese conversion key. */ AKEYCODE_HENKAN = 214, /** Japanese katakana / hiragana key. */ AKEYCODE_KATAKANA_HIRAGANA = 215, /** Japanese Yen key. */ AKEYCODE_YEN = 216, /** Japanese Ro key. */ AKEYCODE_RO = 217, /** Japanese kana key. */ AKEYCODE_KANA = 218, /** Assist key. * Launches the global assist activity. Not delivered to applications. */ AKEYCODE_ASSIST = 219, /** Brightness Down key. * Adjusts the screen brightness down. */ AKEYCODE_BRIGHTNESS_DOWN = 220, /** Brightness Up key. * Adjusts the screen brightness up. */ AKEYCODE_BRIGHTNESS_UP = 221, /** Audio Track key. * Switches the audio tracks. */ AKEYCODE_MEDIA_AUDIO_TRACK = 222, /** Sleep key. * Puts the device to sleep. Behaves somewhat like {@link AKEYCODE_POWER} but it * has no effect if the device is already asleep. */ AKEYCODE_SLEEP = 223, /** Wakeup key. * Wakes up the device. Behaves somewhat like {@link AKEYCODE_POWER} but it * has no effect if the device is already awake. */ AKEYCODE_WAKEUP = 224, /** Pairing key. * Initiates peripheral pairing mode. Useful for pairing remote control * devices or game controllers, especially if no other input mode is * available. */ AKEYCODE_PAIRING = 225, /** Media Top Menu key. * Goes to the top of media menu. */ AKEYCODE_MEDIA_TOP_MENU = 226, /** '11' key. */ AKEYCODE_11 = 227, /** '12' key. */ AKEYCODE_12 = 228, /** Last Channel key. * Goes to the last viewed channel. */ AKEYCODE_LAST_CHANNEL = 229, /** TV data service key. * Displays data services like weather, sports. */ AKEYCODE_TV_DATA_SERVICE = 230, /** Voice Assist key. * Launches the global voice assist activity. Not delivered to applications. */ AKEYCODE_VOICE_ASSIST = 231, /** Radio key. * Toggles TV service / Radio service. */ AKEYCODE_TV_RADIO_SERVICE = 232, /** Teletext key. * Displays Teletext service. */ AKEYCODE_TV_TELETEXT = 233, /** Number entry key. * Initiates to enter multi-digit channel nubmber when each digit key is assigned * for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC * User Control Code. */ AKEYCODE_TV_NUMBER_ENTRY = 234, /** Analog Terrestrial key. * Switches to analog terrestrial broadcast service. */ AKEYCODE_TV_TERRESTRIAL_ANALOG = 235, /** Digital Terrestrial key. * Switches to digital terrestrial broadcast service. */ AKEYCODE_TV_TERRESTRIAL_DIGITAL = 236, /** Satellite key. * Switches to digital satellite broadcast service. */ AKEYCODE_TV_SATELLITE = 237, /** BS key. * Switches to BS digital satellite broadcasting service available in Japan. */ AKEYCODE_TV_SATELLITE_BS = 238, /** CS key. * Switches to CS digital satellite broadcasting service available in Japan. */ AKEYCODE_TV_SATELLITE_CS = 239, /** BS/CS key. * Toggles between BS and CS digital satellite services. */ AKEYCODE_TV_SATELLITE_SERVICE = 240, /** Toggle Network key. * Toggles selecting broacast services. */ AKEYCODE_TV_NETWORK = 241, /** Antenna/Cable key. * Toggles broadcast input source between antenna and cable. */ AKEYCODE_TV_ANTENNA_CABLE = 242, /** HDMI #1 key. * Switches to HDMI input #1. */ AKEYCODE_TV_INPUT_HDMI_1 = 243, /** HDMI #2 key. * Switches to HDMI input #2. */ AKEYCODE_TV_INPUT_HDMI_2 = 244, /** HDMI #3 key. * Switches to HDMI input #3. */ AKEYCODE_TV_INPUT_HDMI_3 = 245, /** HDMI #4 key. * Switches to HDMI input #4. */ AKEYCODE_TV_INPUT_HDMI_4 = 246, /** Composite #1 key. * Switches to composite video input #1. */ AKEYCODE_TV_INPUT_COMPOSITE_1 = 247, /** Composite #2 key. * Switches to composite video input #2. */ AKEYCODE_TV_INPUT_COMPOSITE_2 = 248, /** Component #1 key. * Switches to component video input #1. */ AKEYCODE_TV_INPUT_COMPONENT_1 = 249, /** Component #2 key. * Switches to component video input #2. */ AKEYCODE_TV_INPUT_COMPONENT_2 = 250, /** VGA #1 key. * Switches to VGA (analog RGB) input #1. */ AKEYCODE_TV_INPUT_VGA_1 = 251, /** Audio description key. * Toggles audio description off / on. */ AKEYCODE_TV_AUDIO_DESCRIPTION = 252, /** Audio description mixing volume up key. * Louden audio description volume as compared with normal audio volume. */ AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253, /** Audio description mixing volume down key. * Lessen audio description volume as compared with normal audio volume. */ AKEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254, /** Zoom mode key. * Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) */ AKEYCODE_TV_ZOOM_MODE = 255, /** Contents menu key. * Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control * Code */ AKEYCODE_TV_CONTENTS_MENU = 256, /** Media context menu key. * Goes to the context menu of media contents. Corresponds to Media Context-sensitive * Menu (0x11) of CEC User Control Code. */ AKEYCODE_TV_MEDIA_CONTEXT_MENU = 257, /** Timer programming key. * Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of * CEC User Control Code. */ AKEYCODE_TV_TIMER_PROGRAMMING = 258, /** Help key. */ AKEYCODE_HELP = 259, AKEYCODE_NAVIGATE_PREVIOUS = 260, AKEYCODE_NAVIGATE_NEXT = 261, AKEYCODE_NAVIGATE_IN = 262, AKEYCODE_NAVIGATE_OUT = 263, /** Primary stem key for Wear * Main power/reset button on watch. */ AKEYCODE_STEM_PRIMARY = 264, /** Generic stem key 1 for Wear */ AKEYCODE_STEM_1 = 265, /** Generic stem key 2 for Wear */ AKEYCODE_STEM_2 = 266, /** Generic stem key 3 for Wear */ AKEYCODE_STEM_3 = 267, /** Directional Pad Up-Left */ AKEYCODE_DPAD_UP_LEFT = 268, /** Directional Pad Down-Left */ AKEYCODE_DPAD_DOWN_LEFT = 269, /** Directional Pad Up-Right */ AKEYCODE_DPAD_UP_RIGHT = 270, /** Directional Pad Down-Right */ AKEYCODE_DPAD_DOWN_RIGHT = 271, /** Skip forward media key */ AKEYCODE_MEDIA_SKIP_FORWARD = 272, /** Skip backward media key */ AKEYCODE_MEDIA_SKIP_BACKWARD = 273, /** Step forward media key. * Steps media forward one from at a time. */ AKEYCODE_MEDIA_STEP_FORWARD = 274, /** Step backward media key. * Steps media backward one from at a time. */ AKEYCODE_MEDIA_STEP_BACKWARD = 275, /** Put device to sleep unless a wakelock is held. */ AKEYCODE_SOFT_SLEEP = 276, /** Cut key. */ AKEYCODE_CUT = 277, /** Copy key. */ AKEYCODE_COPY = 278, /** Paste key. */ AKEYCODE_PASTE = 279, /** fingerprint navigation key, up. */ AKEYCODE_SYSTEM_NAVIGATION_UP = 280, /** fingerprint navigation key, down. */ AKEYCODE_SYSTEM_NAVIGATION_DOWN = 281, /** fingerprint navigation key, left. */ AKEYCODE_SYSTEM_NAVIGATION_LEFT = 282, /** fingerprint navigation key, right. */ AKEYCODE_SYSTEM_NAVIGATION_RIGHT = 283, /** all apps */ AKEYCODE_ALL_APPS = 284 }; #endif // _ANDROID_KEYCODES_H scrcpy-1.21/app/src/aoa_hid.c000066400000000000000000000275441415124136000160500ustar00rootroot00000000000000#include "util/log.h" #include #include #include "aoa_hid.h" #include "util/log.h" // See . #define ACCESSORY_REGISTER_HID 54 #define ACCESSORY_SET_HID_REPORT_DESC 56 #define ACCESSORY_SEND_HID_EVENT 57 #define ACCESSORY_UNREGISTER_HID 55 #define DEFAULT_TIMEOUT 1000 static void sc_hid_event_log(const struct sc_hid_event *event) { // HID Event: [00] FF FF FF FF... assert(event->size); unsigned buffer_size = event->size * 3 + 1; char *buffer = malloc(buffer_size); if (!buffer) { LOG_OOM(); return; } for (unsigned i = 0; i < event->size; ++i) { snprintf(buffer + i * 3, 4, " %02x", event->buffer[i]); } LOGV("HID Event: [%d]%s", event->accessory_id, buffer); free(buffer); } void sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, unsigned char *buffer, uint16_t buffer_size) { hid_event->accessory_id = accessory_id; hid_event->buffer = buffer; hid_event->size = buffer_size; hid_event->ack_to_wait = SC_SEQUENCE_INVALID; } void sc_hid_event_destroy(struct sc_hid_event *hid_event) { free(hid_event->buffer); } static inline void log_libusb_error(enum libusb_error errcode) { LOGW("libusb error: %s", libusb_strerror(errcode)); } static bool accept_device(libusb_device *device, const char *serial) { // do not log any USB error in this function, it is expected that many USB // devices available on the computer have permission restrictions struct libusb_device_descriptor desc; libusb_get_device_descriptor(device, &desc); if (!desc.iSerialNumber) { return false; } libusb_device_handle *handle; int result = libusb_open(device, &handle); if (result < 0) { return false; } char buffer[128]; result = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, (unsigned char *) buffer, sizeof(buffer)); libusb_close(handle); if (result < 0) { return false; } buffer[sizeof(buffer) - 1] = '\0'; // just in case // accept the device if its serial matches return !strcmp(buffer, serial); } static libusb_device * sc_aoa_find_usb_device(const char *serial) { if (!serial) { return NULL; } libusb_device **list; libusb_device *result = NULL; ssize_t count = libusb_get_device_list(NULL, &list); if (count < 0) { log_libusb_error((enum libusb_error) count); return NULL; } for (size_t i = 0; i < (size_t) count; ++i) { libusb_device *device = list[i]; if (accept_device(device, serial)) { result = libusb_ref_device(device); break; } } libusb_free_device_list(list, 1); return result; } static int sc_aoa_open_usb_handle(libusb_device *device, libusb_device_handle **handle) { int result = libusb_open(device, handle); if (result < 0) { log_libusb_error((enum libusb_error) result); return result; } return 0; } bool sc_aoa_init(struct sc_aoa *aoa, const char *serial, struct sc_acksync *acksync) { assert(acksync); cbuf_init(&aoa->queue); if (!sc_mutex_init(&aoa->mutex)) { return false; } if (!sc_cond_init(&aoa->event_cond)) { sc_mutex_destroy(&aoa->mutex); return false; } if (libusb_init(&aoa->usb_context) != LIBUSB_SUCCESS) { sc_cond_destroy(&aoa->event_cond); sc_mutex_destroy(&aoa->mutex); return false; } aoa->usb_device = sc_aoa_find_usb_device(serial); if (!aoa->usb_device) { LOGW("USB device of serial %s not found", serial); libusb_exit(aoa->usb_context); sc_mutex_destroy(&aoa->mutex); sc_cond_destroy(&aoa->event_cond); return false; } if (sc_aoa_open_usb_handle(aoa->usb_device, &aoa->usb_handle) < 0) { LOGW("Open USB handle failed"); libusb_unref_device(aoa->usb_device); libusb_exit(aoa->usb_context); sc_cond_destroy(&aoa->event_cond); sc_mutex_destroy(&aoa->mutex); return false; } aoa->stopped = false; aoa->acksync = acksync; return true; } void sc_aoa_destroy(struct sc_aoa *aoa) { // Destroy remaining events struct sc_hid_event event; while (cbuf_take(&aoa->queue, &event)) { sc_hid_event_destroy(&event); } libusb_close(aoa->usb_handle); libusb_unref_device(aoa->usb_device); libusb_exit(aoa->usb_context); sc_cond_destroy(&aoa->event_cond); sc_mutex_destroy(&aoa->mutex); } static bool sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id, uint16_t report_desc_size) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_REGISTER_HID; // // value (arg0): accessory assigned ID for the HID device // index (arg1): total length of the HID report descriptor uint16_t value = accessory_id; uint16_t index = report_desc_size; unsigned char *buffer = NULL; uint16_t length = 0; int result = libusb_control_transfer(aoa->usb_handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); return false; } return true; } static bool sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, const unsigned char *report_desc, uint16_t report_desc_size) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; /** * If the HID descriptor is longer than the endpoint zero max packet size, * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC * commands. The data for the descriptor must be sent sequentially * if multiple packets are needed. * * * libusb handles packet abstraction internally, so we don't need to care * about bMaxPacketSize0 here. * * See */ // value (arg0): accessory assigned ID for the HID device // index (arg1): offset of data (buffer) in descriptor uint16_t value = accessory_id; uint16_t index = 0; // libusb_control_transfer expects a pointer to non-const unsigned char *buffer = (unsigned char *) report_desc; uint16_t length = report_desc_size; int result = libusb_control_transfer(aoa->usb_handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); return false; } return true; } bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, const unsigned char *report_desc, uint16_t report_desc_size) { bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); if (!ok) { return false; } ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc, report_desc_size); if (!ok) { if (!sc_aoa_unregister_hid(aoa, accessory_id)) { LOGW("Could not unregister HID"); } return false; } return true; } static bool sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_SEND_HID_EVENT; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 (unused) uint16_t value = event->accessory_id; uint16_t index = 0; unsigned char *buffer = event->buffer; uint16_t length = event->size; int result = libusb_control_transfer(aoa->usb_handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); return false; } return true; } bool sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request = ACCESSORY_UNREGISTER_HID; // // value (arg0): accessory assigned ID for the HID device // index (arg1): 0 uint16_t value = accessory_id; uint16_t index = 0; unsigned char *buffer = NULL; uint16_t length = 0; int result = libusb_control_transfer(aoa->usb_handle, request_type, request, value, index, buffer, length, DEFAULT_TIMEOUT); if (result < 0) { log_libusb_error((enum libusb_error) result); return false; } return true; } bool sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { sc_hid_event_log(event); } sc_mutex_lock(&aoa->mutex); bool was_empty = cbuf_is_empty(&aoa->queue); bool res = cbuf_push(&aoa->queue, *event); if (was_empty) { sc_cond_signal(&aoa->event_cond); } sc_mutex_unlock(&aoa->mutex); return res; } static int run_aoa_thread(void *data) { struct sc_aoa *aoa = data; for (;;) { sc_mutex_lock(&aoa->mutex); while (!aoa->stopped && cbuf_is_empty(&aoa->queue)) { sc_cond_wait(&aoa->event_cond, &aoa->mutex); } if (aoa->stopped) { // Stop immediately, do not process further events sc_mutex_unlock(&aoa->mutex); break; } struct sc_hid_event event; bool non_empty = cbuf_take(&aoa->queue, &event); assert(non_empty); (void) non_empty; 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); // 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"); sc_hid_event_destroy(&event); continue; } else if (result == SC_ACKSYNC_WAIT_INTR) { // stopped sc_hid_event_destroy(&event); break; } } bool ok = sc_aoa_send_hid_event(aoa, &event); sc_hid_event_destroy(&event); if (!ok) { LOGW("Could not send HID event to USB device"); } } return 0; } bool sc_aoa_start(struct sc_aoa *aoa) { LOGD("Starting AOA thread"); bool ok = sc_thread_create(&aoa->thread, run_aoa_thread, "aoa_thread", aoa); if (!ok) { LOGC("Could not start AOA thread"); return false; } return true; } void sc_aoa_stop(struct sc_aoa *aoa) { sc_mutex_lock(&aoa->mutex); aoa->stopped = true; sc_cond_signal(&aoa->event_cond); sc_mutex_unlock(&aoa->mutex); sc_acksync_interrupt(aoa->acksync); } void sc_aoa_join(struct sc_aoa *aoa) { sc_thread_join(&aoa->thread, NULL); } scrcpy-1.21/app/src/aoa_hid.h000066400000000000000000000026611415124136000160460ustar00rootroot00000000000000#ifndef SC_AOA_HID_H #define SC_AOA_HID_H #include #include #include #include "util/acksync.h" #include "util/cbuf.h" #include "util/thread.h" #include "util/tick.h" struct sc_hid_event { uint16_t accessory_id; unsigned char *buffer; uint16_t size; uint64_t ack_to_wait; }; // Takes ownership of buffer void sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id, unsigned char *buffer, uint16_t buffer_size); void sc_hid_event_destroy(struct sc_hid_event *hid_event); struct sc_hid_event_queue CBUF(struct sc_hid_event, 64); struct sc_aoa { libusb_context *usb_context; libusb_device *usb_device; libusb_device_handle *usb_handle; sc_thread thread; sc_mutex mutex; sc_cond event_cond; bool stopped; struct sc_hid_event_queue queue; struct sc_acksync *acksync; }; bool sc_aoa_init(struct sc_aoa *aoa, const char *serial, 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 unsigned char *report_desc, uint16_t report_desc_size); bool sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); bool sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event); #endif scrcpy-1.21/app/src/cli.c000066400000000000000000001323151415124136000152240ustar00rootroot00000000000000#include "cli.h" #include #include #include #include #include #include #include "options.h" #include "util/log.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) #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 #define OPT_TUNNEL_HOST 1030 #define OPT_TUNNEL_PORT 1031 #define OPT_NO_CLIPBOARD_AUTOSYNC 1032 #define OPT_TCPIP 1033 #define OPT_RAW_KEY_EVENTS 1034 struct sc_option { char shortopt; int longopt_id; // either shortopt or longopt_id is non-zero const char *longopt; // no argument: argdesc == NULL && !optional_arg // optional argument: argdesc != NULL && optional_arg // required argument: argdesc != NULL && !optional_arg const char *argdesc; bool optional_arg; const char *text; // if NULL, the option does not appear in the help }; #define MAX_EQUIVALENT_SHORTCUTS 3 struct sc_shortcut { const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1]; const char *text; }; struct sc_getopt_adapter { char *optstring; struct option *longopts; }; static const struct sc_option options[] = { { .longopt_id = OPT_ALWAYS_ON_TOP, .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, { .shortopt = 'b', .longopt = "bit-rate", .argdesc = "value", .text = "Encode the video at the gitven bit-rate, expressed in bits/s. " "Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" "Default is " STR(DEFAULT_BIT_RATE) ".", }, { .longopt_id = OPT_CODEC_OPTIONS, .longopt = "codec-options", .argdesc = "key[:type]=value[,...]", .text = "Set a list of comma-separated key:type=value options for the " "device encoder.\n" "The possible values for 'type' are 'int' (default), 'long', " "'float' and 'string'.\n" "The list of possible codec options is available in the " "Android documentation: " "", }, { .longopt_id = OPT_CROP, .longopt = "crop", .argdesc = "width:height:x:y", .text = "Crop the device screen on the server.\n" "The values are expressed in the device natural orientation " "(typically, portrait for a phone, landscape for a tablet). " "Any --max-size value is cmoputed on the cropped size.", }, { .longopt_id = OPT_DISABLE_SCREENSAVER, .longopt = "disable-screensaver", .text = "Disable screensaver while scrcpy is running.", }, { .longopt_id = OPT_DISPLAY_ID, .longopt = "display", .argdesc = "id", .text = "Specify the display id to mirror.\n" "The list of possible display ids can be listed by:\n" " adb shell dumpsys display\n" "(search \"mDisplayId=\" in the output)\n" "Default is 0.", }, { .longopt_id = OPT_DISPLAY_BUFFER, .longopt = "display-buffer", .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before displaying. " "This increases latency to compensate for jitter.\n" "Default is 0 (no buffering).", }, { .longopt_id = OPT_ENCODER_NAME, .longopt = "encoder", .argdesc = "name", .text = "Use a specific MediaCodec encoder (must be a H.264 encoder).", }, { .longopt_id = OPT_FORCE_ADB_FORWARD, .longopt = "force-adb-forward", .text = "Do not attempt to use \"adb reverse\" to connect to the " "device.", }, { .longopt_id = OPT_FORWARD_ALL_CLICKS, .longopt = "forward-all-clicks", .text = "By default, right-click triggers BACK (or POWER on) and " "middle-click triggers HOME. This option disables these " "shortcuts and forwards the clicks to the device instead.", }, { .shortopt = 'f', .longopt = "fullscreen", .text = "Start in fullscreen.", }, { .shortopt = 'K', .longopt = "hid-keyboard", .text = "Simulate a physical keyboard by using HID over AOAv2.\n" "It provides a better experience for IME users, and allows to " "generate non-ASCII characters, contrary to the default " "injection method.\n" "It may only work over USB, and is currently only supported " "on Linux.\n" "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: `adb shell am start -a " "android.settings.HARD_KEYBOARD_SETTINGS`.\n" "However, the option is only available when the HID keyboard " "is enabled (or a physical keyboard is connected).", }, { .shortopt = 'h', .longopt = "help", .text = "Print this help.", }, { .longopt_id = OPT_LEGACY_PASTE, .longopt = "legacy-paste", .text = "Inject computer clipboard text as a sequence of key events " "on Ctrl+v (like MOD+Shift+v).\n" "This is a workaround for some devices not behaving as " "expected when setting the device clipboard programmatically.", }, { .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt = "lock-video-orientation", .argdesc = "value", .optional_arg = true, .text = "Lock video orientation to value.\n" "Possible values are \"unlocked\", \"initial\" (locked to the " "initial orientation), 0, 1, 2 and 3. Natural device " "orientation is 0, and each increment adds a 90 degrees " "rotation counterclockwise.\n" "Default is \"unlocked\".\n" "Passing the option without argument is equivalent to passing " "\"initial\".", }, { .longopt_id = OPT_MAX_FPS, .longopt = "max-fps", .argdesc = "value", .text = "Limit the frame rate of screen capture (officially supported " "since Android 10, but may work on earlier versions).", }, { .shortopt = 'm', .longopt = "max-size", .argdesc = "value", .text = "Limit both the width and height of the video to value. The " "other dimension is computed so that the device aspect-ratio " "is preserved.\n" "Default is 0 (unlimited).", }, { .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." }, { .shortopt = 'n', .longopt = "no-control", .text = "Disable device control (mirror the device in read-only).", }, { .shortopt = 'N', .longopt = "no-display", .text = "Do not display device (only when screen recording " #ifdef HAVE_V4L2 "or V4L2 sink " #endif "is enabled).", }, { .longopt_id = OPT_NO_KEY_REPEAT, .longopt = "no-key-repeat", .text = "Do not forward repeated key events when a key is held down.", }, { .longopt_id = OPT_NO_MIPMAPS, .longopt = "no-mipmaps", .text = "If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then " "mipmaps are automatically generated to improve downscaling " "quality. This option disables the generation of mipmaps.", }, { .shortopt = 'p', .longopt = "port", .argdesc = "port[:port]", .text = "Set the TCP port (range) used by the client to listen.\n" "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", }, { .longopt_id = OPT_POWER_OFF_ON_CLOSE, .longopt = "power-off-on-close", .text = "Turn the device screen off when closing scrcpy.", }, { .longopt_id = OPT_PREFER_TEXT, .longopt = "prefer-text", .text = "Inject alpha characters and space as text events instead of" "key events.\n" "This avoids issues when combining multiple keys to enter a " "special character, but breaks the expected behavior of alpha " "keys in games (typically WASD).", }, { .longopt_id = OPT_PUSH_TARGET, .longopt = "push-target", .argdesc = "path", .text = "Set the target directory for pushing files to the device by " "drag & drop. It is passed as is to \"adb push\".\n" "Default is \"/sdcard/Download/\".", }, { .longopt_id = OPT_RAW_KEY_EVENTS, .longopt = "raw-key-events", .text = "Inject key events for all input keys, and ignore text events." }, { .shortopt = 'r', .longopt = "record", .argdesc = "file.mp4", .text = "Record screen to file.\n" "The format is determined by the --record-format option if " "set, or by the file extension (.mp4 or .mkv).", }, { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", .text = "Force recording format (either mp4 or mkv).", }, { .longopt_id = OPT_RENDER_DRIVER, .longopt = "render-driver", .argdesc = "name", .text = "Request SDL to use the given render driver (this is just a " "hint).\n" "Supported names are currently \"direct3d\", \"opengl\", " "\"opengles2\", \"opengles\", \"metal\" and \"software\".\n" "", }, { // deprecated .longopt_id = OPT_RENDER_EXPIRED_FRAMES, .longopt = "render-expired-frames", }, { .longopt_id = OPT_ROTATION, .longopt = "rotation", .argdesc = "value", .text = "Set the initial display rotation.\n" "Possible values are 0, 1, 2 and 3. Each increment adds a 90 " "degrees rotation counterclockwise.", }, { .shortopt = 's', .longopt = "serial", .argdesc = "serial", .text = "The device serial number. Mandatory only if several devices " "are connected to adb.", }, { .longopt_id = OPT_SHORTCUT_MOD, .longopt = "shortcut-mod", .argdesc = "key[+...][,...]", .text = "Specify the modifiers to use for scrcpy shortcuts.\n" "Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", " "\"lsuper\" and \"rsuper\".\n" "A shortcut can consist in several keys, separated by '+'. " "Several shortcuts can be specified, separated by ','.\n" "For example, to use either LCtrl+LAlt or LSuper for scrcpy " "shortcuts, pass \"lctrl+lalt,lsuper\".\n" "Default is \"lalt,lsuper\" (left-Alt or left-Super).", }, { .shortopt = 'S', .longopt = "turn-screen-off", .text = "Turn the device screen off immediately.", }, { .shortopt = 't', .longopt = "show-touches", .text = "Enable \"show touches\" on start, restore the initial value " "on exit.\n" "It only shows physical touches (not clicks from scrcpy).", }, { .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.", }, #ifdef HAVE_V4L2 { .longopt_id = OPT_V4L2_SINK, .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" "It requires to lock the video orientation (see " "--lock-video-orientation).", }, { .longopt_id = OPT_V4L2_BUFFER, .longopt = "v4l2-buffer", .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before pushing " "frames. This increases latency to compensate for jitter.\n" "This option is similar to --display-buffer, but specific to " "V4L2 sink.\n" "Default is 0 (no buffering).", }, #endif { .shortopt = 'V', .longopt = "verbosity", .argdesc = "value", .text = "Set the log level (verbose, debug, info, warn or error).\n" #ifndef NDEBUG "Default is debug.", #else "Default is info.", #endif }, { .shortopt = 'v', .longopt = "version", .text = "Print the version of scrcpy.", }, { .shortopt = 'w', .longopt = "stay-awake", .text = "Keep the device on while scrcpy is running, when the device " "is plugged in.", }, { .longopt_id = OPT_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_WINDOW_BORDERLESS, .longopt = "window-borderless", .text = "Disable window decorations (display borderless window)." }, { .longopt_id = OPT_WINDOW_TITLE, .longopt = "window-title", .argdesc = "text", .text = "Set a custom window title.", }, { .longopt_id = OPT_WINDOW_X, .longopt = "window-x", .argdesc = "value", .text = "Set the initial window horizontal position.\n" "Default is \"auto\".", }, { .longopt_id = OPT_WINDOW_Y, .longopt = "window-y", .argdesc = "value", .text = "Set the initial window vertical position.\n" "Default is \"auto\".", }, { .longopt_id = OPT_WINDOW_WIDTH, .longopt = "window-width", .argdesc = "value", .text = "Set the initial window width.\n" "Default is 0 (automatic).", }, { .longopt_id = OPT_WINDOW_HEIGHT, .longopt = "window-height", .argdesc = "value", .text = "Set the initial window height.\n" "Default is 0 (automatic).", }, }; static const struct sc_shortcut shortcuts[] = { { .shortcuts = { "MOD+f" }, .text = "Switch fullscreen mode", }, { .shortcuts = { "MOD+Left" }, .text = "Rotate display left", }, { .shortcuts = { "MOD+Right" }, .text = "Rotate display right", }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", }, { .shortcuts = { "MOD+w", "Double-click on black borders" }, .text = "Resize window to remove black borders", }, { .shortcuts = { "MOD+h", "Middle-click" }, .text = "Click on HOME", }, { .shortcuts = { "MOD+b", "MOD+Backspace", "Right-click (when screen is on)", }, .text = "Click on BACK", }, { .shortcuts = { "MOD+s" }, .text = "Click on APP_SWITCH", }, { .shortcuts = { "MOD+m" }, .text = "Click on MENU", }, { .shortcuts = { "MOD+Up" }, .text = "Click on VOLUME_UP", }, { .shortcuts = { "MOD+Down" }, .text = "Click on VOLUME_DOWN", }, { .shortcuts = { "MOD+p" }, .text = "Click on POWER (turn screen on/off)", }, { .shortcuts = { "Right-click (when screen is off)" }, .text = "Power on", }, { .shortcuts = { "MOD+o" }, .text = "Turn device screen off (keep mirroring)", }, { .shortcuts = { "MOD+Shift+o" }, .text = "Turn device screen on", }, { .shortcuts = { "MOD+r" }, .text = "Rotate device screen", }, { .shortcuts = { "MOD+n" }, .text = "Expand notification panel", }, { .shortcuts = { "MOD+Shift+n" }, .text = "Collapse notification panel", }, { .shortcuts = { "MOD+c" }, .text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)", }, { .shortcuts = { "MOD+x" }, .text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)", }, { .shortcuts = { "MOD+v" }, .text = "Copy computer clipboard to device, then paste (inject PASTE " "keycode, Android >= 7 only)", }, { .shortcuts = { "MOD+Shift+v" }, .text = "Inject computer clipboard text as a sequence of key events", }, { .shortcuts = { "MOD+i" }, .text = "Enable/disable FPS counter (print frames/second in logs)", }, { .shortcuts = { "Ctrl+click-and-move" }, .text = "Pinch-to-zoom from the center of the screen", }, { .shortcuts = { "Drag & drop APK file" }, .text = "Install APK from computer", }, { .shortcuts = { "Drag & drop non-APK file" }, .text = "Push file to device (see --push-target)", }, }; static char * sc_getopt_adapter_create_optstring(void) { struct sc_strbuf buf; if (!sc_strbuf_init(&buf, 64)) { return false; } for (size_t i = 0; i < ARRAY_LEN(options); ++i) { const struct sc_option *opt = &options[i]; if (opt->shortopt) { if (!sc_strbuf_append_char(&buf, opt->shortopt)) { goto error; } // If there is an argument, add ':' if (opt->argdesc) { if (!sc_strbuf_append_char(&buf, ':')) { goto error; } // If the argument is optional, add another ':' if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) { goto error; } } } } return buf.s; error: free(buf.s); return NULL; } static struct option * sc_getopt_adapter_create_longopts(void) { struct option *longopts = malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts)); if (!longopts) { 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("%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); } 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\n"); print_shortcuts_intro(cols); for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { print_shortcut(&shortcuts[i], cols); } } static bool parse_integer_arg(const char *s, long *out, bool accept_suffix, long min, long max, const char *name) { long value; bool ok; if (accept_suffix) { ok = sc_str_parse_integer_with_suffix(s, &value); } else { ok = sc_str_parse_integer(s, &value); } if (!ok) { LOGE("Could not parse %s: %s", name, s); return false; } if (value < min || value > max) { LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", name, value, min, max); return false; } *out = value; return true; } static size_t parse_integers_arg(const char *s, size_t max_items, long *out, long min, long max, const char *name) { size_t count = sc_str_parse_integers(s, ':', max_items, out); if (!count) { LOGE("Could not parse %s: %s", name, s); return 0; } for (size_t i = 0; i < count; ++i) { long value = out[i]; if (value < min || value > max) { LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)", name, value, min, max); return 0; } } return count; } static bool parse_bit_rate(const char *s, uint32_t *bit_rate) { long value; // long may be 32 bits (it is the case on mingw), so do not use more than // 31 bits (long is signed) bool ok = parse_integer_arg(s, &value, true, 0, 0x7FFFFFFF, "bit-rate"); if (!ok) { return false; } *bit_rate = (uint32_t) value; return true; } static bool parse_max_size(const char *s, uint16_t *max_size) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size"); if (!ok) { return false; } *max_size = (uint16_t) value; return true; } 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"); if (!ok) { return false; } *max_fps = (uint16_t) value; return true; } static bool parse_buffering_time(const char *s, sc_tick *tick) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "buffering time"); if (!ok) { return false; } *tick = SC_TICK_FROM_MS(value); return true; } static bool parse_lock_video_orientation(const char *s, enum sc_lock_video_orientation *lock_mode) { if (!s || !strcmp(s, "initial")) { // Without argument, lock the initial orientation *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; return true; } if (!strcmp(s, "unlocked")) { *lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED; return true; } long value; bool ok = parse_integer_arg(s, &value, false, 0, 3, "lock video orientation"); if (!ok) { return false; } *lock_mode = (enum sc_lock_video_orientation) value; return true; } static bool parse_rotation(const char *s, uint8_t *rotation) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); if (!ok) { return false; } *rotation = (uint8_t) value; return true; } static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value"); if (!strcmp(s, "auto")) { *position = SC_WINDOW_POSITION_UNDEFINED; return true; } long value; bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF, "window position"); if (!ok) { return false; } *position = (int16_t) value; return true; } static bool parse_window_dimension(const char *s, uint16_t *dimension) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "window dimension"); if (!ok) { return false; } *dimension = (uint16_t) value; return true; } 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"); if (!count) { return false; } uint16_t v0 = (uint16_t) values[0]; if (count == 1) { port_range->first = v0; port_range->last = v0; return true; } assert(count == 2); uint16_t v1 = (uint16_t) values[1]; if (v0 < v1) { port_range->first = v0; port_range->last = v1; } else { port_range->first = v1; port_range->last = v0; } return true; } static bool parse_display_id(const char *s, uint32_t *display_id) { long value; bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "display id"); if (!ok) { return false; } *display_id = (uint32_t) value; return true; } static bool parse_log_level(const char *s, enum sc_log_level *log_level) { if (!strcmp(s, "verbose")) { *log_level = SC_LOG_LEVEL_VERBOSE; return true; } if (!strcmp(s, "debug")) { *log_level = SC_LOG_LEVEL_DEBUG; return true; } if (!strcmp(s, "info")) { *log_level = SC_LOG_LEVEL_INFO; return true; } if (!strcmp(s, "warn")) { *log_level = SC_LOG_LEVEL_WARN; return true; } if (!strcmp(s, "error")) { *log_level = SC_LOG_LEVEL_ERROR; return true; } LOGE("Could not parse log level: %s", s); return false; } // item is a list of mod keys separated by '+' (e.g. "lctrl+lalt") // returns a bitwise-or of SC_MOD_* constants (or 0 on error) static unsigned parse_shortcut_mods_item(const char *item, size_t len) { unsigned mod = 0; for (;;) { char *plus = strchr(item, '+'); // strchr() does not consider the "len" parameter, to it could find an // occurrence too far in the string (there is no strnchr()) bool has_plus = plus && plus < item + len; assert(!has_plus || plus > item); size_t key_len = has_plus ? (size_t) (plus - item) : len; #define STREQ(literal, s, len) \ ((sizeof(literal)-1 == len) && !memcmp(literal, s, len)) if (STREQ("lctrl", item, key_len)) { mod |= SC_MOD_LCTRL; } else if (STREQ("rctrl", item, key_len)) { mod |= SC_MOD_RCTRL; } else if (STREQ("lalt", item, key_len)) { mod |= SC_MOD_LALT; } else if (STREQ("ralt", item, key_len)) { mod |= SC_MOD_RALT; } else if (STREQ("lsuper", item, key_len)) { mod |= SC_MOD_LSUPER; } else if (STREQ("rsuper", item, key_len)) { mod |= SC_MOD_RSUPER; } else { LOGE("Unknown modifier key: %.*s " "(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)", (int) key_len, item); return 0; } #undef STREQ if (!has_plus) { break; } item = plus + 1; assert(len >= key_len + 1); len -= key_len + 1; } return mod; } static bool parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { unsigned count = 0; unsigned current = 0; // LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper" for (;;) { char *comma = strchr(s, ','); if (comma && count == SC_MAX_SHORTCUT_MODS - 1) { assert(count < SC_MAX_SHORTCUT_MODS); LOGW("Too many shortcut modifiers alternatives"); return false; } assert(!comma || comma > s); size_t limit = comma ? (size_t) (comma - s) : strlen(s); unsigned mod = parse_shortcut_mods_item(s, limit); if (!mod) { LOGE("Invalid modifier keys: %.*s", (int) limit, s); return false; } mods->data[current++] = mod; ++count; if (!comma) { break; } s = comma + 1; } mods->count = count; return true; } #ifdef SC_TEST // expose the function to unit-tests bool sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) { return parse_shortcut_mods(s, mods); } #endif static bool parse_record_format(const char *optarg, enum sc_record_format *format) { if (!strcmp(optarg, "mp4")) { *format = SC_RECORD_FORMAT_MP4; return true; } if (!strcmp(optarg, "mkv")) { *format = SC_RECORD_FORMAT_MKV; return true; } LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); return false; } 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; } *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) { return 0; } const char *ext = &filename[len - 4]; if (!strcmp(ext, ".mp4")) { return SC_RECORD_FORMAT_MP4; } if (!strcmp(ext, ".mkv")) { return SC_RECORD_FORMAT_MKV; } return 0; } static bool parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], const char *optstring, const struct option *longopts) { struct scrcpy_options *opts = &args->opts; optind = 0; // reset to start from the first argument in tests int c; while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) { switch (c) { case 'b': if (!parse_bit_rate(optarg, &opts->bit_rate)) { return false; } break; 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': args->help = true; break; case 'K': opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; break; case OPT_MAX_FPS: if (!parse_max_fps(optarg, &opts->max_fps)) { return false; } break; case 'm': if (!parse_max_size(optarg, &opts->max_size)) { return false; } break; 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 'N': opts->display = false; break; case 'p': if (!parse_port_range(optarg, &opts->port_range)) { return false; } break; case 'r': opts->record_filename = optarg; break; case 's': opts->serial = optarg; break; case 'S': opts->turn_screen_off = true; break; case 't': opts->show_touches = true; break; case OPT_ALWAYS_ON_TOP: opts->always_on_top = true; break; case 'v': args->version = true; break; case 'V': if (!parse_log_level(optarg, &opts->log_level)) { return false; } break; case 'w': opts->stay_awake = true; break; case OPT_RENDER_EXPIRED_FRAMES: LOGW("Option --render-expired-frames has been removed. This " "flag has been ignored."); break; case OPT_WINDOW_TITLE: opts->window_title = optarg; break; case OPT_WINDOW_X: if (!parse_window_position(optarg, &opts->window_x)) { return false; } break; case OPT_WINDOW_Y: if (!parse_window_position(optarg, &opts->window_y)) { return false; } break; case OPT_WINDOW_WIDTH: if (!parse_window_dimension(optarg, &opts->window_width)) { return false; } break; case OPT_WINDOW_HEIGHT: if (!parse_window_dimension(optarg, &opts->window_height)) { return false; } break; case OPT_WINDOW_BORDERLESS: opts->window_borderless = true; break; case OPT_PUSH_TARGET: opts->push_target = optarg; break; case OPT_PREFER_TEXT: 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)) { return false; } break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; case OPT_NO_MIPMAPS: opts->mipmaps = false; break; case OPT_NO_KEY_REPEAT: opts->forward_key_repeat = false; break; case OPT_CODEC_OPTIONS: opts->codec_options = optarg; break; case OPT_ENCODER_NAME: opts->encoder_name = optarg; break; case OPT_FORCE_ADB_FORWARD: opts->force_adb_forward = true; break; case OPT_DISABLE_SCREENSAVER: opts->disable_screensaver = true; break; case OPT_SHORTCUT_MOD: if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) { return false; } break; case OPT_FORWARD_ALL_CLICKS: opts->forward_all_clicks = true; break; case OPT_LEGACY_PASTE: opts->legacy_paste = true; break; case OPT_POWER_OFF_ON_CLOSE: opts->power_off_on_close = true; break; case OPT_DISPLAY_BUFFER: if (!parse_buffering_time(optarg, &opts->display_buffer)) { return false; } break; case OPT_NO_CLIPBOARD_AUTOSYNC: opts->clipboard_autosync = false; break; case OPT_TCPIP: opts->tcpip = true; opts->tcpip_dst = optarg; break; #ifdef HAVE_V4L2 case OPT_V4L2_SINK: opts->v4l2_device = optarg; break; case OPT_V4L2_BUFFER: if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { return false; } break; #endif default: // getopt prints the error message on stderr 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); if (opts->serial && opts->tcpip_dst) { LOGE("Incompatible options: -s/--serial and --tcpip with an argument"); return false; } #ifdef HAVE_V4L2 if (!opts->display && !opts->record_filename && !opts->v4l2_device) { LOGE("-N/--no-display requires either screen recording (-r/--record)" " or sink to v4l2loopback device (--v4l2-sink)"); return false; } if (opts->v4l2_device && opts->lock_video_orientation == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " "See --lock-video-orientation."); opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; } 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 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->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_format) { LOGE("No format specified for \"%s\" " "(try with --record-format=mkv)", opts->record_filename); 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->control && opts->stay_awake) { LOGE("Could not request to stay awake if control is disabled"); return false; } return true; } bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { struct sc_getopt_adapter adapter; if (!sc_getopt_adapter_init(&adapter)) { LOGW("Could not create getopt adapter"); return false; } bool ret = parse_args_with_getopt(args, argc, argv, adapter.optstring, adapter.longopts); sc_getopt_adapter_destroy(&adapter); return ret; } scrcpy-1.21/app/src/cli.h000066400000000000000000000006561415124136000152330ustar00rootroot00000000000000#ifndef SCRCPY_CLI_H #define SCRCPY_CLI_H #include "common.h" #include #include "options.h" struct scrcpy_cli_args { struct scrcpy_options opts; bool help; bool version; }; void scrcpy_print_usage(const char *arg0); bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]); #ifdef SC_TEST bool sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods); #endif #endif scrcpy-1.21/app/src/clock.c000066400000000000000000000070301415124136000155430ustar00rootroot00000000000000#include "clock.h" #include "util/log.h" #define SC_CLOCK_NDEBUG // comment to debug void sc_clock_init(struct sc_clock *clock) { clock->count = 0; clock->head = 0; clock->left_sum.system = 0; clock->left_sum.stream = 0; clock->right_sum.system = 0; clock->right_sum.stream = 0; } // Estimate the affine function f(stream) = slope * stream + offset static void sc_clock_estimate(struct sc_clock *clock, double *out_slope, sc_tick *out_offset) { assert(clock->count > 1); // two points are necessary struct sc_clock_point left_avg = { .system = clock->left_sum.system / (clock->count / 2), .stream = clock->left_sum.stream / (clock->count / 2), }; struct sc_clock_point right_avg = { .system = clock->right_sum.system / ((clock->count + 1) / 2), .stream = clock->right_sum.stream / ((clock->count + 1) / 2), }; double slope = (double) (right_avg.system - left_avg.system) / (right_avg.stream - left_avg.stream); if (clock->count < SC_CLOCK_RANGE) { /* The first frames are typically received and decoded with more delay * than the others, causing a wrong slope estimation on start. To * compensate, assume an initial slope of 1, then progressively use the * estimated slope. */ slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count)) / SC_CLOCK_RANGE; } struct sc_clock_point global_avg = { .system = (clock->left_sum.system + clock->right_sum.system) / clock->count, .stream = (clock->left_sum.stream + clock->right_sum.stream) / clock->count, }; sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); *out_slope = slope; *out_offset = offset; } void sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { struct sc_clock_point *point = &clock->points[clock->head]; if (clock->count == SC_CLOCK_RANGE || clock->count & 1) { // One point passes from the right sum to the left sum unsigned mid; if (clock->count == SC_CLOCK_RANGE) { mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE; } else { // Only for the first frames mid = clock->count / 2; } struct sc_clock_point *mid_point = &clock->points[mid]; clock->left_sum.system += mid_point->system; clock->left_sum.stream += mid_point->stream; clock->right_sum.system -= mid_point->system; clock->right_sum.stream -= mid_point->stream; } if (clock->count == SC_CLOCK_RANGE) { // The current point overwrites the previous value in the circular // array, update the left sum accordingly clock->left_sum.system -= point->system; clock->left_sum.stream -= point->stream; } else { ++clock->count; } point->system = system; point->stream = stream; clock->right_sum.system += system; clock->right_sum.stream += stream; clock->head = (clock->head + 1) % SC_CLOCK_RANGE; if (clock->count > 1) { // Update estimation sc_clock_estimate(clock, &clock->slope, &clock->offset); #ifndef SC_CLOCK_NDEBUG LOGD("Clock estimation: %g * pts + %" PRItick, clock->slope, clock->offset); #endif } } sc_tick sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { assert(clock->count > 1); // sc_clock_update() must have been called return (sc_tick) (stream * clock->slope) + clock->offset; } scrcpy-1.21/app/src/clock.h000066400000000000000000000035631415124136000155570ustar00rootroot00000000000000#ifndef SC_CLOCK_H #define SC_CLOCK_H #include "common.h" #include #include "util/tick.h" #define SC_CLOCK_RANGE 32 static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even"); struct sc_clock_point { sc_tick system; sc_tick stream; }; /** * The clock aims to estimate the affine relation between the stream (device) * time and the system time: * * f(stream) = slope * stream + offset * * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps * of a frame expressed both in stream time and system time) in a circular * array. * * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two * sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average * point"). The slope of the estimated affine function is that of the line * passing through these two points. * * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE * points. The resulting affine function passes by this centroid. * * With a circular array, the rolling sums (and average) are quick to compute. * In practice, the estimation is stable and the evolution is smooth. */ struct sc_clock { // Circular array struct sc_clock_point points[SC_CLOCK_RANGE]; // Number of points in the array (count <= SC_CLOCK_RANGE) unsigned count; // Index of the next point to write unsigned head; // Sum of the first count/2 points struct sc_clock_point left_sum; // Sum of the last (count+1)/2 points struct sc_clock_point right_sum; // Estimated slope and offset // (computed on sc_clock_update(), used by sc_clock_to_system_time()) double slope; sc_tick offset; }; void sc_clock_init(struct sc_clock *clock); void sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream); sc_tick sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream); #endif scrcpy-1.21/app/src/common.h000066400000000000000000000004701415124136000157460ustar00rootroot00000000000000#ifndef COMMON_H #define 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 container_of(ptr, type, member) \ ((type *) (((char *) (ptr)) - offsetof(type, member))) #endif scrcpy-1.21/app/src/compat.c000066400000000000000000000016651415124136000157430ustar00rootroot00000000000000#include "compat.h" #include "config.h" #include #include #include #include #include #ifndef HAVE_STRDUP char *strdup(const char *s) { size_t size = strlen(s) + 1; char *dup = malloc(size); if (dup) { memcpy(dup, s, size); } return dup; } #endif #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 scrcpy-1.21/app/src/compat.h000066400000000000000000000037661415124136000157540ustar00rootroot00000000000000#ifndef COMPAT_H #define COMPAT_H #include "config.h" #include #include #ifndef __WIN32 # define PRIu64_ PRIu64 #else # define PRIu64_ "I64u" // Windows... #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(), // av_register_all(), av_iformat_next(), av_oformat_next(). // Add av_demuxer_iterate(), and av_muxer_iterate(). #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100) # define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API #else # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL #endif // In ffmpeg/doc/APIchanges: // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h // Deprecate AVFormatContext filename field which had limited length, use the // new dynamically allocated url field instead. // // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h // Add url field to AVFormatContext and add ff_format_set_url helper function. #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100) # define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL #endif #if SDL_VERSION_ATLEAST(2, 0, 5) // # define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH // # define SCRCPY_SDL_HAS_GET_DISPLAY_USABLE_BOUNDS // # define SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP #endif #if SDL_VERSION_ATLEAST(2, 0, 6) // # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS #endif #if SDL_VERSION_ATLEAST(2, 0, 8) // # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR #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 #endif scrcpy-1.21/app/src/control_msg.c000066400000000000000000000215431415124136000170030ustar00rootroot00000000000000#include "control_msg.h" #include #include #include #include #include "util/buffer_util.h" #include "util/log.h" #include "util/str.h" /** * Map an enum value to a string based on an array, without crashing on an * out-of-bounds index. */ #define ENUM_TO_LABEL(labels, value) \ ((size_t) (value) < ARRAY_LEN(labels) ? labels[value] : "???") #define KEYEVENT_ACTION_LABEL(value) \ ENUM_TO_LABEL(android_keyevent_action_labels, value) #define MOTIONEVENT_ACTION_LABEL(value) \ ENUM_TO_LABEL(android_motionevent_action_labels, value) #define SCREEN_POWER_MODE_LABEL(value) \ ENUM_TO_LABEL(screen_power_mode_labels, value) static const char *const android_keyevent_action_labels[] = { "down", "up", "multi", }; static const char *const android_motionevent_action_labels[] = { "down", "up", "move", "cancel", "outside", "ponter-down", "pointer-up", "hover-move", "scroll", "hover-enter" "hover-exit", "btn-press", "btn-release", }; static const char *const screen_power_mode_labels[] = { "off", "doze", "normal", "doze-suspend", "suspend", }; static const char *const copy_key_labels[] = { "none", "copy", "cut", }; static void write_position(uint8_t *buf, const struct sc_position *position) { buffer_write32be(&buf[0], position->point.x); buffer_write32be(&buf[4], position->point.y); buffer_write16be(&buf[8], position->screen_size.width); buffer_write16be(&buf[10], position->screen_size.height); } // write length (2 bytes) + string (non nul-terminated) static size_t write_string(const char *utf8, size_t max_len, unsigned char *buf) { size_t len = sc_str_utf8_truncation_index(utf8, max_len); buffer_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) { buf[0] = msg->type; switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: buf[1] = msg->inject_keycode.action; buffer_write32be(&buf[2], msg->inject_keycode.keycode); buffer_write32be(&buf[6], msg->inject_keycode.repeat); buffer_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(msg->inject_text.text, CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; buffer_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: 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); return 21; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; case CONTROL_MSG_TYPE_GET_CLIPBOARD: buf[1] = msg->get_clipboard.copy_key; return 2; case CONTROL_MSG_TYPE_SET_CLIPBOARD: { buffer_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[10]); return 10 + len; } case 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_ROTATE_DEVICE: // no additional data return 1; default: LOGW("Unknown message type: %u", (unsigned) msg->type); return 0; } } void control_msg_log(const struct control_msg *msg) { #define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_KEYCODE: LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action), (int) msg->inject_keycode.keycode, msg->inject_keycode.repeat, (long) msg->inject_keycode.metastate); break; case CONTROL_MSG_TYPE_INJECT_TEXT: LOG_CMSG("text \"%s\"", msg->inject_text.text); break; case CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { int action = msg->inject_touch_event.action & AMOTION_EVENT_ACTION_MASK; uint64_t id = msg->inject_touch_event.pointer_id; if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%g buttons=%06lx", id == POINTER_ID_MOUSE ? "mouse" : "vfinger", MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, (long) msg->inject_touch_event.buttons); } else { // numeric pointer id LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%g buttons=%06lx", id, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, (long) msg->inject_touch_event.buttons); } break; } case CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32 " vscroll=%" PRIi32, msg->inject_scroll_event.position.point.x, msg->inject_scroll_event.position.point.y, msg->inject_scroll_event.hscroll, msg->inject_scroll_event.vscroll); break; case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: LOG_CMSG("back-or-screen-on %s", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); break; case CONTROL_MSG_TYPE_GET_CLIPBOARD: LOG_CMSG("get clipboard copy_key=%s", copy_key_labels[msg->get_clipboard.copy_key]); break; case 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: LOG_CMSG("power mode %s", SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); break; case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); break; case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: LOG_CMSG("expand settings panel"); break; case CONTROL_MSG_TYPE_COLLAPSE_PANELS: LOG_CMSG("collapse panels"); break; case CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; } } void control_msg_destroy(struct control_msg *msg) { switch (msg->type) { case CONTROL_MSG_TYPE_INJECT_TEXT: free(msg->inject_text.text); break; case CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; default: // do nothing break; } } scrcpy-1.21/app/src/control_msg.h000066400000000000000000000056231415124136000170110ustar00rootroot00000000000000#ifndef CONTROLMSG_H #define CONTROLMSG_H #include "common.h" #include #include #include #include "android/input.h" #include "android/keycodes.h" #include "coords.h" #define 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 POINTER_ID_MOUSE UINT64_C(-1) #define POINTER_ID_VIRTUAL_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, }; enum screen_power_mode { // see SCREEN_POWER_MODE_OFF = 0, SCREEN_POWER_MODE_NORMAL = 2, }; enum get_clipboard_copy_key { GET_CLIPBOARD_COPY_KEY_NONE, GET_CLIPBOARD_COPY_KEY_COPY, GET_CLIPBOARD_COPY_KEY_CUT, }; struct control_msg { enum control_msg_type type; union { struct { enum android_keyevent_action action; enum android_keycode keycode; uint32_t repeat; enum android_metastate metastate; } inject_keycode; struct { char *text; // owned, to be freed by free() } inject_text; struct { enum android_motionevent_action action; enum android_motionevent_buttons buttons; uint64_t pointer_id; struct sc_position position; float pressure; } inject_touch_event; struct { struct sc_position position; int32_t hscroll; int32_t vscroll; } 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 get_clipboard_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; } set_screen_power_mode; }; }; // buf size must be at least CONTROL_MSG_MAX_SIZE // return the number of bytes written size_t control_msg_serialize(const struct control_msg *msg, unsigned char *buf); void control_msg_log(const struct control_msg *msg); void control_msg_destroy(struct control_msg *msg); #endif scrcpy-1.21/app/src/controller.c000066400000000000000000000071461415124136000166430ustar00rootroot00000000000000#include "controller.h" #include #include "util/log.h" bool controller_init(struct controller *controller, sc_socket control_socket, struct sc_acksync *acksync) { cbuf_init(&controller->queue); bool ok = receiver_init(&controller->receiver, control_socket, acksync); if (!ok) { return false; } ok = sc_mutex_init(&controller->mutex); if (!ok) { receiver_destroy(&controller->receiver); return false; } ok = sc_cond_init(&controller->msg_cond); if (!ok) { receiver_destroy(&controller->receiver); sc_mutex_destroy(&controller->mutex); return false; } controller->control_socket = control_socket; controller->stopped = false; return true; } void controller_destroy(struct 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); } receiver_destroy(&controller->receiver); } bool controller_push_msg(struct controller *controller, const struct control_msg *msg) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { control_msg_log(msg); } sc_mutex_lock(&controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); bool res = cbuf_push(&controller->queue, *msg); if (was_empty) { sc_cond_signal(&controller->msg_cond); } sc_mutex_unlock(&controller->mutex); return res; } 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); if (!length) { return false; } 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; for (;;) { sc_mutex_lock(&controller->mutex); while (!controller->stopped && cbuf_is_empty(&controller->queue)) { sc_cond_wait(&controller->msg_cond, &controller->mutex); } if (controller->stopped) { // stop immediately, do not process further msgs sc_mutex_unlock(&controller->mutex); break; } struct control_msg msg; bool non_empty = cbuf_take(&controller->queue, &msg); assert(non_empty); (void) non_empty; sc_mutex_unlock(&controller->mutex); bool ok = process_msg(controller, &msg); control_msg_destroy(&msg); if (!ok) { LOGD("Could not write msg to socket"); break; } } return 0; } bool controller_start(struct controller *controller) { LOGD("Starting controller thread"); bool ok = sc_thread_create(&controller->thread, run_controller, "controller", controller); if (!ok) { LOGC("Could not start controller thread"); return false; } if (!receiver_start(&controller->receiver)) { controller_stop(controller); sc_thread_join(&controller->thread, NULL); return false; } return true; } void controller_stop(struct controller *controller) { sc_mutex_lock(&controller->mutex); controller->stopped = true; sc_cond_signal(&controller->msg_cond); sc_mutex_unlock(&controller->mutex); } void controller_join(struct controller *controller) { sc_thread_join(&controller->thread, NULL); receiver_join(&controller->receiver); } scrcpy-1.21/app/src/controller.h000066400000000000000000000016641415124136000166470ustar00rootroot00000000000000#ifndef CONTROLLER_H #define CONTROLLER_H #include "common.h" #include #include "control_msg.h" #include "receiver.h" #include "util/acksync.h" #include "util/cbuf.h" #include "util/net.h" #include "util/thread.h" struct control_msg_queue CBUF(struct control_msg, 64); struct 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; }; bool controller_init(struct controller *controller, sc_socket control_socket, struct sc_acksync *acksync); void controller_destroy(struct controller *controller); bool controller_start(struct controller *controller); void controller_stop(struct controller *controller); void controller_join(struct controller *controller); bool controller_push_msg(struct controller *controller, const struct control_msg *msg); #endif scrcpy-1.21/app/src/coords.h000066400000000000000000000006621415124136000157520ustar00rootroot00000000000000#ifndef SC_COORDS #define SC_COORDS #include struct sc_size { uint16_t width; uint16_t height; }; struct sc_point { int32_t x; int32_t y; }; 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 sc_size screen_size; struct sc_point point; }; #endif scrcpy-1.21/app/src/decoder.c000066400000000000000000000104231415124136000160550ustar00rootroot00000000000000#include "decoder.h" #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; } static bool decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); if (!decoder->codec_ctx) { LOG_OOM(); return false; } if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) { LOGE("Could not open codec"); avcodec_free_context(&decoder->codec_ctx); return false; } decoder->frame = av_frame_alloc(); if (!decoder->frame) { LOG_OOM(); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); return false; } if (!decoder_open_sinks(decoder)) { LOGE("Could not open decoder sinks"); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); return false; } return true; } static void decoder_close(struct decoder *decoder) { decoder_close_sinks(decoder); av_frame_free(&decoder->frame); avcodec_close(decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx); } 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) { bool is_config = packet->pts == AV_NOPTS_VALUE; if (is_config) { // nothing to do return true; } int ret = avcodec_send_packet(decoder->codec_ctx, packet); if (ret < 0 && ret != AVERROR(EAGAIN)) { LOGE("Could not send video packet: %d", ret); return false; } ret = avcodec_receive_frame(decoder->codec_ctx, decoder->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; av_frame_unref(decoder->frame); } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive video frame: %d", ret); 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); } static void decoder_packet_sink_close(struct sc_packet_sink *sink) { struct decoder *decoder = DOWNCAST(sink); decoder_close(decoder); } static bool decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { struct decoder *decoder = DOWNCAST(sink); return decoder_push(decoder, packet); } void decoder_init(struct decoder *decoder) { decoder->sink_count = 0; static const struct sc_packet_sink_ops ops = { .open = decoder_packet_sink_open, .close = decoder_packet_sink_close, .push = decoder_packet_sink_push, }; decoder->packet_sink.ops = &ops; } void decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink) { assert(decoder->sink_count < DECODER_MAX_SINKS); assert(sink); assert(sink->ops); decoder->sinks[decoder->sink_count++] = sink; } scrcpy-1.21/app/src/decoder.h000066400000000000000000000010031415124136000160540ustar00rootroot00000000000000#ifndef DECODER_H #define DECODER_H #include "common.h" #include "trait/packet_sink.h" #include #include #define DECODER_MAX_SINKS 2 struct decoder { struct sc_packet_sink packet_sink; // packet sink trait struct sc_frame_sink *sinks[DECODER_MAX_SINKS]; unsigned sink_count; AVCodecContext *codec_ctx; AVFrame *frame; }; void decoder_init(struct decoder *decoder); void decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink); #endif scrcpy-1.21/app/src/device_msg.c000066400000000000000000000026761415124136000165700ustar00rootroot00000000000000#include "device_msg.h" #include #include #include #include "util/buffer_util.h" #include "util/log.h" ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, struct device_msg *msg) { if (len < 5) { // at least type + empty string length return 0; // not available } msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { size_t clipboard_len = buffer_read32be(&buf[1]); if (clipboard_len > len - 5) { return 0; // not available } char *text = malloc(clipboard_len + 1); if (!text) { LOG_OOM(); return -1; } if (clipboard_len) { memcpy(text, &buf[5], clipboard_len); } text[clipboard_len] = '\0'; msg->clipboard.text = text; return 5 + clipboard_len; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { uint64_t sequence = buffer_read64be(&buf[1]); msg->ack_clipboard.sequence = sequence; return 9; } default: LOGW("Unknown device message type: %d", (int) msg->type); return -1; // error, we cannot recover } } void device_msg_destroy(struct device_msg *msg) { if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { free(msg->clipboard.text); } } scrcpy-1.21/app/src/device_msg.h000066400000000000000000000015231415124136000165630ustar00rootroot00000000000000#ifndef DEVICEMSG_H #define DEVICEMSG_H #include "common.h" #include #include #include #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) enum device_msg_type { DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD, }; struct device_msg { enum device_msg_type type; union { struct { char *text; // owned, to be freed by free() } clipboard; struct { uint64_t sequence; } ack_clipboard; }; }; // 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); void device_msg_destroy(struct device_msg *msg); #endif scrcpy-1.21/app/src/events.h000066400000000000000000000003471415124136000157650ustar00rootroot00000000000000#define EVENT_NEW_FRAME SDL_USEREVENT #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1) #define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2) #define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3) scrcpy-1.21/app/src/file_handler.c000066400000000000000000000115761415124136000170760ustar00rootroot00000000000000#include "file_handler.h" #include #include #include "adb.h" #include "util/log.h" #include "util/process_intr.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) { assert(serial); 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; } ok = sc_intr_init(&file_handler->intr); if (!ok) { sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); return false; } file_handler->serial = strdup(serial); if (!file_handler->serial) { LOG_OOM(); sc_intr_destroy(&file_handler->intr); sc_cond_destroy(&file_handler->event_cond); sc_mutex_destroy(&file_handler->mutex); return false; } // lazy initialization file_handler->initialized = false; file_handler->stopped = false; 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); sc_intr_destroy(&file_handler->intr); free(file_handler->serial); struct file_handler_request req; while (cbuf_take(&file_handler->queue, &req)) { file_handler_request_destroy(&req); } } 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; struct sc_intr *intr = &file_handler->intr; const char *serial = file_handler->serial; assert(serial); const char *push_target = file_handler->push_target; assert(push_target); for (;;) { sc_mutex_lock(&file_handler->mutex); 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; sc_mutex_unlock(&file_handler->mutex); if (req.action == ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); bool ok = 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 = 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); } } 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); sc_intr_interrupt(&file_handler->intr); sc_mutex_unlock(&file_handler->mutex); } void file_handler_join(struct file_handler *file_handler) { sc_thread_join(&file_handler->thread, NULL); } scrcpy-1.21/app/src/file_handler.h000066400000000000000000000023311415124136000170700ustar00rootroot00000000000000#ifndef FILE_HANDLER_H #define FILE_HANDLER_H #include "common.h" #include #include "adb.h" #include "util/cbuf.h" #include "util/thread.h" #include "util/intr.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; struct file_handler_request_queue queue; struct sc_intr intr; }; 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 scrcpy-1.21/app/src/fps_counter.c000066400000000000000000000113421415124136000170000ustar00rootroot00000000000000#include "fps_counter.h" #include #include "util/log.h" #define FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool fps_counter_init(struct fps_counter *counter) { bool ok = sc_mutex_init(&counter->mutex); if (!ok) { return false; } ok = sc_cond_init(&counter->state_cond); if (!ok) { sc_mutex_destroy(&counter->mutex); return false; } counter->thread_started = false; atomic_init(&counter->started, 0); // no need to initialize the other fields, they are unused until started return true; } void fps_counter_destroy(struct fps_counter *counter) { sc_cond_destroy(&counter->state_cond); sc_mutex_destroy(&counter->mutex); } static inline bool is_started(struct fps_counter *counter) { return atomic_load_explicit(&counter->started, memory_order_acquire); } static inline void set_started(struct fps_counter *counter, bool started) { atomic_store_explicit(&counter->started, started, memory_order_release); } // must be called with mutex locked static void display_fps(struct fps_counter *counter) { unsigned rendered_per_second = counter->nr_rendered * SC_TICK_FREQ / FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); } else { LOGI("%u fps", rendered_per_second); } } // must be called with mutex locked static void check_interval_expired(struct fps_counter *counter, uint32_t now) { if (now < counter->next_timestamp) { return; } display_fps(counter); counter->nr_rendered = 0; 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; } static int run_fps_counter(void *data) { struct fps_counter *counter = data; sc_mutex_lock(&counter->mutex); while (!counter->interrupted) { while (!counter->interrupted && !is_started(counter)) { sc_cond_wait(&counter->state_cond, &counter->mutex); } while (!counter->interrupted && is_started(counter)) { sc_tick now = sc_tick_now(); check_interval_expired(counter, now); // ignore the reason (timeout or signaled), we just loop anyway sc_cond_timedwait(&counter->state_cond, &counter->mutex, counter->next_timestamp); } } sc_mutex_unlock(&counter->mutex); return 0; } bool fps_counter_start(struct fps_counter *counter) { sc_mutex_lock(&counter->mutex); counter->next_timestamp = sc_tick_now() + FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; sc_mutex_unlock(&counter->mutex); set_started(counter, true); sc_cond_signal(&counter->state_cond); // counter->thread_started and counter->thread are always accessed from the // same thread, no need to lock if (!counter->thread_started) { bool ok = sc_thread_create(&counter->thread, run_fps_counter, "fps counter", counter); if (!ok) { LOGE("Could not start FPS counter thread"); return false; } counter->thread_started = true; } return true; } void fps_counter_stop(struct fps_counter *counter) { set_started(counter, false); sc_cond_signal(&counter->state_cond); } bool fps_counter_is_started(struct fps_counter *counter) { return is_started(counter); } void fps_counter_interrupt(struct fps_counter *counter) { if (!counter->thread_started) { return; } sc_mutex_lock(&counter->mutex); counter->interrupted = true; sc_mutex_unlock(&counter->mutex); // wake up blocking wait sc_cond_signal(&counter->state_cond); } void fps_counter_join(struct fps_counter *counter) { if (counter->thread_started) { // interrupted must be set by the thread calling join(), so no need to // lock for the assertion assert(counter->interrupted); sc_thread_join(&counter->thread, NULL); } } void fps_counter_add_rendered_frame(struct fps_counter *counter) { if (!is_started(counter)) { return; } sc_mutex_lock(&counter->mutex); sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_rendered; sc_mutex_unlock(&counter->mutex); } void fps_counter_add_skipped_frame(struct fps_counter *counter) { if (!is_started(counter)) { return; } sc_mutex_lock(&counter->mutex); sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_skipped; sc_mutex_unlock(&counter->mutex); } scrcpy-1.21/app/src/fps_counter.h000066400000000000000000000022611415124136000170050ustar00rootroot00000000000000#ifndef FPSCOUNTER_H #define FPSCOUNTER_H #include "common.h" #include #include #include #include "util/thread.h" struct fps_counter { sc_thread thread; sc_mutex mutex; sc_cond state_cond; bool thread_started; // atomic so that we can check without locking the mutex // if the FPS counter is disabled, we don't want to lock unnecessarily atomic_bool started; // the following fields are protected by the mutex bool interrupted; unsigned nr_rendered; unsigned nr_skipped; sc_tick next_timestamp; }; bool fps_counter_init(struct fps_counter *counter); void fps_counter_destroy(struct fps_counter *counter); bool fps_counter_start(struct fps_counter *counter); void fps_counter_stop(struct fps_counter *counter); bool fps_counter_is_started(struct fps_counter *counter); // request to stop the thread (on quit) // must be called before fps_counter_join() void fps_counter_interrupt(struct fps_counter *counter); void fps_counter_join(struct fps_counter *counter); void fps_counter_add_rendered_frame(struct fps_counter *counter); void fps_counter_add_skipped_frame(struct fps_counter *counter); #endif scrcpy-1.21/app/src/frame_buffer.c000066400000000000000000000043131415124136000170740ustar00rootroot00000000000000#include "frame_buffer.h" #include #include #include #include "util/log.h" bool sc_frame_buffer_init(struct sc_frame_buffer *fb) { fb->pending_frame = av_frame_alloc(); if (!fb->pending_frame) { LOG_OOM(); return false; } fb->tmp_frame = av_frame_alloc(); if (!fb->tmp_frame) { LOG_OOM(); av_frame_free(&fb->pending_frame); return false; } bool ok = sc_mutex_init(&fb->mutex); if (!ok) { av_frame_free(&fb->pending_frame); av_frame_free(&fb->tmp_frame); return false; } // there is initially no frame, so consider it has already been consumed fb->pending_frame_consumed = true; return true; } void sc_frame_buffer_destroy(struct sc_frame_buffer *fb) { sc_mutex_destroy(&fb->mutex); av_frame_free(&fb->pending_frame); av_frame_free(&fb->tmp_frame); } static inline void swap_frames(AVFrame **lhs, AVFrame **rhs) { AVFrame *tmp = *lhs; *lhs = *rhs; *rhs = tmp; } bool sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, bool *previous_frame_skipped) { sc_mutex_lock(&fb->mutex); // Use a temporary frame to preserve pending_frame in case of error. // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. int r = av_frame_ref(fb->tmp_frame, frame); if (r) { LOGE("Could not ref frame: %d", r); return false; } // Now that av_frame_ref() succeeded, we can replace the previous // pending_frame swap_frames(&fb->pending_frame, &fb->tmp_frame); av_frame_unref(fb->tmp_frame); if (previous_frame_skipped) { *previous_frame_skipped = !fb->pending_frame_consumed; } fb->pending_frame_consumed = false; sc_mutex_unlock(&fb->mutex); return true; } void sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst) { sc_mutex_lock(&fb->mutex); assert(!fb->pending_frame_consumed); fb->pending_frame_consumed = true; av_frame_move_ref(dst, fb->pending_frame); // av_frame_move_ref() resets its source frame, so no need to call // av_frame_unref() sc_mutex_unlock(&fb->mutex); } scrcpy-1.21/app/src/frame_buffer.h000066400000000000000000000017331415124136000171040ustar00rootroot00000000000000#ifndef SC_FRAME_BUFFER_H #define SC_FRAME_BUFFER_H #include "common.h" #include #include "util/thread.h" // forward declarations typedef struct AVFrame AVFrame; /** * A frame buffer holds 1 pending frame, which is the last frame received from * the producer (typically, the decoder). * * If a pending frame has not been consumed when the producer pushes a new * frame, then it is lost. The intent is to always provide access to the very * last frame to minimize latency. */ struct sc_frame_buffer { AVFrame *pending_frame; AVFrame *tmp_frame; // To preserve the pending frame on error sc_mutex mutex; bool pending_frame_consumed; }; bool sc_frame_buffer_init(struct sc_frame_buffer *fb); void sc_frame_buffer_destroy(struct sc_frame_buffer *fb); bool sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, bool *skipped); void sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst); #endif scrcpy-1.21/app/src/hid_keyboard.c000066400000000000000000000245561415124136000171100ustar00rootroot00000000000000#include "hid_keyboard.h" #include #include #include "util/log.h" /** Downcast key processor to hid_keyboard */ #define DOWNCAST(KP) container_of(KP, struct sc_hid_keyboard, key_processor) #define HID_KEYBOARD_ACCESSORY_ID 1 #define HID_MODIFIER_NONE 0x00 #define HID_MODIFIER_LEFT_CONTROL (1 << 0) #define HID_MODIFIER_LEFT_SHIFT (1 << 1) #define HID_MODIFIER_LEFT_ALT (1 << 2) #define HID_MODIFIER_LEFT_GUI (1 << 3) #define HID_MODIFIER_RIGHT_CONTROL (1 << 4) #define HID_MODIFIER_RIGHT_SHIFT (1 << 5) #define HID_MODIFIER_RIGHT_ALT (1 << 6) #define HID_MODIFIER_RIGHT_GUI (1 << 7) #define HID_KEYBOARD_INDEX_MODIFIER 0 #define HID_KEYBOARD_INDEX_KEYS 2 // USB HID protocol says 6 keys in an event is the requirement for BIOS // keyboard support, though OS could support more keys via modifying the report // desc. 6 should be enough for scrcpy. #define HID_KEYBOARD_MAX_KEYS 6 #define HID_KEYBOARD_EVENT_SIZE (2 + HID_KEYBOARD_MAX_KEYS) #define HID_RESERVED 0x00 #define HID_ERROR_ROLL_OVER 0x01 /** * For HID over AOAv2, only report descriptor is needed. * * The specification is available here: * * * In particular, read: * - 6.2.2 Report Descriptor * - Appendix B.1 Protocol 1 (Keyboard) * - Appendix C: Keyboard Implementation * * Normally a basic HID keyboard uses 8 bytes: * Modifier Reserved Key Key Key Key Key Key * * You can dump your device's report descriptor with: * * sudo usbhid-dump -m vid:pid -e descriptor * * (change vid:pid' to your device's vendor ID and product ID). */ static const unsigned char keyboard_report_desc[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Keyboard) 0x09, 0x06, // Collection (Application) 0xA1, 0x01, // Usage Page (Key Codes) 0x05, 0x07, // Usage Minimum (224) 0x19, 0xE0, // Usage Maximum (231) 0x29, 0xE7, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (1) 0x25, 0x01, // Report Size (1) 0x75, 0x01, // Report Count (8) 0x95, 0x08, // Input (Data, Variable, Absolute): Modifier byte 0x81, 0x02, // Report Size (8) 0x75, 0x08, // Report Count (1) 0x95, 0x01, // Input (Constant): Reserved byte 0x81, 0x01, // Usage Page (LEDs) 0x05, 0x08, // Usage Minimum (1) 0x19, 0x01, // Usage Maximum (5) 0x29, 0x05, // Report Size (1) 0x75, 0x01, // Report Count (5) 0x95, 0x05, // Output (Data, Variable, Absolute): LED report 0x91, 0x02, // Report Size (3) 0x75, 0x03, // Report Count (1) 0x95, 0x01, // Output (Constant): LED report padding 0x91, 0x01, // Usage Page (Key Codes) 0x05, 0x07, // Usage Minimum (0) 0x19, 0x00, // Usage Maximum (101) 0x29, SC_HID_KEYBOARD_KEYS - 1, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum(101) 0x25, SC_HID_KEYBOARD_KEYS - 1, // Report Size (8) 0x75, 0x08, // Report Count (6) 0x95, HID_KEYBOARD_MAX_KEYS, // Input (Data, Array): Keys 0x81, 0x00, // End Collection 0xC0 }; static unsigned char sdl_keymod_to_hid_modifiers(SDL_Keymod mod) { unsigned char modifiers = HID_MODIFIER_NONE; if (mod & KMOD_LCTRL) { modifiers |= HID_MODIFIER_LEFT_CONTROL; } if (mod & KMOD_LSHIFT) { modifiers |= HID_MODIFIER_LEFT_SHIFT; } if (mod & KMOD_LALT) { modifiers |= HID_MODIFIER_LEFT_ALT; } if (mod & KMOD_LGUI) { modifiers |= HID_MODIFIER_LEFT_GUI; } if (mod & KMOD_RCTRL) { modifiers |= HID_MODIFIER_RIGHT_CONTROL; } if (mod & KMOD_RSHIFT) { modifiers |= HID_MODIFIER_RIGHT_SHIFT; } if (mod & KMOD_RALT) { modifiers |= HID_MODIFIER_RIGHT_ALT; } if (mod & KMOD_RGUI) { modifiers |= HID_MODIFIER_RIGHT_GUI; } return modifiers; } static bool sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE); if (!buffer) { LOG_OOM(); return false; } buffer[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; buffer[1] = HID_RESERVED; memset(&buffer[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, buffer, HID_KEYBOARD_EVENT_SIZE); return true; } static inline bool scancode_is_modifier(SDL_Scancode scancode) { return scancode >= SDL_SCANCODE_LCTRL && scancode <= SDL_SCANCODE_RGUI; } static bool convert_hid_keyboard_event(struct sc_hid_keyboard *kb, struct sc_hid_event *hid_event, const SDL_KeyboardEvent *event) { SDL_Scancode scancode = event->keysym.scancode; assert(scancode >= 0); // SDL also generates events when only modifiers are pressed, we cannot // ignore them totally, for example press 'a' first then press 'Control', // if we ignore 'Control' event, only 'a' is sent. if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) { // Scancode to ignore return false; } if (!sc_hid_keyboard_event_init(hid_event)) { LOGW("Could not initialize HID keyboard event"); return false; } unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->keysym.mod); if (scancode < SC_HID_KEYBOARD_KEYS) { // Pressed is true and released is false kb->keys[scancode] = (event->type == SDL_KEYDOWN); LOGV("keys[%02x] = %s", scancode, kb->keys[scancode] ? "true" : "false"); } hid_event->buffer[HID_KEYBOARD_INDEX_MODIFIER] = modifiers; unsigned char *keys_buffer = &hid_event->buffer[HID_KEYBOARD_INDEX_KEYS]; // Re-calculate pressed keys every time int keys_pressed_count = 0; for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { if (kb->keys[i]) { // USB HID protocol says that if keys exceeds report count, a // phantom state should be reported if (keys_pressed_count >= HID_KEYBOARD_MAX_KEYS) { // Pantom state: // - Modifiers // - Reserved // - ErrorRollOver * HID_MAX_KEYS memset(keys_buffer, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS); goto end; } keys_buffer[keys_pressed_count] = i; ++keys_pressed_count; } } end: LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", event->type == SDL_KEYDOWN ? "down" : "up", event->keysym.scancode, event->keysym.scancode, modifiers); return true; } static bool push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t sdl_mod) { bool capslock = sdl_mod & KMOD_CAPS; bool numlock = sdl_mod & KMOD_NUM; if (!capslock && !numlock) { // Nothing to do return true; } struct sc_hid_event hid_event; if (!sc_hid_keyboard_event_init(&hid_event)) { LOGW("Could not initialize HID keyboard event"); return false; } #define SC_SCANCODE_CAPSLOCK SDL_SCANCODE_CAPSLOCK #define SC_SCANCODE_NUMLOCK SDL_SCANCODE_NUMLOCKCLEAR unsigned i = 0; if (capslock) { hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; ++i; } if (numlock) { hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; ++i; } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could request HID event"); return false; } LOGD("HID keyboard state synchronized"); return true; } static void sc_key_processor_process_key(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, 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_hid_keyboard *kb = DOWNCAST(kp); struct sc_hid_event hid_event; // Not all keys are supported, just ignore unsupported keys if (convert_hid_keyboard_event(kb, &hid_event, event)) { if (!kb->mod_lock_synchronized) { // Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize // keyboard state if (push_mod_lock_state(kb, event->keysym.mod)) { kb->mod_lock_synchronized = true; } } if (ack_to_wait) { // 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. hid_event.ack_to_wait = ack_to_wait; } if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could request HID event"); } } } static void sc_key_processor_process_text(struct sc_key_processor *kp, const SDL_TextInputEvent *event) { (void) kp; (void) event; // Never forward text input via HID (all the keys are injected separately) } bool sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) { kb->aoa = aoa; bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID, keyboard_report_desc, ARRAY_LEN(keyboard_report_desc)); if (!ok) { LOGW("Register HID keyboard failed"); return false; } // Reset all states memset(kb->keys, false, SC_HID_KEYBOARD_KEYS); kb->mod_lock_synchronized = false; static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, .process_text = sc_key_processor_process_text, }; // 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.ops = &ops; return true; } void sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb) { // Unregister HID keyboard so the soft keyboard shows again on Android bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID); if (!ok) { LOGW("Could not unregister HID keyboard"); } } scrcpy-1.21/app/src/hid_keyboard.h000066400000000000000000000026231415124136000171040ustar00rootroot00000000000000#ifndef SC_HID_KEYBOARD_H #define SC_HID_KEYBOARD_H #include "common.h" #include #include "aoa_hid.h" #include "trait/key_processor.h" // See "SDL2/SDL_scancode.h". // Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB // HID protocol. // 0x65 is Application, typically AT-101 Keyboard ends here. #define SC_HID_KEYBOARD_KEYS 0x66 /** * HID keyboard events are sequence-based, every time keyboard state changes * it sends an array of currently pressed keys, the host is responsible for * compare events and determine which key becomes pressed and which key becomes * released. In order to convert SDL_KeyboardEvent to HID events, we first use * an array of keys to save each keys' state. And when a SDL_KeyboardEvent was * emitted, we updated our state, and then we use a loop to generate HID * events. The sequence of array elements is unimportant and when too much keys * pressed at the same time (more than report count), we should generate * phantom state. Don't forget that modifiers should be updated too, even for * phantom state. */ struct sc_hid_keyboard { struct sc_key_processor key_processor; // key processor trait struct sc_aoa *aoa; bool keys[SC_HID_KEYBOARD_KEYS]; bool mod_lock_synchronized; }; bool sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa); void sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb); #endif scrcpy-1.21/app/src/icon.c000066400000000000000000000175221415124136000154070ustar00rootroot00000000000000#include "icon.h" #include #include #include #include #include #include "config.h" #include "compat.h" #include "util/file.h" #include "util/log.h" #include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png" static char * get_icon_path(void) { #ifdef __WINDOWS__ const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); #else const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); #endif if (icon_path_env) { // if the envvar is set, use it #ifdef __WINDOWS__ char *icon_path = sc_str_from_wchars(icon_path_env); #else char *icon_path = strdup(icon_path_env); #endif if (!icon_path) { 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 image codec: %s", path); goto free_ctx; } if (avformat_find_stream_info(ctx, NULL) < 0) { LOGE("Could not find image stream info"); goto close_input; } int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (stream < 0 ) { LOGE("Could not find best image stream"); goto close_input; } AVCodecParameters *params = ctx->streams[stream]->codecpar; AVCodec *codec = avcodec_find_decoder(params->codec_id); if (!codec) { LOGE("Could not find image decoder"); goto close_input; } AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { 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() { 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); } scrcpy-1.21/app/src/icon.h000066400000000000000000000003361415124136000154070ustar00rootroot00000000000000#ifndef ICON_H #define ICON_H #include "common.h" #include #include #include SDL_Surface * scrcpy_icon_load(void); void scrcpy_icon_destroy(SDL_Surface *icon); #endif scrcpy-1.21/app/src/input_manager.c000066400000000000000000000550761415124136000173160ustar00rootroot00000000000000#include "input_manager.h" #include #include #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) { uint16_t sdl_mod = 0; if (mod & SC_MOD_LCTRL) { sdl_mod |= KMOD_LCTRL; } if (mod & SC_MOD_RCTRL) { sdl_mod |= KMOD_RCTRL; } if (mod & SC_MOD_LALT) { sdl_mod |= KMOD_LALT; } if (mod & SC_MOD_RALT) { sdl_mod |= KMOD_RALT; } if (mod & SC_MOD_LSUPER) { sdl_mod |= KMOD_LGUI; } if (mod & SC_MOD_RSUPER) { sdl_mod |= KMOD_RGUI; } return sdl_mod; } static bool is_shortcut_mod(struct input_manager *im, uint16_t sdl_mod) { // keep only the relevant modifier keys sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; assert(im->sdl_shortcut_mods.count); assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS); for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) { if (im->sdl_shortcut_mods.data[i] == sdl_mod) { return true; } } return false; } void input_manager_init(struct input_manager *im, struct controller *controller, struct screen *screen, struct sc_key_processor *kp, struct sc_mouse_processor *mp, const struct scrcpy_options *options) { assert(!options->control || (kp && kp->ops)); assert(!options->control || (mp && mp->ops)); im->controller = controller; im->screen = screen; im->kp = kp; im->mp = mp; im->control = options->control; im->forward_all_clicks = options->forward_all_clicks; im->legacy_paste = options->legacy_paste; im->clipboard_autosync = options->clipboard_autosync; const struct sc_shortcut_mods *shortcut_mods = &options->shortcut_mods; assert(shortcut_mods->count); assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); for (unsigned i = 0; i < shortcut_mods->count; ++i) { uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]); assert(sdl_mod); im->sdl_shortcut_mods.data[i] = sdl_mod; } im->sdl_shortcut_mods.count = shortcut_mods->count; im->vfinger_down = false; im->last_keycode = SDLK_UNKNOWN; im->last_mod = 0; im->key_repeat = 0; 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 DOWN event struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; msg.inject_keycode.repeat = 0; if (actions & ACTION_DOWN) { msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN; 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); } } } 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"); } static inline void action_power(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_POWER, actions, "POWER"); } static inline void action_volume_up(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_VOLUME_UP, actions, "VOLUME_UP"); } static inline void action_volume_down(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_VOLUME_DOWN, actions, "VOLUME_DOWN"); } static inline void action_menu(struct controller *controller, int actions) { send_keycode(controller, AKEYCODE_MENU, actions, "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; } } 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'"); } } } static void expand_notification_panel(struct controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; if (!controller_push_msg(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; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand settings panel'"); } } static void collapse_panels(struct controller *controller) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); } } static bool get_device_clipboard(struct controller *controller, enum get_clipboard_copy_key copy_key) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'get device clipboard'"); return false; } return true; } static bool set_device_clipboard(struct controller *controller, bool paste, uint64_t sequence) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); return false; } char *text_dup = strdup(text); SDL_free(text); if (!text_dup) { LOGW("Could not strdup input text"); return false; } struct control_msg msg; msg.type = 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)) { 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; msg.set_screen_power_mode.mode = mode; if (!controller_push_msg(controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } static void switch_fps_counter_state(struct fps_counter *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"); } else { if (fps_counter_start(fps_counter)) { LOGI("FPS counter started"); } else { LOGE("FPS counter starting failed"); } } } static void clipboard_paste(struct controller *controller) { char *text = SDL_GetClipboardText(); if (!text) { LOGW("Could not get clipboard text: %s", SDL_GetError()); return; } if (!*text) { // empty text SDL_free(text); return; } char *text_dup = strdup(text); SDL_free(text); if (!text_dup) { LOGW("Could not strdup input text"); return; } struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; if (!controller_push_msg(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; if (!controller_push_msg(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); } static void rotate_client_right(struct screen *screen) { unsigned new_rotation = (screen->rotation + 3) % 4; screen_set_rotation(screen, new_rotation); } static void input_manager_process_text_input(struct input_manager *im, const SDL_TextInputEvent *event) { if (is_shortcut_mod(im, SDL_GetModState())) { // A shortcut must never generate text events return; } im->kp->ops->process_text(im->kp, event); } static bool simulate_virtual_finger(struct input_manager *im, enum android_motionevent_action action, struct sc_point point) { bool up = action == AMOTION_EVENT_ACTION_UP; struct control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; msg.inject_touch_event.action = action; msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.buttons = 0; if (!controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'inject virtual finger event'"); return false; } return true; } static struct sc_point inverse_point(struct sc_point point, struct sc_size size) { point.x = size.width - point.x; point.y = size.height - point.y; return point; } static 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; SDL_Keycode keycode = event->keysym.sym; uint16_t mod = event->keysym.mod; bool down = event->type == SDL_KEYDOWN; bool ctrl = event->keysym.mod & KMOD_CTRL; bool shift = event->keysym.mod & KMOD_SHIFT; bool repeat = event->repeat; bool smod = is_shortcut_mod(im, mod); if (down && !repeat) { if (keycode == im->last_keycode && mod == im->last_mod) { ++im->key_repeat; } else { im->key_repeat = 0; im->last_keycode = keycode; im->last_mod = mod; } } // The shortcut modifier is pressed if (smod) { int action = down ? ACTION_DOWN : ACTION_UP; switch (keycode) { case SDLK_h: if (control && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: if (control && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: if (control && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: if (control && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: if (control && !shift && !repeat) { action_power(controller, 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); } return; case SDLK_DOWN: if (control && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: if (control && !shift) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: if (!shift && !repeat && down) { rotate_client_left(im->screen); } return; case SDLK_RIGHT: if (!shift && !repeat && down) { rotate_client_right(im->screen); } return; case SDLK_c: if (control && !shift && !repeat && down) { get_device_clipboard(controller, GET_CLIPBOARD_COPY_KEY_COPY); } return; case SDLK_x: if (control && !shift && !repeat && down) { get_device_clipboard(controller, GET_CLIPBOARD_COPY_KEY_CUT); } return; case SDLK_v: if (control && !repeat && down) { if (shift || im->legacy_paste) { // inject the text as input events clipboard_paste(controller); } else { // store the text in the device clipboard and paste, // without requesting an acknowledgment set_device_clipboard(controller, true, SC_SEQUENCE_INVALID); } } return; case SDLK_f: if (!shift && !repeat && down) { screen_switch_fullscreen(im->screen); } return; case SDLK_w: if (!shift && !repeat && down) { screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && !repeat && down) { screen_resize_to_pixel_perfect(im->screen); } return; case SDLK_i: if (!shift && !repeat && down) { switch_fps_counter_state(&im->screen->fps_counter); } return; case SDLK_n: if (control && !repeat && down) { if (shift) { collapse_panels(controller); } else if (im->key_repeat == 0) { expand_notification_panel(controller); } else { expand_settings_panel(controller); } } return; case SDLK_r: if (control && !shift && !repeat && down) { rotate_device(controller); } return; } return; } if (!control) { return; } 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); 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. bool ok = set_device_clipboard(controller, false, sequence); if (!ok) { LOGW("Clipboard could not be synchronized, Ctrl+v not injected"); return; } 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; } } im->kp->ops->process_key(im->kp, event, 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; } if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } im->mp->ops->process_mouse_motion(im->mp, event); if (im->vfinger_down) { struct sc_point mouse = screen_convert_window_to_frame_coords(im->screen, event->x, event->y); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } static void input_manager_process_touch(struct input_manager *im, const SDL_TouchFingerEvent *event) { im->mp->ops->process_touch(im->mp, event); } static void input_manager_process_mouse_button(struct input_manager *im, const SDL_MouseButtonEvent *event) { bool control = im->control; if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } bool down = event->type == SDL_MOUSEBUTTONDOWN; if (!im->forward_all_clicks) { int action = down ? ACTION_DOWN : ACTION_UP; if (control && event->button == SDL_BUTTON_X1) { action_app_switch(im->controller, action); return; } if (control && event->button == SDL_BUTTON_X2 && down) { if (event->clicks < 2) { expand_notification_panel(im->controller); } else { expand_settings_panel(im->controller); } return; } if (control && event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(im->controller, 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); 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); } return; } } // otherwise, send the click event to the device } if (!control) { return; } im->mp->ops->process_mouse_button(im->mp, event); // Pinch-to-zoom simulation. // // If Ctrl is hold when the left-click button is pressed, then // pinch-to-zoom mode is enabled: on every mouse event until the left-click // button is released, an additional "virtual finger" event is generated, // having a position inverted through the center of the screen. // // In other words, the center of the rotation/scaling is the center of the // screen. #define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) if ((down && !im->vfinger_down && CTRL_PRESSED) || (!down && im->vfinger_down)) { struct sc_point mouse = screen_convert_window_to_frame_coords(im->screen, event->x, event->y); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); enum android_motionevent_action action = down ? AMOTION_EVENT_ACTION_DOWN : AMOTION_EVENT_ACTION_UP; if (!simulate_virtual_finger(im, action, vfinger)) { return; } im->vfinger_down = down; } } static void input_manager_process_mouse_wheel(struct input_manager *im, const SDL_MouseWheelEvent *event) { im->mp->ops->process_mouse_wheel(im->mp, event); } bool input_manager_handle_event(struct input_manager *im, SDL_Event *event) { switch (event->type) { case SDL_TEXTINPUT: if (!im->control) { return true; } input_manager_process_text_input(im, &event->text); return true; case SDL_KEYDOWN: case SDL_KEYUP: // some key events do not interact with the device, so process the // event even if control is disabled input_manager_process_key(im, &event->key); return true; case SDL_MOUSEMOTION: if (!im->control) { break; } input_manager_process_mouse_motion(im, &event->motion); return true; case SDL_MOUSEWHEEL: if (!im->control) { break; } input_manager_process_mouse_wheel(im, &event->wheel); return true; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // some mouse events do not interact with the device, so process // the event even if control is disabled input_manager_process_mouse_button(im, &event->button); return true; case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: input_manager_process_touch(im, &event->tfinger); return true; } return false; } scrcpy-1.21/app/src/input_manager.h000066400000000000000000000025041415124136000173070ustar00rootroot00000000000000#ifndef INPUTMANAGER_H #define INPUTMANAGER_H #include "common.h" #include #include #include "controller.h" #include "fps_counter.h" #include "options.h" #include "screen.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" struct input_manager { struct controller *controller; struct screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; bool control; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; struct { unsigned data[SC_MAX_SHORTCUT_MODS]; unsigned count; } sdl_shortcut_mods; bool vfinger_down; // Tracks the number of identical consecutive shortcut key down events. // Not to be confused with event->repeat, which counts the number of // system-generated repeated key presses. unsigned key_repeat; SDL_Keycode last_keycode; uint16_t last_mod; uint64_t next_sequence; // used for request acknowledgements }; void input_manager_init(struct input_manager *im, struct controller *controller, struct screen *screen, struct sc_key_processor *kp, struct sc_mouse_processor *mp, const struct scrcpy_options *options); bool input_manager_handle_event(struct input_manager *im, SDL_Event *event); #endif scrcpy-1.21/app/src/keyboard_inject.c000066400000000000000000000267431415124136000176200ustar00rootroot00000000000000#include "keyboard_inject.h" #include #include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "util/intmap.h" #include "util/log.h" /** Downcast key processor to sc_keyboard_inject */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) static bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) { static const struct sc_intmap_entry actions[] = { {SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN}, {SDL_KEYUP, AKEY_EVENT_ACTION_UP}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); if (entry) { *to = entry->value; return true; } return false; } static bool convert_keycode(SDL_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[] = { {SDLK_RETURN, AKEYCODE_ENTER}, {SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, {SDLK_ESCAPE, AKEYCODE_ESCAPE}, {SDLK_BACKSPACE, AKEYCODE_DEL}, {SDLK_TAB, AKEYCODE_TAB}, {SDLK_PAGEUP, AKEYCODE_PAGE_UP}, {SDLK_DELETE, AKEYCODE_FORWARD_DEL}, {SDLK_HOME, AKEYCODE_MOVE_HOME}, {SDLK_END, AKEYCODE_MOVE_END}, {SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN}, {SDLK_RIGHT, AKEYCODE_DPAD_RIGHT}, {SDLK_LEFT, AKEYCODE_DPAD_LEFT}, {SDLK_DOWN, AKEYCODE_DPAD_DOWN}, {SDLK_UP, AKEYCODE_DPAD_UP}, {SDLK_LCTRL, AKEYCODE_CTRL_LEFT}, {SDLK_RCTRL, AKEYCODE_CTRL_RIGHT}, {SDLK_LSHIFT, AKEYCODE_SHIFT_LEFT}, {SDLK_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[] = { {SDLK_KP_0, AKEYCODE_INSERT}, {SDLK_KP_1, AKEYCODE_MOVE_END}, {SDLK_KP_2, AKEYCODE_DPAD_DOWN}, {SDLK_KP_3, AKEYCODE_PAGE_DOWN}, {SDLK_KP_4, AKEYCODE_DPAD_LEFT}, {SDLK_KP_6, AKEYCODE_DPAD_RIGHT}, {SDLK_KP_7, AKEYCODE_MOVE_HOME}, {SDLK_KP_8, AKEYCODE_DPAD_UP}, {SDLK_KP_9, AKEYCODE_PAGE_UP}, {SDLK_KP_PERIOD, AKEYCODE_FORWARD_DEL}, }; // Letters and space. // Used in non-text mode. static const struct sc_intmap_entry alphaspace_keys[] = { {SDLK_a, AKEYCODE_A}, {SDLK_b, AKEYCODE_B}, {SDLK_c, AKEYCODE_C}, {SDLK_d, AKEYCODE_D}, {SDLK_e, AKEYCODE_E}, {SDLK_f, AKEYCODE_F}, {SDLK_g, AKEYCODE_G}, {SDLK_h, AKEYCODE_H}, {SDLK_i, AKEYCODE_I}, {SDLK_j, AKEYCODE_J}, {SDLK_k, AKEYCODE_K}, {SDLK_l, AKEYCODE_L}, {SDLK_m, AKEYCODE_M}, {SDLK_n, AKEYCODE_N}, {SDLK_o, AKEYCODE_O}, {SDLK_p, AKEYCODE_P}, {SDLK_q, AKEYCODE_Q}, {SDLK_r, AKEYCODE_R}, {SDLK_s, AKEYCODE_S}, {SDLK_t, AKEYCODE_T}, {SDLK_u, AKEYCODE_U}, {SDLK_v, AKEYCODE_V}, {SDLK_w, AKEYCODE_W}, {SDLK_x, AKEYCODE_X}, {SDLK_y, AKEYCODE_Y}, {SDLK_z, AKEYCODE_Z}, {SDLK_SPACE, AKEYCODE_SPACE}, }; // Numbers and punctuation keys. // Used in raw mode only. static const struct sc_intmap_entry numbers_punct_keys[] = { {SDLK_HASH, AKEYCODE_POUND}, {SDLK_PERCENT, AKEYCODE_PERIOD}, {SDLK_QUOTE, AKEYCODE_APOSTROPHE}, {SDLK_ASTERISK, AKEYCODE_STAR}, {SDLK_PLUS, AKEYCODE_PLUS}, {SDLK_COMMA, AKEYCODE_COMMA}, {SDLK_MINUS, AKEYCODE_MINUS}, {SDLK_PERIOD, AKEYCODE_PERIOD}, {SDLK_SLASH, AKEYCODE_SLASH}, {SDLK_0, AKEYCODE_0}, {SDLK_1, AKEYCODE_1}, {SDLK_2, AKEYCODE_2}, {SDLK_3, AKEYCODE_3}, {SDLK_4, AKEYCODE_4}, {SDLK_5, AKEYCODE_5}, {SDLK_6, AKEYCODE_6}, {SDLK_7, AKEYCODE_7}, {SDLK_8, AKEYCODE_8}, {SDLK_9, AKEYCODE_9}, {SDLK_SEMICOLON, AKEYCODE_SEMICOLON}, {SDLK_EQUALS, AKEYCODE_EQUALS}, {SDLK_AT, AKEYCODE_AT}, {SDLK_LEFTBRACKET, AKEYCODE_LEFT_BRACKET}, {SDLK_BACKSLASH, AKEYCODE_BACKSLASH}, {SDLK_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET}, {SDLK_BACKQUOTE, AKEYCODE_GRAVE}, {SDLK_KP_1, AKEYCODE_NUMPAD_1}, {SDLK_KP_2, AKEYCODE_NUMPAD_2}, {SDLK_KP_3, AKEYCODE_NUMPAD_3}, {SDLK_KP_4, AKEYCODE_NUMPAD_4}, {SDLK_KP_5, AKEYCODE_NUMPAD_5}, {SDLK_KP_6, AKEYCODE_NUMPAD_6}, {SDLK_KP_7, AKEYCODE_NUMPAD_7}, {SDLK_KP_8, AKEYCODE_NUMPAD_8}, {SDLK_KP_9, AKEYCODE_NUMPAD_9}, {SDLK_KP_0, AKEYCODE_NUMPAD_0}, {SDLK_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE}, {SDLK_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY}, {SDLK_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT}, {SDLK_KP_PLUS, AKEYCODE_NUMPAD_ADD}, {SDLK_KP_PERIOD, AKEYCODE_NUMPAD_DOT}, {SDLK_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS}, {SDLK_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN}, {SDLK_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 & (KMOD_NUM | KMOD_SHIFT))) { // 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 & 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 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(SDL_Keymod mod) { enum android_metastate metastate = 0; if (mod & KMOD_LSHIFT) { metastate |= AMETA_SHIFT_LEFT_ON; } if (mod & KMOD_RSHIFT) { metastate |= AMETA_SHIFT_RIGHT_ON; } if (mod & KMOD_LCTRL) { metastate |= AMETA_CTRL_LEFT_ON; } if (mod & KMOD_RCTRL) { metastate |= AMETA_CTRL_RIGHT_ON; } if (mod & KMOD_LALT) { metastate |= AMETA_ALT_LEFT_ON; } if (mod & KMOD_RALT) { metastate |= AMETA_ALT_RIGHT_ON; } if (mod & KMOD_LGUI) { // Windows key metastate |= AMETA_META_LEFT_ON; } if (mod & KMOD_RGUI) { // Windows key metastate |= AMETA_META_RIGHT_ON; } if (mod & KMOD_NUM) { metastate |= AMETA_NUM_LOCK_ON; } if (mod & KMOD_CAPS) { metastate |= AMETA_CAPS_LOCK_ON; } if (mod & KMOD_MODE) { // Alt Gr // no mapping? } // fill the dependent fields return autocomplete_metastate(metastate); } static bool convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to, enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { to->type = CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode_action(from->type, &to->inject_keycode.action)) { return false; } uint16_t mod = from->keysym.mod; if (!convert_keycode(from->keysym.sym, &to->inject_keycode.keycode, mod, key_inject_mode)) { return false; } to->inject_keycode.repeat = repeat; to->inject_keycode.metastate = convert_meta_state(mod); return true; } static void sc_key_processor_process_key(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, 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_inject *ki = DOWNCAST(kp); if (event->repeat) { if (!ki->forward_key_repeat) { return; } ++ki->repeat; } else { ki->repeat = 0; } struct control_msg msg; if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { if (!controller_push_msg(ki->controller, &msg)) { LOGW("Could not request 'inject keycode'"); } } } static void sc_key_processor_process_text(struct sc_key_processor *kp, const SDL_TextInputEvent *event) { struct sc_keyboard_inject *ki = DOWNCAST(kp); if (ki->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { // Never inject text events return; } if (ki->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 control_msg msg; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = strdup(event->text); if (!msg.inject_text.text) { LOGW("Could not strdup input text"); return; } if (!controller_push_msg(ki->controller, &msg)) { free(msg.inject_text.text); LOGW("Could not request 'inject text'"); } } void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, struct controller *controller, const struct scrcpy_options *options) { ki->controller = controller; ki->key_inject_mode = options->key_inject_mode; ki->forward_key_repeat = options->forward_key_repeat; ki->repeat = 0; static const struct sc_key_processor_ops ops = { .process_key = sc_key_processor_process_key, .process_text = sc_key_processor_process_text, }; // Key injection and clipboard synchronization are serialized ki->key_processor.async_paste = false; ki->key_processor.ops = &ops; } scrcpy-1.21/app/src/keyboard_inject.h000066400000000000000000000013541415124136000176140ustar00rootroot00000000000000#ifndef SC_KEYBOARD_INJECT_H #define SC_KEYBOARD_INJECT_H #include "common.h" #include #include "controller.h" #include "options.h" #include "trait/key_processor.h" struct sc_keyboard_inject { struct sc_key_processor key_processor; // key processor trait struct controller *controller; // SDL reports repeated events as a boolean, but Android expects the actual // number of repetitions. This variable keeps track of the count. unsigned repeat; enum sc_key_inject_mode key_inject_mode; bool forward_key_repeat; }; void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, struct controller *controller, const struct scrcpy_options *options); #endif scrcpy-1.21/app/src/main.c000066400000000000000000000052041415124136000153750ustar00rootroot00000000000000#include "common.h" #include #include #include #include #ifdef HAVE_V4L2 # include #endif #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #include #include "cli.h" #include "options.h" #include "scrcpy.h" #include "util/log.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); #endif } int main(int argc, char *argv[]) { #ifdef __WINDOWS__ // 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, .help = false, .version = false, }; #ifndef NDEBUG args.opts.log_level = SC_LOG_LEVEL_DEBUG; #endif if (!scrcpy_parse_args(&args, argc, argv)) { return 1; } sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); return 0; } if (args.version) { print_version(); return 0; } #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL av_register_all(); #endif #ifdef HAVE_V4L2 if (args.opts.v4l2_device) { avdevice_register_all(); } #endif if (avformat_network_init()) { return 1; } int res = scrcpy(&args.opts) ? 0 : 1; avformat_network_deinit(); // ignore failure return res; } scrcpy-1.21/app/src/mouse_inject.c000066400000000000000000000157111415124136000171410ustar00rootroot00000000000000#include "mouse_inject.h" #include #include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_inject */ #define DOWNCAST(MP) container_of(MP, struct sc_mouse_inject, mouse_processor) static enum android_motionevent_buttons convert_mouse_buttons(uint32_t state) { enum android_motionevent_buttons buttons = 0; if (state & SDL_BUTTON_LMASK) { buttons |= AMOTION_EVENT_BUTTON_PRIMARY; } if (state & SDL_BUTTON_RMASK) { buttons |= AMOTION_EVENT_BUTTON_SECONDARY; } if (state & SDL_BUTTON_MMASK) { buttons |= AMOTION_EVENT_BUTTON_TERTIARY; } if (state & SDL_BUTTON_X1MASK) { buttons |= AMOTION_EVENT_BUTTON_BACK; } if (state & SDL_BUTTON_X2MASK) { buttons |= AMOTION_EVENT_BUTTON_FORWARD; } return buttons; } static bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) { static const struct sc_intmap_entry actions[] = { {SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN}, {SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); if (entry) { *to = entry->value; return true; } return false; } static bool convert_touch_action(SDL_EventType from, enum android_motionevent_action *to) { static const struct sc_intmap_entry actions[] = { {SDL_FINGERMOTION, AMOTION_EVENT_ACTION_MOVE}, {SDL_FINGERDOWN, AMOTION_EVENT_ACTION_DOWN}, {SDL_FINGERUP, AMOTION_EVENT_ACTION_UP}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(actions, from); if (entry) { *to = entry->value; return true; } return false; } static bool convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; to->inject_touch_event.action = AMOTION_EVENT_ACTION_MOVE; to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = 1.f; to->inject_touch_event.buttons = convert_mouse_buttons(from->state); return true; } static bool convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; if (!convert_touch_action(from->type, &to->inject_touch_event.action)) { return false; } to->inject_touch_event.pointer_id = from->fingerId; to->inject_touch_event.position.screen_size = screen->frame_size; int dw; int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); // SDL touch event coordinates are normalized in the range [0; 1] int32_t x = from->x * dw; int32_t y = from->y * dh; to->inject_touch_event.position.point = screen_convert_drawable_to_frame_coords(screen, x, y); to->inject_touch_event.pressure = from->pressure; to->inject_touch_event.buttons = 0; return true; } static bool convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen, struct control_msg *to) { to->type = CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; if (!convert_mouse_action(from->type, &to->inject_touch_event.action)) { return false; } to->inject_touch_event.pointer_id = POINTER_ID_MOUSE; to->inject_touch_event.position.screen_size = screen->frame_size; to->inject_touch_event.position.point = screen_convert_window_to_frame_coords(screen, from->x, from->y); to->inject_touch_event.pressure = from->type == SDL_MOUSEBUTTONDOWN ? 1.f : 0.f; to->inject_touch_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button)); return true; } static bool convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen, struct control_msg *to) { // mouse_x and mouse_y are expressed in pixels relative to the window int mouse_x; int mouse_y; SDL_GetMouseState(&mouse_x, &mouse_y); struct sc_position position = { .screen_size = screen->frame_size, .point = screen_convert_window_to_frame_coords(screen, mouse_x, mouse_y), }; to->type = CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT; to->inject_scroll_event.position = position; to->inject_scroll_event.hscroll = from->x; to->inject_scroll_event.vscroll = from->y; return true; } static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const SDL_MouseMotionEvent *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; if (!convert_mouse_motion(event, mi->screen, &msg)) { return; } if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } static void sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, const SDL_TouchFingerEvent *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; if (convert_touch(event, mi->screen, &msg)) { if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } } static void sc_mouse_processor_process_mouse_button(struct sc_mouse_processor *mp, const SDL_MouseButtonEvent *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; if (convert_mouse_button(event, mi->screen, &msg)) { if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse button event'"); } } } static void sc_mouse_processor_process_mouse_wheel(struct sc_mouse_processor *mp, const SDL_MouseWheelEvent *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct control_msg msg; if (convert_mouse_wheel(event, mi->screen, &msg)) { if (!controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse wheel event'"); } } } void sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller, struct screen *screen) { mi->controller = controller; mi->screen = screen; static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, .process_touch = sc_mouse_processor_process_touch, .process_mouse_button = sc_mouse_processor_process_mouse_button, .process_mouse_wheel = sc_mouse_processor_process_mouse_wheel, }; mi->mouse_processor.ops = &ops; } scrcpy-1.21/app/src/mouse_inject.h000066400000000000000000000007361415124136000171470ustar00rootroot00000000000000#ifndef SC_MOUSE_INJECT_H #define SC_MOUSE_INJECT_H #include "common.h" #include #include "controller.h" #include "screen.h" #include "trait/mouse_processor.h" struct sc_mouse_inject { struct sc_mouse_processor mouse_processor; // mouse processor trait struct controller *controller; struct screen *screen; }; void sc_mouse_inject_init(struct sc_mouse_inject *mi, struct controller *controller, struct screen *screen); #endif scrcpy-1.21/app/src/opengl.c000066400000000000000000000031461415124136000157400ustar00rootroot00000000000000#include "opengl.h" #include #include #include "SDL2/SDL.h" void sc_opengl_init(struct sc_opengl *gl) { gl->GetString = SDL_GL_GetProcAddress("glGetString"); assert(gl->GetString); gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf"); assert(gl->TexParameterf); gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri"); assert(gl->TexParameteri); // optional gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap"); const char *version = (const char *) gl->GetString(GL_VERSION); assert(version); gl->version = version; #define OPENGL_ES_PREFIX "OpenGL ES " /* starts with "OpenGL ES " */ gl->is_opengles = !strncmp(gl->version, OPENGL_ES_PREFIX, sizeof(OPENGL_ES_PREFIX) - 1); if (gl->is_opengles) { /* skip the prefix */ version += sizeof(PREFIX) - 1; } int r = sscanf(version, "%d.%d", &gl->version_major, &gl->version_minor); if (r != 2) { // failed to parse the version gl->version_major = 0; gl->version_minor = 0; } } bool sc_opengl_version_at_least(struct sc_opengl *gl, int minver_major, int minver_minor, int minver_es_major, int minver_es_minor) { if (gl->is_opengles) { return gl->version_major > minver_es_major || (gl->version_major == minver_es_major && gl->version_minor >= minver_es_minor); } return gl->version_major > minver_major || (gl->version_major == minver_major && gl->version_minor >= minver_minor); } scrcpy-1.21/app/src/opengl.h000066400000000000000000000013161415124136000157420ustar00rootroot00000000000000#ifndef SC_OPENGL_H #define SC_OPENGL_H #include "common.h" #include #include struct sc_opengl { const char *version; bool is_opengles; int version_major; int version_minor; const GLubyte * (*GetString)(GLenum name); void (*TexParameterf)(GLenum target, GLenum pname, GLfloat param); void (*TexParameteri)(GLenum target, GLenum pname, GLint param); void (*GenerateMipmap)(GLenum target); }; void sc_opengl_init(struct sc_opengl *gl); bool sc_opengl_version_at_least(struct sc_opengl *gl, int minver_major, int minver_minor, int minver_es_major, int minver_es_minor); #endif scrcpy-1.21/app/src/options.c000066400000000000000000000031441415124136000161450ustar00rootroot00000000000000#include "options.h" const struct scrcpy_options scrcpy_options_default = { .serial = NULL, .crop = NULL, .record_filename = NULL, .window_title = NULL, .push_target = NULL, .render_driver = NULL, .codec_options = NULL, .encoder_name = NULL, #ifdef HAVE_V4L2 .v4l2_device = NULL, #endif .log_level = SC_LOG_LEVEL_INFO, .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .last = DEFAULT_LOCAL_PORT_RANGE_LAST, }, .tunnel_host = 0, .tunnel_port = 0, .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, .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, .tcpip = false, .tcpip_dst = NULL, }; scrcpy-1.21/app/src/options.h000066400000000000000000000062061415124136000161540ustar00rootroot00000000000000#ifndef SCRCPY_OPTIONS_H #define SCRCPY_OPTIONS_H #include "common.h" #include #include #include #include "util/tick.h" enum sc_log_level { SC_LOG_LEVEL_VERBOSE, SC_LOG_LEVEL_DEBUG, SC_LOG_LEVEL_INFO, SC_LOG_LEVEL_WARN, SC_LOG_LEVEL_ERROR, }; enum sc_record_format { SC_RECORD_FORMAT_AUTO, SC_RECORD_FORMAT_MP4, SC_RECORD_FORMAT_MKV, }; enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_1, SC_LOCK_VIDEO_ORIENTATION_2, SC_LOCK_VIDEO_ORIENTATION_3, }; enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_INJECT, SC_KEYBOARD_INPUT_MODE_HID, }; 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_MOD_LCTRL = 1 << 0, SC_MOD_RCTRL = 1 << 1, SC_MOD_LALT = 1 << 2, SC_MOD_RALT = 1 << 3, SC_MOD_LSUPER = 1 << 4, SC_MOD_RSUPER = 1 << 5, }; struct sc_shortcut_mods { unsigned data[SC_MAX_SHORTCUT_MODS]; unsigned count; }; struct sc_port_range { uint16_t first; uint16_t last; }; #define SC_WINDOW_POSITION_UNDEFINED (-0x8000) struct scrcpy_options { const char *serial; const char *crop; const char *record_filename; const char *window_title; const char *push_target; const char *render_driver; const char *codec_options; const char *encoder_name; #ifdef HAVE_V4L2 const char *v4l2_device; #endif enum sc_log_level log_level; enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; 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; 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 tcpip; const char *tcpip_dst; }; extern const struct scrcpy_options scrcpy_options_default; #endif scrcpy-1.21/app/src/receiver.c000066400000000000000000000060451415124136000162610ustar00rootroot00000000000000#include "receiver.h" #include #include #include "device_msg.h" #include "util/log.h" bool receiver_init(struct receiver *receiver, sc_socket control_socket, struct sc_acksync *acksync) { bool ok = sc_mutex_init(&receiver->mutex); if (!ok) { return false; } receiver->control_socket = control_socket; receiver->acksync = acksync; return true; } void receiver_destroy(struct receiver *receiver) { sc_mutex_destroy(&receiver->mutex); } static void process_msg(struct receiver *receiver, struct device_msg *msg) { switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { char *current = SDL_GetClipboardText(); bool same = current && !strcmp(current, msg->clipboard.text); SDL_free(current); if (same) { LOGD("Computer clipboard unchanged"); return; } LOGI("Device clipboard copied"); SDL_SetClipboardText(msg->clipboard.text); break; } case DEVICE_MSG_TYPE_ACK_CLIPBOARD: assert(receiver->acksync); LOGD("Ack device clipboard sequence=%" PRIu64_, msg->ack_clipboard.sequence); sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); break; } } static ssize_t process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) { size_t head = 0; for (;;) { struct device_msg msg; ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); if (r == -1) { return -1; } if (r == 0) { return head; } process_msg(receiver, &msg); device_msg_destroy(&msg); head += r; assert(head <= len); if (head == len) { return head; } } } static int run_receiver(void *data) { struct receiver *receiver = data; static unsigned char buf[DEVICE_MSG_MAX_SIZE]; size_t head = 0; for (;;) { assert(head < DEVICE_MSG_MAX_SIZE); ssize_t r = net_recv(receiver->control_socket, buf + head, DEVICE_MSG_MAX_SIZE - head); if (r <= 0) { LOGD("Receiver stopped"); break; } head += r; ssize_t consumed = process_msgs(receiver, buf, head); if (consumed == -1) { // an error occurred break; } if (consumed) { head -= consumed; // shift the remaining data in the buffer memmove(buf, &buf[consumed], head); } } return 0; } bool receiver_start(struct receiver *receiver) { LOGD("Starting receiver thread"); bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver", receiver); if (!ok) { LOGC("Could not start receiver thread"); return false; } return true; } void receiver_join(struct receiver *receiver) { sc_thread_join(&receiver->thread, NULL); } scrcpy-1.21/app/src/receiver.h000066400000000000000000000012671415124136000162670ustar00rootroot00000000000000#ifndef RECEIVER_H #define RECEIVER_H #include "common.h" #include #include "util/acksync.h" #include "util/net.h" #include "util/thread.h" // receive events from the device // managed by the controller struct receiver { sc_socket control_socket; sc_thread thread; sc_mutex mutex; struct sc_acksync *acksync; }; bool receiver_init(struct receiver *receiver, sc_socket control_socket, struct sc_acksync *acksync); void receiver_destroy(struct receiver *receiver); bool receiver_start(struct receiver *receiver); // no receiver_stop(), it will automatically stop on control_socket shutdown void receiver_join(struct receiver *receiver); #endif scrcpy-1.21/app/src/recorder.c000066400000000000000000000264371415124136000162710ustar00rootroot00000000000000#include "recorder.h" #include #include #include #include #include "util/log.h" #include "util/str.h" /** Downcast packet_sink to recorder */ #define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVOutputFormat * find_muxer(const char *name) { #ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API void *opaque = NULL; #endif const AVOutputFormat *oformat = NULL; do { #ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API oformat = av_muxer_iterate(&opaque); #else oformat = av_oformat_next(oformat); #endif // until null or containing the requested name } while (oformat && !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) { LOG_OOM(); return NULL; } rec->packet = av_packet_alloc(); if (!rec->packet) { LOG_OOM(); free(rec); 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); } 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); } } static const char * 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; } } static bool recorder_write_header(struct recorder *recorder, const AVPacket *packet) { AVStream *ostream = recorder->ctx->streams[0]; uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { LOG_OOM(); return false; } // copy the first packet to the extra data memcpy(extradata, packet->data, packet->size); ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; int ret = avformat_write_header(recorder->ctx, NULL); if (ret < 0) { LOGE("Failed to write header to %s", recorder->filename); return false; } 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); } 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; } bool ok = recorder_write_header(recorder, packet); if (!ok) { return false; } recorder->header_written = true; return true; } if (packet->pts == AV_NOPTS_VALUE) { // ignore config packets return true; } recorder_rescale_packet(recorder, packet); return av_write_frame(recorder->ctx, packet) >= 0; } static int run_recorder(void *data) { struct recorder *recorder = data; for (;;) { sc_mutex_lock(&recorder->mutex); while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_cond_wait(&recorder->queue_cond, &recorder->mutex); } // if stopped is set, continue to process the remaining events (to // finish the recording) before actually stopping if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) { sc_mutex_unlock(&recorder->mutex); struct 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); 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; if (!previous) { // we just received the first packet continue; } // config packets have no PTS, we must ignore them if (rec->packet->pts != AV_NOPTS_VALUE && previous->packet->pts != AV_NOPTS_VALUE) { // we now know the duration of the previous packet previous->packet->duration = rec->packet->pts - previous->packet->pts; } bool ok = recorder_write(recorder, previous->packet); record_packet_delete(previous); if (!ok) { LOGE("Could not record packet"); sc_mutex_lock(&recorder->mutex); recorder->failed = true; // discard pending packets recorder_queue_clear(&recorder->queue); sc_mutex_unlock(&recorder->mutex); break; } } if (!recorder->failed) { if (recorder->header_written) { int ret = av_write_trailer(recorder->ctx); if (ret < 0) { LOGE("Failed to write trailer to %s", recorder->filename); recorder->failed = true; } } else { // the recorded file is empty recorder->failed = true; } } if (recorder->failed) { LOGE("Recording failed to %s", recorder->filename); } else { const char *format_name = recorder_get_format_name(recorder->format); LOGI("Recording complete to %s file: %s", format_name, recorder->filename); } LOGD("Recorder thread ended"); return 0; } static bool recorder_open(struct recorder *recorder, const AVCodec *input_codec) { bool ok = sc_mutex_init(&recorder->mutex); if (!ok) { return false; } ok = sc_cond_init(&recorder->queue_cond); if (!ok) { goto error_mutex_destroy; } sc_queue_init(&recorder->queue); recorder->stopped = false; recorder->failed = false; recorder->header_written = false; recorder->previous = NULL; const char *format_name = recorder_get_format_name(recorder->format); assert(format_name); const AVOutputFormat *format = find_muxer(format_name); if (!format) { LOGE("Could not find muxer"); goto error_cond_destroy; } recorder->ctx = avformat_alloc_context(); if (!recorder->ctx) { LOG_OOM(); goto error_cond_destroy; } // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat // still expects a pointer-to-non-const (it has not be updated accordingly) // recorder->ctx->oformat = (AVOutputFormat *) format; av_dict_set(&recorder->ctx->metadata, "comment", "Recorded by scrcpy " SCRCPY_VERSION, 0); AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec); if (!ostream) { goto error_avformat_free_context; } ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; ostream->codecpar->codec_id = input_codec->id; ostream->codecpar->format = AV_PIX_FMT_YUV420P; ostream->codecpar->width = recorder->declared_frame_size.width; ostream->codecpar->height = recorder->declared_frame_size.height; int ret = avio_open(&recorder->ctx->pb, recorder->filename, AVIO_FLAG_WRITE); if (ret < 0) { LOGE("Failed to open output file: %s", recorder->filename); // ostream will be cleaned up during context cleaning goto error_avformat_free_context; } LOGD("Starting recorder thread"); ok = sc_thread_create(&recorder->thread, run_recorder, "recorder", recorder); if (!ok) { LOGC("Could not start recorder thread"); goto error_avio_close; } LOGI("Recording started to %s file: %s", format_name, recorder->filename); return true; error_avio_close: avio_close(recorder->ctx->pb); error_avformat_free_context: avformat_free_context(recorder->ctx); error_cond_destroy: sc_cond_destroy(&recorder->queue_cond); error_mutex_destroy: sc_mutex_destroy(&recorder->mutex); return false; } static void recorder_close(struct recorder *recorder) { sc_mutex_lock(&recorder->mutex); recorder->stopped = true; sc_cond_signal(&recorder->queue_cond); sc_mutex_unlock(&recorder->mutex); sc_thread_join(&recorder->thread, NULL); avio_close(recorder->ctx->pb); avformat_free_context(recorder->ctx); sc_cond_destroy(&recorder->queue_cond); sc_mutex_destroy(&recorder->mutex); } static bool recorder_push(struct recorder *recorder, const AVPacket *packet) { sc_mutex_lock(&recorder->mutex); assert(!recorder->stopped); if (recorder->failed) { // reject any new packet (this will stop the stream) sc_mutex_unlock(&recorder->mutex); return false; } struct record_packet *rec = record_packet_new(packet); if (!rec) { LOG_OOM(); sc_mutex_unlock(&recorder->mutex); return false; } sc_queue_push(&recorder->queue, next, rec); sc_cond_signal(&recorder->queue_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); } static bool recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { struct recorder *recorder = DOWNCAST(sink); return recorder_push(recorder, packet); } bool recorder_init(struct recorder *recorder, const char *filename, enum sc_record_format format, struct sc_size declared_frame_size) { recorder->filename = strdup(filename); if (!recorder->filename) { LOG_OOM(); return false; } recorder->format = format; recorder->declared_frame_size = declared_frame_size; static const struct sc_packet_sink_ops ops = { .open = recorder_packet_sink_open, .close = recorder_packet_sink_close, .push = recorder_packet_sink_push, }; recorder->packet_sink.ops = &ops; return true; } void recorder_destroy(struct recorder *recorder) { free(recorder->filename); } scrcpy-1.21/app/src/recorder.h000066400000000000000000000024051415124136000162630ustar00rootroot00000000000000#ifndef RECORDER_H #define RECORDER_H #include "common.h" #include #include #include "coords.h" #include "options.h" #include "trait/packet_sink.h" #include "util/queue.h" #include "util/thread.h" struct record_packet { AVPacket *packet; struct record_packet *next; }; struct recorder_queue SC_QUEUE(struct record_packet); struct recorder { struct sc_packet_sink packet_sink; // packet sink trait char *filename; enum sc_record_format format; AVFormatContext *ctx; struct sc_size declared_frame_size; bool header_written; sc_thread thread; sc_mutex mutex; sc_cond queue_cond; 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; }; bool recorder_init(struct recorder *recorder, const char *filename, enum sc_record_format format, struct sc_size declared_frame_size); void recorder_destroy(struct recorder *recorder); #endif scrcpy-1.21/app/src/scrcpy.c000066400000000000000000000444421415124136000157630ustar00rootroot00000000000000#include "scrcpy.h" #include #include #include #include #include #include #ifdef _WIN32 // not needed here, but winsock2.h must never be included AFTER windows.h # include # include #endif #include "controller.h" #include "decoder.h" #include "events.h" #include "file_handler.h" #include "input_manager.h" #ifdef HAVE_AOA_HID # include "hid_keyboard.h" #endif #include "keyboard_inject.h" #include "mouse_inject.h" #include "recorder.h" #include "screen.h" #include "server.h" #include "stream.h" #include "util/acksync.h" #include "util/log.h" #include "util/net.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif struct scrcpy { struct sc_server server; struct screen screen; struct stream stream; struct decoder decoder; struct recorder recorder; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; #endif struct controller controller; struct file_handler file_handler; #ifdef HAVE_AOA_HID struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; #endif union { struct sc_keyboard_inject keyboard_inject; #ifdef HAVE_AOA_HID struct sc_hid_keyboard keyboard_hid; #endif }; struct sc_mouse_inject mouse_inject; struct input_manager input_manager; }; static inline void push_event(uint32_t type, const char *name) { SDL_Event event; event.type = type; int ret = SDL_PushEvent(&event); if (ret < 0) { LOGE("Could not post %s event: %s", name, SDL_GetError()); // What could we do? } } #define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) #ifdef _WIN32 BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { if (ctrl_type == CTRL_C_EVENT) { PUSH_EVENT(SDL_QUIT); return TRUE; } return FALSE; } #endif // _WIN32 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"); } // Linear filtering if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); } #ifdef SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH // 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"); } #endif #ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS // Disable synthetic mouse events from touch events // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is // better not to generate them in the first place. if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) { LOGW("Could not disable synthetic mouse events"); } #endif #ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR // Disable compositor bypassing on X11 if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) { LOGW("Could not disable X11 compositor bypass"); } #endif // Do not minimize on focus loss if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) { LOGW("Could not disable minimize on focus loss"); } } static void sdl_configure(bool display, bool disable_screensaver) { #ifdef _WIN32 // Clean up properly on Ctrl+C on Windows bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); if (!ok) { LOGW("Could not set Ctrl+C handler"); } #endif // _WIN32 if (!display) { return; } if (disable_screensaver) { LOGD("Screensaver disabled"); SDL_DisableScreenSaver(); } else { LOGD("Screensaver enabled"); SDL_EnableScreenSaver(); } } 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"); 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; } static bool event_loop(struct scrcpy *s, const struct scrcpy_options *options) { SDL_Event event; while (SDL_WaitEvent(&event)) { enum event_result result = handle_event(s, options, &event); switch (result) { case EVENT_RESULT_STOPPED_BY_USER: return true; case EVENT_RESULT_STOPPED_BY_EOS: LOGW("Device disconnected"); return false; case EVENT_RESULT_CONTINUE: break; } } return false; } static bool await_for_server(void) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case SDL_QUIT: LOGD("User requested to quit"); return false; case EVENT_SERVER_CONNECTION_FAILED: LOGE("Server connection failed"); return false; case EVENT_SERVER_CONNECTED: LOGD("Server connected"); return true; default: break; } } LOGE("SDL_WaitEvent() error: %s", SDL_GetError()); return false; } static SDL_LogPriority sdl_priority_from_av_level(int level) { switch (level) { 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 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_VIDEO, priority, local_fmt, vl); free(local_fmt); } static void stream_on_eos(struct stream *stream, void *userdata) { (void) stream; (void) userdata; PUSH_EVENT(EVENT_STREAM_STOPPED); } static void sc_server_on_connection_failed(struct sc_server *server, void *userdata) { (void) server; (void) userdata; PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED); } static void sc_server_on_connected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; PUSH_EVENT(EVENT_SERVER_CONNECTED); } static void sc_server_on_disconnected(struct sc_server *server, void *userdata) { (void) server; (void) userdata; LOGD("Server disconnected"); // Do nothing, the disconnection will be handled by the "stream stopped" // event } bool scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; struct scrcpy *s = &scrcpy; // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); return false; } atexit(SDL_Quit); bool ret = false; bool server_started = false; bool file_handler_initialized = false; bool recorder_initialized = false; #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif bool stream_started = false; #ifdef HAVE_AOA_HID bool aoa_hid_initialized = false; #endif bool controller_initialized = false; bool controller_started = false; bool screen_initialized = false; struct sc_acksync *acksync = NULL; struct sc_server_params params = { .serial = options->serial, .log_level = options->log_level, .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, .max_fps = options->max_fps, .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, .show_touches = options->show_touches, .stay_awake = options->stay_awake, .codec_options = options->codec_options, .encoder_name = options->encoder_name, .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, .tcpip = options->tcpip, .tcpip_dst = options->tcpip_dst, }; static const struct sc_server_callbacks cbs = { .on_connection_failed = sc_server_on_connection_failed, .on_connected = sc_server_on_connected, .on_disconnected = sc_server_on_disconnected, }; if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) { return false; } if (!sc_server_start(&s->server)) { goto end; } server_started = true; if (options->display) { sdl_set_hints(options->render_driver); } // Initialize SDL video in addition if display is enabled if (options->display && SDL_Init(SDL_INIT_VIDEO)) { LOGC("Could not initialize SDL: %s", SDL_GetError()); goto end; } sdl_configure(options->display, options->disable_screensaver); // Await for server without blocking Ctrl+C handling if (!await_for_server()) { goto end; } // It is necessarily initialized here, since the device is connected struct sc_server_info *info = &s->server.info; const char *serial = s->server.params.serial; assert(serial); if (options->display && options->control) { if (!file_handler_init(&s->file_handler, serial, options->push_target)) { goto end; } file_handler_initialized = true; } struct decoder *dec = NULL; bool needs_decoder = options->display; #ifdef HAVE_V4L2 needs_decoder |= !!options->v4l2_device; #endif if (needs_decoder) { decoder_init(&s->decoder); dec = &s->decoder; } struct recorder *rec = NULL; if (options->record_filename) { if (!recorder_init(&s->recorder, options->record_filename, options->record_format, info->frame_size)) { 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 (dec) { stream_add_sink(&s->stream, &dec->packet_sink); } if (rec) { stream_add_sink(&s->stream, &rec->packet_sink); } if (options->control) { #ifdef HAVE_AOA_HID if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; } acksync = &s->acksync; } #endif if (!controller_init(&s->controller, s->server.control_socket, acksync)) { goto end; } controller_initialized = true; if (!controller_start(&s->controller)) { goto end; } controller_started = true; if (options->turn_screen_off) { struct control_msg msg; msg.type = CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF; if (!controller_push_msg(&s->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } } if (options->display) { const char *window_title = options->window_title ? options->window_title : info->device_name; struct screen_params screen_params = { .window_title = window_title, .frame_size = info->frame_size, .always_on_top = options->always_on_top, .window_x = options->window_x, .window_y = options->window_y, .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, .rotation = options->rotation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .buffering_time = options->display_buffer, }; if (!screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; decoder_add_sink(&s->decoder, &s->screen.frame_sink); } #ifdef HAVE_V4L2 if (options->v4l2_device) { if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, info->frame_size, options->v4l2_buffer)) { goto end; } decoder_add_sink(&s->decoder, &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; } stream_started = true; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; if (options->control) { if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID) { #ifdef HAVE_AOA_HID bool aoa_hid_ok = false; bool ok = sc_aoa_init(&s->aoa, serial, acksync); if (!ok) { goto aoa_hid_end; } if (!sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } if (!sc_aoa_start(&s->aoa)) { sc_hid_keyboard_destroy(&s->keyboard_hid); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } aoa_hid_ok = true; kp = &s->keyboard_hid.key_processor; aoa_hid_initialized = true; aoa_hid_end: if (!aoa_hid_ok) { LOGE("Failed to enable HID over AOA, " "fallback to default keyboard injection method " "(-K/--hid-keyboard ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; } #else LOGE("HID over AOA is not supported on this platform, " "fallback to default keyboard injection method " "(-K/--hid-keyboard ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; #endif } // keyboard_input_mode may have been reset if HID mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) { sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, options); kp = &s->keyboard_inject.key_processor; } sc_mouse_inject_init(&s->mouse_inject, &s->controller, &s->screen); mp = &s->mouse_inject.mouse_processor; } input_manager_init(&s->input_manager, &s->controller, &s->screen, kp, mp, options); ret = event_loop(s, options); LOGD("quit..."); // Close the window immediately on closing, because screen_destroy() may // only be called once the stream thread is joined (it may take time) screen_hide_window(&s->screen); end: // The stream is not stopped explicitly, because it will stop by itself on // end-of-stream #ifdef HAVE_AOA_HID if (aoa_hid_initialized) { sc_hid_keyboard_destroy(&s->keyboard_hid); sc_aoa_stop(&s->aoa); } if (acksync) { sc_acksync_destroy(acksync); } #endif if (controller_started) { controller_stop(&s->controller); } if (file_handler_initialized) { file_handler_stop(&s->file_handler); } if (screen_initialized) { screen_interrupt(&s->screen); } if (server_started) { // shutdown the sockets and kill the server sc_server_stop(&s->server); } // now that the sockets are shutdown, the stream and controller are // interrupted, we can join them if (stream_started) { stream_join(&s->stream); } #ifdef HAVE_V4L2 if (v4l2_sink_initialized) { sc_v4l2_sink_destroy(&s->v4l2_sink); } #endif #ifdef HAVE_AOA_HID if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); } #endif // Destroy the screen only after the stream is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { screen_join(&s->screen); screen_destroy(&s->screen); } if (controller_started) { controller_join(&s->controller); } if (controller_initialized) { controller_destroy(&s->controller); } if (recorder_initialized) { recorder_destroy(&s->recorder); } if (file_handler_initialized) { file_handler_join(&s->file_handler); file_handler_destroy(&s->file_handler); } sc_server_destroy(&s->server); return ret; } scrcpy-1.21/app/src/scrcpy.h000066400000000000000000000002301415124136000157530ustar00rootroot00000000000000#ifndef SCRCPY_H #define SCRCPY_H #include "common.h" #include #include "options.h" bool scrcpy(struct scrcpy_options *options); #endif scrcpy-1.21/app/src/screen.c000066400000000000000000000645061415124136000157420ustar00rootroot00000000000000#include "screen.h" #include #include #include #include "events.h" #include "icon.h" #include "options.h" #include "video_buffer.h" #include "util/log.h" #define DISPLAY_MARGINS 96 #define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink) static inline struct sc_size get_rotated_size(struct sc_size size, int rotation) { struct sc_size rotated_size; if (rotation & 1) { rotated_size.width = size.height; rotated_size.height = size.width; } else { rotated_size.width = size.width; rotated_size.height = size.height; } return rotated_size; } // get the window size in a struct sc_size static struct sc_size get_window_size(const struct screen *screen) { int width; int height; SDL_GetWindowSize(screen->window, &width, &height); struct sc_size size; size.width = width; size.height = height; return size; } static struct sc_point get_window_position(const struct screen *screen) { int x; int y; SDL_GetWindowPosition(screen->window, &x, &y); struct sc_point point; point.x = x; point.y = y; return point; } // set the window size to be applied when fullscreen is disabled static void set_window_size(struct screen *screen, struct sc_size new_size) { assert(!screen->fullscreen); assert(!screen->maximized); SDL_SetWindowSize(screen->window, new_size.width, new_size.height); } // get the preferred display bounds (i.e. the screen bounds with some margins) static bool 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)) { LOGW("Could not get display usable bounds: %s", SDL_GetError()); return false; } bounds->width = MAX(0, rect.w - DISPLAY_MARGINS); bounds->height = MAX(0, rect.h - DISPLAY_MARGINS); return true; } static bool 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 / content_size.width || current_size.width == current_size.height * content_size.width / content_size.height; } // return the optimal size of the window, with the following constraints: // - it attempts to keep at least one dimension of the current_size (i.e. it // crops the black borders) // - it keeps the aspect ratio // - it scales down to make it fit in the display_size static struct sc_size get_optimal_size(struct sc_size current_size, struct sc_size content_size) { if (content_size.width == 0 || content_size.height == 0) { // avoid division by 0 return current_size; } struct sc_size window_size; struct sc_size display_size; if (!get_preferred_display_bounds(&display_size)) { // could not get display bounds, do not constraint the size window_size.width = current_size.width; window_size.height = current_size.height; } else { window_size.width = MIN(current_size.width, display_size.width); window_size.height = MIN(current_size.height, display_size.height); } if (is_optimal_size(window_size, content_size)) { return window_size; } bool keep_width = content_size.width * window_size.height > content_size.height * window_size.width; if (keep_width) { // remove black borders on top and bottom window_size.height = content_size.height * window_size.width / content_size.width; } else { // remove black borders on left and right (or none at all if it already // fits) window_size.width = content_size.width * window_size.height / content_size.height; } return window_size; } // initially, there is no current size, so use the frame size as current size // req_width and req_height, if not 0, are the sizes requested by the user static inline struct sc_size get_initial_optimal_size(struct sc_size content_size, uint16_t req_width, uint16_t req_height) { struct sc_size window_size; if (!req_width && !req_height) { window_size = get_optimal_size(content_size, content_size); } else { if (req_width) { window_size.width = req_width; } else { // compute from the requested height window_size.width = (uint32_t) req_height * content_size.width / content_size.height; } if (req_height) { window_size.height = req_height; } else { // compute from the requested width window_size.height = (uint32_t) req_width * content_size.height / content_size.width; } } return window_size; } static void screen_update_content_rect(struct screen *screen) { int dw; int dh; SDL_GL_GetDrawableSize(screen->window, &dw, &dh); struct sc_size content_size = screen->content_size; // The drawable size is the window size * the HiDPI scale struct sc_size drawable_size = {dw, dh}; SDL_Rect *rect = &screen->rect; if (is_optimal_size(drawable_size, content_size)) { rect->x = 0; rect->y = 0; rect->w = drawable_size.width; rect->h = drawable_size.height; return; } bool keep_width = content_size.width * drawable_size.height > content_size.height * drawable_size.width; if (keep_width) { rect->x = 0; rect->w = drawable_size.width; rect->h = drawable_size.width * content_size.height / content_size.width; rect->y = (drawable_size.height - rect->h) / 2; } else { rect->y = 0; rect->h = drawable_size.height; rect->w = drawable_size.height * content_size.width / content_size.height; rect->x = (drawable_size.width - rect->w) / 2; } } static inline SDL_Texture * create_texture(struct screen *screen) { SDL_Renderer *renderer = screen->renderer; struct sc_size size = screen->frame_size; SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, size.width, size.height); if (!texture) { return NULL; } if (screen->mipmaps) { struct sc_opengl *gl = &screen->gl; SDL_GL_BindTexture(texture, NULL, NULL); // Enable trilinear filtering for downscaling gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f); SDL_GL_UnbindTexture(texture); } return texture; } // render the texture to the renderer // // Set the update_content_rect flag if the window or content size may have // changed, so that the content rectangle is recomputed static void screen_render(struct screen *screen, bool update_content_rect) { if (update_content_rect) { screen_update_content_rect(screen); } SDL_RenderClear(screen->renderer); if (screen->rotation == 0) { SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect); } else { // rotation in RenderCopyEx() is clockwise, while screen->rotation is // counterclockwise (to be consistent with --lock-video-orientation) int cw_rotation = (4 - screen->rotation) % 4; double angle = 90 * cw_rotation; SDL_Rect *dstrect = NULL; SDL_Rect rect; if (screen->rotation & 1) { rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2; rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2; rect.w = screen->rect.h; rect.h = screen->rect.w; dstrect = ▭ } else { assert(screen->rotation == 2); dstrect = &screen->rect; } SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect, angle, NULL, 0); } SDL_RenderPresent(screen->renderer); } #if defined(__APPLE__) || defined(__WINDOWS__) # define CONTINUOUS_RESIZING_WORKAROUND #endif #ifdef CONTINUOUS_RESIZING_WORKAROUND // On Windows and MacOS, resizing blocks the event loop, so resizing events are // not triggered. As a workaround, handle them in an event handler. // // // static int event_watcher(void *data, SDL_Event *event) { struct screen *screen = data; if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { // In practice, it seems to always be called from the same thread in // that specific case. Anyway, it's just a workaround. screen_render(screen, true); } return 0; } #endif static bool screen_frame_sink_open(struct sc_frame_sink *sink) { struct screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG screen->open = true; #endif // nothing to do, the screen is already open on the main thread return true; } static void screen_frame_sink_close(struct sc_frame_sink *sink) { struct screen *screen = DOWNCAST(sink); (void) screen; #ifndef NDEBUG screen->open = false; #endif // nothing to do, the screen lifecycle is not managed by the frame producer } static bool screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct screen *screen = DOWNCAST(sink); return sc_video_buffer_push(&screen->vb, frame); } static void sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, void *userdata) { (void) vb; struct screen *screen = userdata; // event_failed implies previous_skipped (the previous frame may not have // been consumed if the event was not sent) assert(!screen->event_failed || previous_skipped); bool need_new_event; if (previous_skipped) { fps_counter_add_skipped_frame(&screen->fps_counter); // The EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead, unless the previous event failed need_new_event = screen->event_failed; } else { need_new_event = true; } if (need_new_event) { static SDL_Event new_frame_event = { .type = EVENT_NEW_FRAME, }; // Post the event on the UI thread int ret = SDL_PushEvent(&new_frame_event); if (ret < 0) { LOGW("Could not post new frame event: %s", SDL_GetError()); screen->event_failed = true; } else { screen->event_failed = false; } } } bool screen_init(struct screen *screen, const struct screen_params *params) { screen->resize_pending = false; screen->has_frame = false; screen->fullscreen = false; screen->maximized = false; screen->event_failed = false; static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, }; bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, screen); if (!ok) { return false; } ok = sc_video_buffer_start(&screen->vb); if (!ok) { goto error_destroy_video_buffer; } if (!fps_counter_init(&screen->fps_counter)) { goto error_stop_and_join_video_buffer; } screen->frame_size = params->frame_size; screen->rotation = params->rotation; if (screen->rotation) { LOGI("Initial display rotation set to %u", screen->rotation); } struct sc_size content_size = get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; struct sc_size window_size = get_initial_optimal_size(content_size,params->window_width, params->window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 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); if (!screen->window) { LOGC("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()); 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 = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, 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) { LOG_OOM(); goto error_destroy_texture; } // Reset the window size to trigger a SIZE_CHANGED event, to workaround // HiDPI issues with some SDL renderers when several displays having // different HiDPI scaling are connected SDL_SetWindowSize(screen->window, window_size.width, window_size.height); screen_update_content_rect(screen); if (params->fullscreen) { screen_switch_fullscreen(screen); } #ifdef CONTINUOUS_RESIZING_WORKAROUND SDL_AddEventWatch(event_watcher, screen); #endif static const struct sc_frame_sink_ops ops = { .open = screen_frame_sink_open, .close = screen_frame_sink_close, .push = screen_frame_sink_push, }; screen->frame_sink.ops = &ops; #ifndef NDEBUG screen->open = false; #endif return true; error_destroy_texture: SDL_DestroyTexture(screen->texture); error_destroy_renderer: SDL_DestroyRenderer(screen->renderer); error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_fps_counter: fps_counter_destroy(&screen->fps_counter); error_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); return false; } static void screen_show_window(struct screen *screen) { SDL_ShowWindow(screen->window); } void screen_hide_window(struct screen *screen) { SDL_HideWindow(screen->window); } void screen_interrupt(struct screen *screen) { sc_video_buffer_stop(&screen->vb); fps_counter_interrupt(&screen->fps_counter); } void screen_join(struct screen *screen) { sc_video_buffer_join(&screen->vb); fps_counter_join(&screen->fps_counter); } void screen_destroy(struct screen *screen) { #ifndef NDEBUG assert(!screen->open); #endif av_frame_free(&screen->frame); SDL_DestroyTexture(screen->texture); SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); fps_counter_destroy(&screen->fps_counter); sc_video_buffer_destroy(&screen->vb); } static void resize_for_content(struct screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width / old_content_size.width, .height = (uint32_t) window_size.height * new_content_size.height / old_content_size.height, }; target_size = get_optimal_size(target_size, new_content_size); set_window_size(screen, target_size); } static void set_content_size(struct screen *screen, struct sc_size new_content_size) { if (!screen->fullscreen && !screen->maximized) { resize_for_content(screen, screen->content_size, new_content_size); } else if (!screen->resize_pending) { // Store the windowed size to be able to compute the optimal size once // fullscreen and maximized are disabled screen->windowed_content_size = screen->content_size; screen->resize_pending = true; } screen->content_size = new_content_size; } static void apply_pending_resize(struct screen *screen) { assert(!screen->fullscreen); assert(!screen->maximized); if (screen->resize_pending) { resize_for_content(screen, screen->windowed_content_size, screen->content_size); screen->resize_pending = false; } } void screen_set_rotation(struct screen *screen, unsigned rotation) { assert(rotation < 4); if (rotation == screen->rotation) { return; } struct sc_size new_content_size = get_rotated_size(screen->frame_size, rotation); set_content_size(screen, new_content_size); screen->rotation = rotation; LOGI("Display rotation set to %u", rotation); screen_render(screen, true); } // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct screen *screen, struct sc_size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { // frame dimension changed, destroy texture SDL_DestroyTexture(screen->texture); screen->frame_size = new_frame_size; struct sc_size new_content_size = get_rotated_size(new_frame_size, screen->rotation); set_content_size(screen, new_content_size); screen_update_content_rect(screen); LOGI("New texture: %" PRIu16 "x%" PRIu16, screen->frame_size.width, screen->frame_size.height); screen->texture = create_texture(screen); if (!screen->texture) { LOGC("Could not create texture: %s", SDL_GetError()); return false; } } return true; } // 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]); if (screen->mipmaps) { SDL_GL_BindTexture(screen->texture, NULL, NULL); screen->gl.GenerateMipmap(GL_TEXTURE_2D); SDL_GL_UnbindTexture(screen->texture); } } static bool screen_update_frame(struct screen *screen) { av_frame_unref(screen->frame); sc_video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; fps_counter_add_rendered_frame(&screen->fps_counter); struct sc_size new_frame_size = {frame->width, frame->height}; if (!prepare_for_frame(screen, new_frame_size)) { return false; } update_texture(screen, frame); screen_render(screen, false); return true; } void screen_switch_fullscreen(struct 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()); return; } screen->fullscreen = !screen->fullscreen; if (!screen->fullscreen && !screen->maximized) { apply_pending_resize(screen); } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); screen_render(screen, true); } void screen_resize_to_fit(struct screen *screen) { if (screen->fullscreen || screen->maximized) { return; } struct sc_point point = get_window_position(screen); struct sc_size window_size = get_window_size(screen); struct sc_size optimal_size = get_optimal_size(window_size, screen->content_size); // Center the window related to the device screen assert(optimal_size.width <= window_size.width); assert(optimal_size.height <= window_size.height); uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2; uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2; SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); SDL_SetWindowPosition(screen->window, new_x, new_y); LOGD("Resized to optimal size: %ux%u", optimal_size.width, optimal_size.height); } void screen_resize_to_pixel_perfect(struct screen *screen) { if (screen->fullscreen) { return; } if (screen->maximized) { SDL_RestoreWindow(screen->window); screen->maximized = false; } 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); } bool screen_handle_event(struct screen *screen, SDL_Event *event) { switch (event->type) { case EVENT_NEW_FRAME: if (!screen->has_frame) { screen->has_frame = true; // this is the very first frame, show the window screen_show_window(screen); } bool ok = screen_update_frame(screen); if (!ok) { LOGW("Frame update failed\n"); } return true; case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing return true; } switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: screen_render(screen, true); break; case SDL_WINDOWEVENT_MAXIMIZED: screen->maximized = true; break; case SDL_WINDOWEVENT_RESTORED: if (screen->fullscreen) { // On Windows, in maximized+fullscreen, disabling // fullscreen mode unexpectedly triggers the "restored" // then "maximized" events, leaving the window in a // weird state (maximized according to the events, but // not maximized visually). break; } screen->maximized = false; apply_pending_resize(screen); screen_render(screen, true); break; } return true; } return false; } struct sc_point screen_convert_drawable_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { unsigned rotation = screen->rotation; assert(rotation < 4); int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; // rotate struct sc_point result; switch (rotation) { case 0: result.x = x; result.y = y; break; case 1: result.x = h - y; result.y = x; break; case 2: result.x = w - x; result.y = h - y; break; default: assert(rotation == 3); result.x = y; result.y = w - x; break; } return result; } struct sc_point screen_convert_window_to_frame_coords(struct screen *screen, int32_t x, int32_t y) { screen_hidpi_scale_coords(screen, &x, &y); return screen_convert_drawable_to_frame_coords(screen, x, y); } void screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y) { // take the HiDPI scaling (dw/ww and dh/wh) into account int ww, wh, dw, dh; SDL_GetWindowSize(screen->window, &ww, &wh); SDL_GL_GetDrawableSize(screen->window, &dw, &dh); // scale for HiDPI (64 bits for intermediate multiplications) *x = (int64_t) *x * dw / ww; *y = (int64_t) *y * dh / wh; } scrcpy-1.21/app/src/screen.h000066400000000000000000000070341415124136000157400ustar00rootroot00000000000000#ifndef SCREEN_H #define SCREEN_H #include "common.h" #include #include #include #include "coords.h" #include "fps_counter.h" #include "opengl.h" #include "trait/frame_sink.h" #include "video_buffer.h" struct screen { struct sc_frame_sink frame_sink; // frame sink trait #ifndef NDEBUG bool open; // track the open/close state to assert correct behavior #endif struct sc_video_buffer vb; struct fps_counter fps_counter; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; struct sc_opengl gl; 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 sc_size windowed_content_size; // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) unsigned rotation; // rectangle of the content (excluding black borders) struct SDL_Rect rect; bool has_frame; bool fullscreen; bool maximized; bool mipmaps; bool event_failed; // in case SDL_PushEvent() returned an error AVFrame *frame; }; struct screen_params { const char *window_title; struct sc_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 bool window_borderless; uint8_t rotation; bool mipmaps; bool fullscreen; sc_tick buffering_time; }; // initialize screen, create window, renderer and texture (window is hidden) bool screen_init(struct screen *screen, const struct screen_params *params); // request to interrupt any inner thread // must be called before screen_join() void screen_interrupt(struct screen *screen); // join any inner thread void screen_join(struct screen *screen); // destroy window, renderer and texture (if any) void screen_destroy(struct screen *screen); // 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); // switch the fullscreen mode void screen_switch_fullscreen(struct screen *screen); // resize window to optimal size (remove black borders) void screen_resize_to_fit(struct screen *screen); // resize window to 1:1 (pixel-perfect) void screen_resize_to_pixel_perfect(struct screen *screen); // set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) void screen_set_rotation(struct screen *screen, unsigned rotation); // react to SDL events bool screen_handle_event(struct screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels struct sc_point screen_convert_window_to_frame_coords(struct screen *screen, int32_t x, int32_t y); // convert point from drawable coordinates to frame coordinates // x and y are expressed in pixels struct sc_point screen_convert_drawable_to_frame_coords(struct screen *screen, int32_t x, int32_t y); // Convert coordinates from window to drawable. // Events are expressed in window coordinates, but content is expressed in // drawable coordinates. They are the same if HiDPI scaling is 1, but differ // otherwise. void screen_hidpi_scale_coords(struct screen *screen, int32_t *x, int32_t *y); #endif scrcpy-1.21/app/src/server.c000066400000000000000000000560021415124136000157610ustar00rootroot00000000000000#include "server.h" #include #include #include #include #include #include #include "adb.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" #include "util/process_intr.h" #include "util/str.h" #define SC_SERVER_FILENAME "scrcpy-server" #define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME #define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar" static char * get_server_path(void) { #ifdef __WINDOWS__ const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); #else const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); #endif if (server_path_env) { // if the envvar is set, use it #ifdef __WINDOWS__ char *server_path = sc_str_from_wchars(server_path_env); #else char *server_path = strdup(server_path_env); #endif if (!server_path) { LOG_OOM(); return NULL; } LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); return server_path; } #ifndef PORTABLE LOGD("Using server: " SC_SERVER_PATH_DEFAULT); char *server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { LOG_OOM(); return NULL; } #else char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME); if (!server_path) { LOGE("Could not get local file path, " "using " SC_SERVER_FILENAME " from current directory"); return strdup(SC_SERVER_FILENAME); } LOGD("Using server (portable): %s", server_path); #endif return server_path; } static void sc_server_params_destroy(struct sc_server_params *params) { // The server stores a copy of the params provided by the user free((char *) params->serial); free((char *) params->crop); free((char *) params->codec_options); free((char *) params->encoder_name); free((char *) params->tcpip_dst); } static bool sc_server_params_copy(struct sc_server_params *dst, const struct sc_server_params *src) { *dst = *src; // The params reference user-allocated memory, so we must copy them to // handle them from another thread #define COPY(FIELD) \ dst->FIELD = NULL; \ if (src->FIELD) { \ dst->FIELD = strdup(src->FIELD); \ if (!dst->FIELD) { \ goto error; \ } \ } COPY(serial); COPY(crop); COPY(codec_options); COPY(encoder_name); COPY(tcpip_dst); #undef COPY return true; error: sc_server_params_destroy(dst); return false; } static bool push_server(struct sc_intr *intr, const char *serial) { char *server_path = get_server_path(); if (!server_path) { return false; } if (!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; } bool ok = adb_push(intr, serial, server_path, SC_DEVICE_SERVER_PATH, 0); free(server_path); return ok; } static const char * log_level_to_server_string(enum sc_log_level level) { switch (level) { case SC_LOG_LEVEL_VERBOSE: return "verbose"; case SC_LOG_LEVEL_DEBUG: return "debug"; case SC_LOG_LEVEL_INFO: return "info"; case SC_LOG_LEVEL_WARN: return "warn"; case SC_LOG_LEVEL_ERROR: return "error"; default: assert(!"unexpected log level"); return "(unknown)"; } } static 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 sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { sc_pid pid = SC_PROCESS_NONE; const char *cmd[128]; unsigned count = 0; 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," "server=y,address=" # else /* Android 8 and below */ "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" # endif SERVER_DEBUGGER_PORT; #endif 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, ...) { \ char *p = (char *) &cmd[count]; \ if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ goto end; \ } \ cmd[count++] = p; \ } #define STRBOOL(v) (v ? "true" : "false") ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate); 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=%s", STRBOOL(server->tunnel.forward)); } if (params->crop) { ADD_PARAM("crop=%s", params->crop); } if (!params->control) { // By default, control is true ADD_PARAM("control=%s", STRBOOL(params->control)); } if (params->display_id) { ADD_PARAM("display_id=%" PRIu32, params->display_id); } if (params->show_touches) { ADD_PARAM("show_touches=%s", STRBOOL(params->show_touches)); } if (params->stay_awake) { ADD_PARAM("stay_awake=%s", STRBOOL(params->stay_awake)); } if (params->codec_options) { ADD_PARAM("codec_options=%s", params->codec_options); } if (params->encoder_name) { ADD_PARAM("encoder_name=%s", params->encoder_name); } if (params->power_off_on_close) { ADD_PARAM("power_off_on_close=%s", STRBOOL(params->power_off_on_close)); } if (!params->clipboard_autosync) { // By defaut, clipboard_autosync is true ADD_PARAM("clipboard_autosync=%s", STRBOOL(params->clipboard_autosync)); } #undef ADD_PARAM #undef STRBOOL #ifdef SERVER_DEBUGGER LOGI("Server debugger waiting for a client on device port " SERVER_DEBUGGER_PORT "..."); // From the computer, run // adb forward tcp:5005 tcp:5005 // Then, from Android Studio: Run > Debug > Edit configurations... // On the left, click on '+', "Remote", with: // Host: localhost // Port: 5005 // Then click on "Debug" #endif // Inherit both stdout and stderr (all server logs are printed to stdout) pid = adb_execute(params->serial, cmd, count, 0); end: for (unsigned i = dyn_idx; i < count; ++i) { free((char *) cmd[i]); } return pid; } 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_intr(intr, socket, &byte, 1) != 1) { // the server is not listening yet behind the adb tunnel return false; } return true; } 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: %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) { sc_tick deadline = sc_tick_now() + delay; bool ok = sc_server_sleep(server, deadline); if (!ok) { LOGI("Connection attempt stopped"); break; } } } while (--attempts); return SC_SOCKET_NONE; } 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; } ok = sc_mutex_init(&server->mutex); if (!ok) { sc_server_params_destroy(&server->params); return false; } ok = sc_cond_init(&server->cond_stopped); if (!ok) { sc_mutex_destroy(&server->mutex); sc_server_params_destroy(&server->params); return false; } 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->stopped = false; server->video_socket = SC_SOCKET_NONE; server->control_socket = SC_SOCKET_NONE; sc_adb_tunnel_init(&server->tunnel); assert(cbs); assert(cbs->on_connection_failed); assert(cbs->on_connected); assert(cbs->on_disconnected); server->cbs = cbs; server->cbs_userdata = cbs_userdata; return true; } static bool device_read_info(struct sc_intr *intr, sc_socket device_socket, struct sc_server_info *info) { unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH + 4]; ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) { LOGE("Could not retrieve device information"); return false; } // in case the client sends garbage buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; memcpy(info->device_name, (char *) buf, sizeof(info->device_name)); info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8) | buf[SC_DEVICE_NAME_FIELD_LENGTH + 1]; info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8) | buf[SC_DEVICE_NAME_FIELD_LENGTH + 3]; return true; } static bool sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) { struct sc_adb_tunnel *tunnel = &server->tunnel; assert(tunnel->enabled); const char *serial = server->params.serial; sc_socket video_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE; if (!tunnel->forward) { video_socket = net_accept_intr(&server->intr, tunnel->server_socket); if (video_socket == SC_SOCKET_NONE) { goto fail; } 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); video_socket = connect_to_server(server, attempts, delay, tunnel_host, tunnel_port); if (video_socket == SC_SOCKET_NONE) { goto fail; } // we know that the device is listening, we don't need several attempts 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; } } // we don't need the adb tunnel anymore sc_adb_tunnel_close(tunnel, &server->intr, serial); // The sockets will be closed on stop if device_read_info() fails bool ok = device_read_info(&server->intr, video_socket, info); if (!ok) { goto fail; } assert(video_socket != SC_SOCKET_NONE); assert(control_socket != SC_SOCKET_NONE); server->video_socket = video_socket; server->control_socket = control_socket; return true; fail: if (video_socket != SC_SOCKET_NONE) { if (!net_close(video_socket)) { LOGW("Could not close video socket"); } } if (control_socket != SC_SOCKET_NONE) { if (!net_close(control_socket)) { LOGW("Could not close control socket"); } } // Always leave this function with tunnel disabled sc_adb_tunnel_close(tunnel, &server->intr, serial); return false; } 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 bool sc_server_fill_serial(struct sc_server *server) { // Retrieve the actual device immediately if not provided, so that all // future adb commands are executed for this specific device, even if other // devices are connected afterwards (without "more than one // device/emulator" error) if (!server->params.serial) { // The serial is owned by sc_server_params, and will be freed on destroy server->params.serial = adb_get_serialno(&server->intr, 0); if (!server->params.serial) { LOGE("Could not get device serial"); return false; } LOGD("Device serial: %s", server->params.serial); } return true; } static bool is_tcpip_mode_enabled(struct sc_server *server) { struct sc_intr *intr = &server->intr; const char *serial = server->params.serial; char *current_port = adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); if (!current_port) { return false; } // Is the device is listening on TCP on port 5555? bool enabled = !strcmp("5555", current_port); free(current_port); return enabled; } static bool wait_tcpip_mode_enabled(struct sc_server *server, unsigned attempts, sc_tick delay) { if (is_tcpip_mode_enabled(server)) { LOGI("TCP/IP mode enabled"); return true; } // 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; } if (is_tcpip_mode_enabled(server)) { LOGI("TCP/IP mode enabled"); return true; } } while (--attempts); return false; } char * append_port_5555(const char *ip) { size_t len = strlen(ip); // sizeof counts the final '\0' char *ip_port = malloc(len + sizeof(":5555")); if (!ip_port) { LOG_OOM(); return NULL; } memcpy(ip_port, ip, len); memcpy(ip_port + len, ":5555", sizeof(":5555")); return ip_port; } static bool sc_server_switch_to_tcpip(struct sc_server *server, char **out_ip_port) { const char *serial = server->params.serial; assert(serial); struct sc_intr *intr = &server->intr; char *ip = adb_get_device_ip(intr, serial, 0); if (!ip) { LOGE("Device IP not found"); return false; } char *ip_port = append_port_5555(ip); free(ip); if (!ip_port) { return false; } bool tcp_mode = is_tcpip_mode_enabled(server); if (!tcp_mode) { bool ok = adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT); if (!ok) { LOGE("Could not restart adbd in TCP/IP mode"); goto error; } unsigned attempts = 40; sc_tick delay = SC_TICK_FROM_MS(250); ok = wait_tcpip_mode_enabled(server, attempts, delay); if (!ok) { goto error; } } *out_ip_port = ip_port; return true; error: free(ip_port); return false; } static bool 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 adb_disconnect(intr, ip_port, SC_ADB_SILENT); bool ok = adb_connect(intr, ip_port, 0); if (!ok) { LOGE("Could not connect to %s", ip_port); return false; } // Override the serial, owned by the sc_server_params free((void *) server->params.serial); server->params.serial = strdup(ip_port); if (!server->params.serial) { LOG_OOM(); return false; } LOGI("Connected to %s", ip_port); return true; } static bool sc_server_configure_tcpip(struct sc_server *server) { char *ip_port; const struct sc_server_params *params = &server->params; // If tcpip parameter is given, then it must connect to this address. // Therefore, the device is unknown, so serial is meaningless at this point. assert(!params->serial || !params->tcpip_dst); if (params->tcpip_dst) { // Append ":5555" if no port is present bool contains_port = strchr(params->tcpip_dst, ':'); ip_port = contains_port ? strdup(params->tcpip_dst) : append_port_5555(params->tcpip_dst); if (!ip_port) { LOG_OOM(); return false; } } else { // The device IP address must be retrieved from the current // connected device if (!sc_server_fill_serial(server)) { return false; } // The serial is either the real serial when connected via USB, or // the IP:PORT when connected over TCP/IP. Only the latter contains // a colon. bool is_already_tcpip = strchr(params->serial, ':'); if (is_already_tcpip) { // Nothing to do LOGI("Device already connected via TCP/IP: %s", params->serial); return true; } bool ok = sc_server_switch_to_tcpip(server, &ip_port); if (!ok) { return false; } } // On success, this call changes params->serial bool ok = sc_server_connect_to_tcpip(server, ip_port); free(ip_port); return ok; } static int run_server(void *data) { struct sc_server *server = data; const struct sc_server_params *params = &server->params; if (params->serial) { LOGD("Device serial: %s", params->serial); } if (params->tcpip) { // params->serial may be changed after this call bool ok = sc_server_configure_tcpip(server); if (!ok) { goto error_connection_failed; } } // It is ok to call this function even if the device serial has been // changed by switching over TCP/IP if (!sc_server_fill_serial(server)) { goto error_connection_failed; } bool ok = push_server(&server->intr, params->serial); if (!ok) { goto error_connection_failed; } ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, params->serial, params->port_range, params->force_adb_forward); if (!ok) { goto error_connection_failed; } // server will connect to our server socket sc_pid pid = execute_server(server, params); if (pid == SC_PROCESS_NONE) { sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial); goto error_connection_failed; } static const struct sc_process_listener listener = { .on_terminated = sc_server_on_terminated, }; struct sc_process_observer observer; ok = sc_process_observer_init(&observer, pid, &listener, server); if (!ok) { sc_process_terminate(pid); sc_process_wait(pid, true); // ignore exit code sc_adb_tunnel_close(&server->tunnel, &server->intr, params->serial); goto error_connection_failed; } ok = sc_server_connect_to(server, &server->info); // The tunnel is always closed by server_connect_to() if (!ok) { sc_process_terminate(pid); sc_process_wait(pid, true); // ignore exit code sc_process_observer_join(&observer); sc_process_observer_destroy(&observer); goto error_connection_failed; } // Now connected server->cbs->on_connected(server, server->cbs_userdata); // Wait for server_stop() sc_mutex_lock(&server->mutex); while (!server->stopped) { sc_cond_wait(&server->cond_stopped, &server->mutex); } sc_mutex_unlock(&server->mutex); // Give some delay for the server to terminate properly #define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) sc_tick deadline = sc_tick_now() + WATCHDOG_DELAY; bool terminated = sc_process_observer_timedwait(&observer, deadline); // After this delay, kill the server if it's not dead already. // On some devices, closing the sockets is not sufficient to wake up the // blocking calls while the device is asleep. if (!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..."); sc_process_terminate(pid); } sc_process_observer_join(&observer); sc_process_observer_destroy(&observer); sc_process_close(pid); return 0; error_connection_failed: server->cbs->on_connection_failed(server, server->cbs_userdata); return -1; } bool sc_server_start(struct sc_server *server) { bool ok = sc_thread_create(&server->thread, run_server, "server", server); if (!ok) { LOGE("Could not create server thread"); return false; } return true; } void sc_server_stop(struct sc_server *server) { sc_mutex_lock(&server->mutex); server->stopped = true; sc_cond_signal(&server->cond_stopped); sc_intr_interrupt(&server->intr); sc_mutex_unlock(&server->mutex); sc_thread_join(&server->thread, NULL); } void sc_server_destroy(struct sc_server *server) { sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); } scrcpy-1.21/app/src/server.h000066400000000000000000000047051415124136000157710ustar00rootroot00000000000000#ifndef SERVER_H #define SERVER_H #include "common.h" #include #include #include #include "adb.h" #include "adb_tunnel.h" #include "coords.h" #include "options.h" #include "util/intr.h" #include "util/log.h" #include "util/net.h" #include "util/thread.h" #define SC_DEVICE_NAME_FIELD_LENGTH 64 struct sc_server_info { char device_name[SC_DEVICE_NAME_FIELD_LENGTH]; struct sc_size frame_size; }; struct sc_server_params { const char *serial; enum sc_log_level log_level; const char *crop; const char *codec_options; const char *encoder_name; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; uint16_t max_size; uint32_t bit_rate; uint16_t max_fps; int8_t lock_video_orientation; bool control; uint32_t display_id; bool show_touches; bool stay_awake; bool force_adb_forward; bool power_off_on_close; bool clipboard_autosync; bool tcpip; const char *tcpip_dst; }; struct sc_server { // The internal allocated strings are copies owned by the server struct sc_server_params params; 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; sc_socket video_socket; sc_socket control_socket; const struct sc_server_callbacks *cbs; void *cbs_userdata; }; struct sc_server_callbacks { /** * Called when the server failed to connect * * If it is called, then on_connected() and on_disconnected() will never be * called. */ void (*on_connection_failed)(struct sc_server *server, void *userdata); /** * Called on server connection */ void (*on_connected)(struct sc_server *server, void *userdata); /** * Called on server disconnection (after it has been connected) */ void (*on_disconnected)(struct sc_server *server, void *userdata); }; // init the server with the given params bool sc_server_init(struct sc_server *server, const struct sc_server_params *params, const struct sc_server_callbacks *cbs, void *cbs_userdata); // start the server asynchronously bool sc_server_start(struct sc_server *server); // disconnect and kill the server process void sc_server_stop(struct sc_server *server); // close and release sockets void sc_server_destroy(struct sc_server *server); #endif scrcpy-1.21/app/src/stream.c000066400000000000000000000172741415124136000157560ustar00rootroot00000000000000#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)) { LOG_OOM(); 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)) { LOG_OOM(); return false; } } else { offset = 0; stream->pending = av_packet_alloc(); if (!stream->pending) { LOG_OOM(); return false; } if (av_new_packet(stream->pending, packet->size)) { LOG_OOM(); 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) { LOG_OOM(); 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) { LOG_OOM(); 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, sc_socket 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); } scrcpy-1.21/app/src/stream.h000066400000000000000000000017741415124136000157610ustar00rootroot00000000000000#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 { sc_socket 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, sc_socket 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 scrcpy-1.21/app/src/sys/000077500000000000000000000000001415124136000151225ustar00rootroot00000000000000scrcpy-1.21/app/src/sys/unix/000077500000000000000000000000001415124136000161055ustar00rootroot00000000000000scrcpy-1.21/app/src/sys/unix/file.c000066400000000000000000000036131415124136000171730ustar00rootroot00000000000000#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); } scrcpy-1.21/app/src/sys/unix/process.c000066400000000000000000000117401415124136000177320ustar00rootroot00000000000000#include "util/process.h" #include #include #include #include #include #include #include #include "util/log.h" 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); int in[2]; int out[2]; int err[2]; int internal[2]; // communication between parent and children if (pipe(internal) == -1) { perror("pipe"); return SC_PROCESS_ERROR_GENERIC; } if (pin) { if (pipe(in) == -1) { perror("pipe"); close(internal[0]); close(internal[1]); return SC_PROCESS_ERROR_GENERIC; } } 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; } } *pid = fork(); if (*pid == -1) { perror("fork"); // 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) { if (pin) { if (in[0] != STDIN_FILENO) { dup2(in[0], STDIN_FILENO); close(in[0]); } close(in[1]); } // Do not close stdin in the child process, this makes adb fail on Linux if (pout) { if (out[1] != STDOUT_FILENO) { dup2(out[1], STDOUT_FILENO); close(out[1]); } close(out[0]); } else if (!inherit_stdout) { // Close stdout in the child process close(STDOUT_FILENO); } if (perr) { if (err[1] != STDERR_FILENO) { dup2(err[1], STDERR_FILENO); close(err[1]); } close(err[0]); } else if (!inherit_stderr) { // Close stderr in the child process close(STDERR_FILENO); } close(internal[0]); enum sc_process_result err; if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { execvp(argv[0], (char *const *) argv); perror("exec"); err = errno == ENOENT ? SC_PROCESS_ERROR_MISSING_BINARY : SC_PROCESS_ERROR_GENERIC; } else { perror("fcntl"); err = SC_PROCESS_ERROR_GENERIC; } // send err to the parent if (write(internal[1], &err, sizeof(err)) == -1) { perror("write"); } close(internal[1]); _exit(1); } // 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 (perr) { *perr = err[0]; close(err[1]); } return res; } bool sc_process_terminate(pid_t pid) { if (pid <= 0) { LOGC("Requested to kill %d, this is an error. Please report the bug.\n", (int) pid); abort(); } return kill(pid, SIGKILL) != -1; } sc_exit_code sc_process_wait(pid_t pid, bool close) { int code; int options = WEXITED; if (!close) { options |= WNOWAIT; } siginfo_t info; int r = waitid(P_PID, pid, &info, options); if (r == -1 || info.si_code != CLD_EXITED) { // could not wait, or exited unexpectedly, probably by a signal code = SC_EXIT_CODE_NONE; } else { code = info.si_status; } return code; } void sc_process_close(pid_t pid) { sc_process_wait(pid, true); // ignore exit code } ssize_t sc_pipe_read(int pipe, char *data, size_t len) { return read(pipe, data, len); } void sc_pipe_close(int pipe) { if (close(pipe)) { perror("close pipe"); } } scrcpy-1.21/app/src/sys/win/000077500000000000000000000000001415124136000157175ustar00rootroot00000000000000scrcpy-1.21/app/src/sys/win/file.c000066400000000000000000000014731415124136000170070ustar00rootroot00000000000000#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); } scrcpy-1.21/app/src/sys/win/process.c000066400000000000000000000155431415124136000175510ustar00rootroot00000000000000#include "util/process.h" #include #include #include "util/log.h" #include "util/str.h" #define CMD_MAX_LEN 8192 static bool build_cmd(char *cmd, size_t len, const char *const argv[]) { // Windows command-line parsing is WTF: // // only make it work for this very specific program // (don't handle escaping nor quotes) size_t ret = sc_str_join(cmd, argv, ' ', len); if (ret >= len) { LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1); return false; } return true; } 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 || inherit_stdout) + (perr || inherit_stderr); 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.StartupInfo.cb = sizeof(si); HANDLE handles[3]; LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; if (handle_count) { si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; unsigned i = 0; if (pin) { si.StartupInfo.hStdInput = stdin_read_handle; handles[i++] = si.StartupInfo.hStdInput; } if (pout || inherit_stdout) { si.StartupInfo.hStdOutput = pout ? stdout_write_handle : GetStdHandle(STD_OUTPUT_HANDLE); handles[i++] = si.StartupInfo.hStdOutput; } if (perr || inherit_stderr) { si.StartupInfo.hStdError = perr ? stderr_write_handle : GetStdHandle(STD_ERROR_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)) { LOG_OOM(); goto error_free_attribute_list; } wchar_t *wide = sc_str_to_wchars(cmd); free(cmd); if (!wide) { LOG_OOM(); goto error_free_attribute_list; } BOOL bInheritHandles = handle_count > 0; // DETACHED_PROCESS to disable stdin, stdout and stderr DWORD dwCreationFlags = handle_count > 0 ? EXTENDED_STARTUPINFO_PRESENT : DETACHED_PROCESS; BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles, dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); free(wide); if (!ok) { if (GetLastError() == ERROR_FILE_NOT_FOUND) { ret = SC_PROCESS_ERROR_MISSING_BINARY; } 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); } *handle = pi.hProcess; 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 sc_process_terminate(HANDLE handle) { return TerminateProcess(handle, 1); } 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 = SC_EXIT_CODE_NONE; } if (close) { CloseHandle(handle); } return code; } void sc_process_close(HANDLE handle) { bool closed = CloseHandle(handle); assert(closed); (void) closed; } ssize_t sc_pipe_read(HANDLE pipe, char *data, size_t len) { DWORD r; if (!ReadFile(pipe, data, len, &r, NULL)) { return -1; } return r; } void sc_pipe_close(HANDLE pipe) { if (!CloseHandle(pipe)) { LOGW("Cannot close pipe"); } } scrcpy-1.21/app/src/trait/000077500000000000000000000000001415124136000154275ustar00rootroot00000000000000scrcpy-1.21/app/src/trait/frame_sink.h000066400000000000000000000007771415124136000177310ustar00rootroot00000000000000#ifndef SC_FRAME_SINK_H #define SC_FRAME_SINK_H #include "common.h" #include #include typedef struct AVFrame AVFrame; /** * Frame sink trait. * * Component able to receive AVFrames should implement this trait. */ struct sc_frame_sink { const struct sc_frame_sink_ops *ops; }; struct sc_frame_sink_ops { bool (*open)(struct sc_frame_sink *sink); void (*close)(struct sc_frame_sink *sink); bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame); }; #endif scrcpy-1.21/app/src/trait/key_processor.h000066400000000000000000000024431415124136000204720ustar00rootroot00000000000000#ifndef SC_KEY_PROCESSOR_H #define SC_KEY_PROCESSOR_H #include "common.h" #include #include #include /** * Key processor trait. * * Component able to process and inject keys should implement this trait. */ struct sc_key_processor { /** * 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; const struct sc_key_processor_ops *ops; }; struct sc_key_processor_ops { /** * Process the 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. */ void (*process_key)(struct sc_key_processor *kp, const SDL_KeyboardEvent *event, uint64_t ack_to_wait); void (*process_text)(struct sc_key_processor *kp, const SDL_TextInputEvent *event); }; #endif scrcpy-1.21/app/src/trait/mouse_processor.h000066400000000000000000000016151415124136000210320ustar00rootroot00000000000000#ifndef SC_MOUSE_PROCESSOR_H #define SC_MOUSE_PROCESSOR_H #include "common.h" #include #include #include /** * Mouse processor trait. * * Component able to process and inject mouse events should implement this * trait. */ struct sc_mouse_processor { const struct sc_mouse_processor_ops *ops; }; struct sc_mouse_processor_ops { void (*process_mouse_motion)(struct sc_mouse_processor *mp, const SDL_MouseMotionEvent *event); void (*process_touch)(struct sc_mouse_processor *mp, const SDL_TouchFingerEvent *event); void (*process_mouse_button)(struct sc_mouse_processor *mp, const SDL_MouseButtonEvent *event); void (*process_mouse_wheel)(struct sc_mouse_processor *mp, const SDL_MouseWheelEvent *event); }; #endif scrcpy-1.21/app/src/trait/packet_sink.h000066400000000000000000000011031415124136000200660ustar00rootroot00000000000000#ifndef SC_PACKET_SINK_H #define SC_PACKET_SINK_H #include "common.h" #include #include typedef struct AVCodec AVCodec; typedef struct AVPacket AVPacket; /** * Packet sink trait. * * Component able to receive AVPackets should implement this trait. */ struct sc_packet_sink { const struct sc_packet_sink_ops *ops; }; struct sc_packet_sink_ops { bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec); void (*close)(struct sc_packet_sink *sink); bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet); }; #endif scrcpy-1.21/app/src/util/000077500000000000000000000000001415124136000152615ustar00rootroot00000000000000scrcpy-1.21/app/src/util/acksync.c000066400000000000000000000030701415124136000170600ustar00rootroot00000000000000#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); } scrcpy-1.21/app/src/util/acksync.h000066400000000000000000000025511415124136000170700ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/buffer_util.h000066400000000000000000000017411415124136000177430ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/cbuf.h000066400000000000000000000023421415124136000163520ustar00rootroot00000000000000// 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 scrcpy-1.21/app/src/util/file.c000066400000000000000000000023341415124136000163460ustar00rootroot00000000000000#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; } scrcpy-1.21/app/src/util/file.h000066400000000000000000000017201415124136000163510ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/intmap.c000066400000000000000000000005411415124136000167150ustar00rootroot00000000000000#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; } scrcpy-1.21/app/src/util/intmap.h000066400000000000000000000010021415124136000167130ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/intr.c000066400000000000000000000036341415124136000164070ustar00rootroot00000000000000#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); } scrcpy-1.21/app/src/util/intr.h000066400000000000000000000030541415124136000164100ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/log.c000066400000000000000000000030141415124136000162040ustar00rootroot00000000000000#include "log.h" #include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { switch (level) { case SC_LOG_LEVEL_VERBOSE: return SDL_LOG_PRIORITY_VERBOSE; case SC_LOG_LEVEL_DEBUG: return SDL_LOG_PRIORITY_DEBUG; case SC_LOG_LEVEL_INFO: return SDL_LOG_PRIORITY_INFO; case SC_LOG_LEVEL_WARN: return SDL_LOG_PRIORITY_WARN; case SC_LOG_LEVEL_ERROR: return SDL_LOG_PRIORITY_ERROR; default: assert(!"unexpected log level"); return SDL_LOG_PRIORITY_INFO; } } static enum sc_log_level log_level_sdl_to_sc(SDL_LogPriority priority) { switch (priority) { case SDL_LOG_PRIORITY_VERBOSE: return SC_LOG_LEVEL_VERBOSE; case SDL_LOG_PRIORITY_DEBUG: return SC_LOG_LEVEL_DEBUG; case SDL_LOG_PRIORITY_INFO: return SC_LOG_LEVEL_INFO; case SDL_LOG_PRIORITY_WARN: return SC_LOG_LEVEL_WARN; case SDL_LOG_PRIORITY_ERROR: return SC_LOG_LEVEL_ERROR; default: assert(!"unexpected log level"); return SC_LOG_LEVEL_INFO; } } void sc_set_log_level(enum sc_log_level level) { SDL_LogPriority sdl_log = log_level_sc_to_sdl(level); SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log); } enum sc_log_level sc_get_log_level(void) { SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION); return log_level_sdl_to_sc(sdl_log); } scrcpy-1.21/app/src/util/log.h000066400000000000000000000014351415124136000162160ustar00rootroot00000000000000#ifndef SC_LOG_H #define SC_LOG_H #include "common.h" #include #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() \ LOGC("OOM: %s:%d %s()", __FILE__, __LINE__, __func__) void sc_set_log_level(enum sc_log_level level); enum sc_log_level sc_get_log_level(void); #endif scrcpy-1.21/app/src/util/net.c000066400000000000000000000147241415124136000162230ustar00rootroot00000000000000#include "net.h" #include #include #include #include #include "log.h" #ifdef __WINDOWS__ # 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 __WINDOWS__ WSADATA wsa; int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; if (res < 0) { LOGC("WSAStartup failed with error %d", res); return false; } #endif return true; } void net_cleanup(void) { #ifdef __WINDOWS__ WSACleanup(); #endif } static inline sc_socket wrap(sc_raw_socket sock) { #ifdef __WINDOWS__ 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 __WINDOWS__ if (socket == SC_SOCKET_NONE) { return INVALID_SOCKET; } return socket->socket; #else return socket; #endif } static inline bool sc_raw_socket_close(sc_raw_socket raw_sock) { #ifndef _WIN32 return !close(raw_sock); #else return !closesocket(raw_sock); #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); #else perror(s); #endif } 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 sock; } bool net_connect(sc_socket socket, uint32_t addr, uint16_t port) { sc_raw_socket raw_sock = unwrap(socket); SOCKADDR_IN sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(addr); sin.sin_port = htons(port); if (connect(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("connect"); return false; } return true; } bool net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog) { sc_raw_socket raw_sock = unwrap(socket); int reuse = 1; if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse, sizeof(reuse)) == -1) { net_perror("setsockopt(SO_REUSEADDR)"); } SOCKADDR_IN sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(addr); // htonl() harmless on INADDR_ANY sin.sin_port = htons(port); if (bind(raw_sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) { net_perror("bind"); return false; } if (listen(raw_sock, backlog) == SOCKET_ERROR) { net_perror("listen"); return false; } return true; } sc_socket net_accept(sc_socket server_socket) { sc_raw_socket raw_server_socket = unwrap(server_socket); SOCKADDR_IN csin; socklen_t sinsize = sizeof(csin); #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(sc_socket socket, void *buf, size_t len) { sc_raw_socket raw_sock = unwrap(socket); return recv(raw_sock, buf, len, 0); } ssize_t net_recv_all(sc_socket socket, void *buf, size_t len) { sc_raw_socket raw_sock = unwrap(socket); return recv(raw_sock, buf, len, MSG_WAITALL); } ssize_t net_send(sc_socket socket, const void *buf, size_t len) { sc_raw_socket raw_sock = unwrap(socket); return send(raw_sock, buf, len, 0); } ssize_t net_send_all(sc_socket socket, const void *buf, size_t len) { size_t copied = 0; while (len > 0) { ssize_t w = net_send(socket, buf, len); if (w == -1) { return copied ? (ssize_t) copied : -1; } len -= w; buf = (char *) buf + w; copied += w; } return copied; } bool net_interrupt(sc_socket socket) { assert(socket != SC_SOCKET_NONE); sc_raw_socket raw_sock = unwrap(socket); #ifdef __WINDOWS__ if (!atomic_flag_test_and_set(&socket->closed)) { return !closesocket(raw_sock); } return true; #else return !shutdown(raw_sock, SHUT_RDWR); #endif } bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); #ifdef __WINDOWS__ bool ret = true; if (!atomic_flag_test_and_set(&socket->closed)) { ret = !closesocket(raw_sock); } free(socket); return ret; #else return !close(raw_sock); #endif } 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; } scrcpy-1.21/app/src/util/net.h000066400000000000000000000027021415124136000162210ustar00rootroot00000000000000#ifndef NET_H #define NET_H #include "common.h" #include #include #include #ifdef __WINDOWS__ # include # include # define SC_SOCKET_NONE NULL typedef struct sc_socket_windows { SOCKET socket; atomic_flag closed; } *sc_socket; #else // not __WINDOWS__ # include # define SC_SOCKET_NONE -1 typedef int sc_socket; #endif #define IPV4_LOCALHOST 0x7F000001 bool net_init(void); void net_cleanup(void); sc_socket net_socket(void); bool net_connect(sc_socket socket, uint32_t addr, uint16_t port); bool net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog); sc_socket net_accept(sc_socket server_socket); // the _all versions wait/retry until len bytes have been written/read ssize_t net_recv(sc_socket socket, void *buf, size_t len); ssize_t net_recv_all(sc_socket socket, void *buf, size_t len); ssize_t net_send(sc_socket socket, const void *buf, size_t len); ssize_t 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); // Close the socket. // A socket must always be closed, even if net_interrupt() has been called. bool net_close(sc_socket socket); /** * Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation */ bool net_parse_ipv4(const char *ip, uint32_t *ipv4); #endif scrcpy-1.21/app/src/util/net_intr.c000066400000000000000000000043521415124136000172530ustar00rootroot00000000000000#include "net_intr.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return false; } bool ret = net_connect(socket, addr, port); sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; } bool net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port, int backlog) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return false; } bool ret = net_listen(socket, addr, port, backlog); sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; } sc_socket net_accept_intr(struct sc_intr *intr, sc_socket server_socket) { if (!sc_intr_set_socket(intr, server_socket)) { // Already interrupted return SC_SOCKET_NONE; } sc_socket socket = net_accept(server_socket); sc_intr_set_socket(intr, SC_SOCKET_NONE); return socket; } ssize_t net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t r = net_recv(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return r; } ssize_t net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t r = net_recv_all(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return r; } ssize_t net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t w = net_send(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return w; } ssize_t net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t w = net_send_all(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return w; } scrcpy-1.21/app/src/util/net_intr.h000066400000000000000000000015031415124136000172530ustar00rootroot00000000000000#ifndef SC_NET_INTR_H #define SC_NET_INTR_H #include "common.h" #include "intr.h" #include "net.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port); bool net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port, int backlog); sc_socket net_accept_intr(struct sc_intr *intr, sc_socket server_socket); ssize_t net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); ssize_t net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); ssize_t net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len); ssize_t net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len); #endif scrcpy-1.21/app/src/util/process.c000066400000000000000000000052761415124136000171150ustar00rootroot00000000000000#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 sc_process_observer_init(struct sc_process_observer *observer, sc_pid pid, const struct sc_process_listener *listener, void *listener_userdata) { // Either no listener, or on_terminated() is defined assert(!listener || listener->on_terminated); bool ok = sc_mutex_init(&observer->mutex); if (!ok) { return false; } ok = sc_cond_init(&observer->cond_terminated); if (!ok) { sc_mutex_destroy(&observer->mutex); return false; } observer->pid = pid; observer->listener = listener; observer->listener_userdata = listener_userdata; observer->terminated = false; ok = sc_thread_create(&observer->thread, run_observer, "process_observer", observer); if (!ok) { sc_cond_destroy(&observer->cond_terminated); sc_mutex_destroy(&observer->mutex); return false; } return true; } bool sc_process_observer_timedwait(struct sc_process_observer *observer, sc_tick deadline) { sc_mutex_lock(&observer->mutex); bool timed_out = false; while (!observer->terminated && !timed_out) { timed_out = !sc_cond_timedwait(&observer->cond_terminated, &observer->mutex, deadline); } bool terminated = observer->terminated; sc_mutex_unlock(&observer->mutex); return terminated; } void sc_process_observer_join(struct sc_process_observer *observer) { sc_thread_join(&observer->thread, NULL); } void sc_process_observer_destroy(struct sc_process_observer *observer) { sc_cond_destroy(&observer->cond_terminated); sc_mutex_destroy(&observer->mutex); } scrcpy-1.21/app/src/util/process.h000066400000000000000000000104401415124136000171070ustar00rootroot00000000000000#ifndef SC_PROCESS_H #define SC_PROCESS_H #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 SC_PRIexitcode "lu" // # define SC_PRIsizet "Iu" # define SC_PROCESS_NONE NULL # define SC_EXIT_CODE_NONE -1u // max value as unsigned typedef HANDLE sc_pid; typedef DWORD sc_exit_code; typedef HANDLE sc_pipe; #else # include # define SC_PRIsizet "zu" # define SC_PRIexitcode "d" # define SC_PROCESS_NONE -1 # define SC_EXIT_CODE_NONE -1 typedef pid_t sc_pid; typedef int sc_exit_code; typedef int sc_pipe; #endif struct sc_process_listener { void (*on_terminated)(void *userdata); }; /** * 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; }; enum sc_process_result { SC_PROCESS_SUCCESS, SC_PROCESS_ERROR_GENERIC, SC_PROCESS_ERROR_MISSING_BINARY, }; #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 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 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 sc_process_observer_timedwait(struct sc_process_observer *observer, sc_tick deadline); /** * Join the observer thread */ void sc_process_observer_join(struct sc_process_observer *observer); /** * Destroy the observer * * This does not close the associated process. */ void sc_process_observer_destroy(struct sc_process_observer *observer); #endif scrcpy-1.21/app/src/util/process_intr.c000066400000000000000000000013121415124136000201340ustar00rootroot00000000000000#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 (!sc_intr_set_process(intr, pid)) { // Already interrupted return false; } ssize_t ret = sc_pipe_read(pipe, data, len); 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 (!sc_intr_set_process(intr, pid)) { // Already interrupted return false; } ssize_t ret = sc_pipe_read_all(pipe, data, len); sc_intr_set_process(intr, SC_PROCESS_NONE); return ret; } scrcpy-1.21/app/src/util/process_intr.h000066400000000000000000000005551415124136000201510ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/queue.h000066400000000000000000000037421415124136000165640ustar00rootroot00000000000000// 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 scrcpy-1.21/app/src/util/str.c000066400000000000000000000167421415124136000162470ustar00rootroot00000000000000#include "str.h" #include #include #include #include #include #ifdef _WIN32 # include # include #endif #include "log.h" #include "strbuf.h" size_t 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]; if (n) dest[i] = '\0'; return src[i] == '\0' ? i : n; } size_t 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; while (token) { if (i) { dst[i++] = sep; if (i == n) goto truncated; } size_t w = sc_strncpy(dst + i, token, n - i); if (w >= n - i) goto truncated; i += w; token = *remaining++; } return i; truncated: dst[n - 1] = '\0'; return n; } char * 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); quoted[0] = '"'; quoted[len + 1] = '"'; quoted[len + 2] = '\0'; return quoted; } bool sc_str_parse_integer(const char *s, long *out) { char *endptr; if (*s == '\0') { return false; } errno = 0; long value = strtol(s, &endptr, 0); if (errno == ERANGE) { return false; } if (*endptr != '\0') { return false; } *out = value; return true; } size_t sc_str_parse_integers(const char *s, const char sep, size_t max_items, long *out) { size_t count = 0; char *endptr; do { errno = 0; long value = strtol(s, &endptr, 0); if (errno == ERANGE) { return 0; } if (endptr == s || (*endptr != sep && *endptr != '\0')) { return 0; } out[count++] = value; if (*endptr == sep) { if (count >= max_items) { // max items already reached, could not accept a new item return 0; } // parse the next token during the next iteration s = endptr + 1; } } while (*endptr != '\0'); return count; } bool sc_str_parse_integer_with_suffix(const char *s, long *out) { char *endptr; if (*s == '\0') { return false; } errno = 0; long value = strtol(s, &endptr, 0); if (errno == ERANGE) { return false; } int mul = 1; if (*endptr != '\0') { if (s == endptr) { return false; } if ((*endptr == 'M' || *endptr == 'm') && endptr[1] == '\0') { mul = 1000000; } else if ((*endptr == 'K' || *endptr == 'k') && endptr[1] == '\0') { mul = 1000; } else { return false; } } if ((value < 0 && LONG_MIN / mul > value) || (value > 0 && LONG_MAX / mul < value)) { return false; } *out = value * mul; return true; } bool sc_str_list_contains(const char *list, char sep, const char *s) { char *p; do { p = strchr(list, sep); size_t token_len = p ? (size_t) (p - list) : strlen(list); if (!strncmp(list, s, token_len)) { return true; } if (p) { list = p + 1; } } while (p); return false; } size_t sc_str_utf8_truncation_index(const char *utf8, size_t max_len) { size_t len = strlen(utf8); if (len <= max_len) { return len; } len = max_len; // see UTF-8 encoding while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { // the next byte is not the start of a new UTF-8 codepoint // so if we would cut there, the character would be truncated len--; } return len; } #ifdef _WIN32 wchar_t * sc_str_to_wchars(const char *utf8) { int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); if (!len) { return NULL; } wchar_t *wide = malloc(len * sizeof(wchar_t)); if (!wide) { LOG_OOM(); return NULL; } MultiByteToWideChar(CP_UTF8, 0, utf8, -1, wide, len); return wide; } char * sc_str_from_wchars(const wchar_t *ws) { int len = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL); if (!len) { return NULL; } char *utf8 = malloc(len); if (!utf8) { LOG_OOM(); return NULL; } WideCharToMultiByte(CP_UTF8, 0, ws, -1, utf8, len, NULL, NULL); return utf8; } #endif char * sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent) { assert(indent < columns); struct sc_strbuf buf; // The output string should not be much longer than the input string (just // a few '\n' added), so this initial capacity should hopefully almost // always avoid internal realloc() in string buffer size_t cap = strlen(input) * 3 / 2; if (!sc_strbuf_init(&buf, cap)) { return false; } #define APPEND(S,N) if (!sc_strbuf_append(&buf, S, N)) goto error #define APPEND_CHAR(C) if (!sc_strbuf_append_char(&buf, C)) goto error #define APPEND_N(C,N) if (!sc_strbuf_append_n(&buf, C, N)) goto error #define APPEND_INDENT() if (indent) APPEND_N(' ', indent) APPEND_INDENT(); // The last separator encountered, it must be inserted only conditionnaly, // depending on the next token char pending = 0; // col tracks the current column in the current line size_t col = indent; while (*input) { size_t sep_idx = strcspn(input, "\n "); size_t new_col = col + sep_idx; if (pending == ' ') { // The pending space counts ++new_col; } bool wrap = new_col > columns; char sep = input[sep_idx]; if (sep == ' ') sep = ' '; if (wrap) { APPEND_CHAR('\n'); APPEND_INDENT(); col = indent; } else if (pending) { APPEND_CHAR(pending); ++col; if (pending == '\n') { APPEND_INDENT(); col = indent; } } if (sep_idx) { APPEND(input, sep_idx); col += sep_idx; } pending = sep; input += sep_idx; if (*input != '\0') { // Skip the separator ++input; } } if (pending) APPEND_CHAR(pending); return buf.s; error: free(buf.s); return NULL; } size_t sc_str_truncate(char *data, size_t len, const char *endchars) { data[len - 1] = '\0'; size_t idx = strcspn(data, endchars); data[idx] = '\0'; return idx; } 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; } scrcpy-1.21/app/src/util/str.h000066400000000000000000000071601415124136000162460ustar00rootroot00000000000000#ifndef SC_STR_H #define SC_STR_H #include "common.h" #include #include /** * Like strncpy(), except: * - it copies at most n-1 chars * - the dest string is nul-terminated * - it does not write useless bytes if strlen(src) < n * - it returns the number of chars actually written (max n-1) if src has * been copied completely, or n if src has been truncated */ size_t sc_strncpy(char *dest, const char *src, size_t n); /** * Join tokens by separator `sep` into `dst` * * Return the number of chars actually written (max n-1) if no truncation * occurred, or n if truncated. */ size_t sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); /** * Quote a string * * Return a new allocated string, surrounded with quotes (`"`). */ char * sc_str_quote(const char *src); /** * Parse `s` as an integer into `out` * * Return true if the conversion succeeded, false otherwise. */ bool sc_str_parse_integer(const char *s, long *out); /** * Parse `s` as integers separated by `sep` (for example `1234:2000`) into `out` * * Returns the number of integers on success, 0 on failure. */ size_t sc_str_parse_integers(const char *s, const char sep, size_t max_items, long *out); /** * Parse `s` as an integer into `out` * * Like `sc_str_parse_integer()`, but accept 'k'/'K' (x1000) and 'm'/'M' * (x1000000) as suffixes. * * Return true if the conversion succeeded, false otherwise. */ bool sc_str_parse_integer_with_suffix(const char *s, long *out); /** * Search `s` in the list separated by `sep` * * For example, sc_str_list_contains("a,bc,def", ',', "bc") returns true. */ bool sc_str_list_contains(const char *list, char sep, const char *s); /** * Return the index to truncate a UTF-8 string at a valid position */ size_t sc_str_utf8_truncation_index(const char *utf8, size_t max_len); #ifdef _WIN32 /** * Convert a UTF-8 string to a wchar_t string * * Return the new allocated string, to be freed by the caller. */ wchar_t * sc_str_to_wchars(const char *utf8); /** * Convert a wchar_t string to a UTF-8 string * * Return the new allocated string, to be freed by the caller. */ char * sc_str_from_wchars(const wchar_t *s); #endif /** * Wrap input lines to fit in `columns` columns * * Break input lines at word boundaries (spaces) so that they fit in `columns` * columns, left-indented by `indent` spaces. */ char * sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); /** * Truncate the data after any of the characters from `endchars` * * An '\0' is always written at the end of the data, even if no newline * character is encountered. * * Return the size of the resulting line. */ size_t sc_str_truncate(char *data, size_t len, const char *endchars); /** * 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); #endif scrcpy-1.21/app/src/util/strbuf.c000066400000000000000000000034571415124136000167430ustar00rootroot00000000000000#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; } } scrcpy-1.21/app/src/util/strbuf.h000066400000000000000000000025161415124136000167430ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/term.c000066400000000000000000000016501415124136000163760ustar00rootroot00000000000000#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 } scrcpy-1.21/app/src/util/term.h000066400000000000000000000007001415124136000163760ustar00rootroot00000000000000#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 scrcpy-1.21/app/src/util/thread.c000066400000000000000000000070131415124136000166750ustar00rootroot00000000000000#include "thread.h" #include #include #include "log.h" bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); if (!sdl_thread) { LOG_OOM(); return false; } thread->thread = sdl_thread; return true; } void sc_thread_join(sc_thread *thread, int *status) { SDL_WaitThread(thread->thread, status); } bool sc_mutex_init(sc_mutex *mutex) { SDL_mutex *sdl_mutex = SDL_CreateMutex(); if (!sdl_mutex) { LOG_OOM(); return false; } mutex->mutex = sdl_mutex; #ifndef NDEBUG atomic_init(&mutex->locker, 0); #endif return true; } void sc_mutex_destroy(sc_mutex *mutex) { SDL_DestroyMutex(mutex->mutex); } void sc_mutex_lock(sc_mutex *mutex) { // SDL mutexes are recursive, but we don't want to use recursive mutexes assert(!sc_mutex_held(mutex)); int r = SDL_LockMutex(mutex->mutex); #ifndef NDEBUG if (r) { LOGC("Could not lock mutex: %s", SDL_GetError()); abort(); } atomic_store_explicit(&mutex->locker, sc_thread_get_id(), memory_order_relaxed); #else (void) r; #endif } void sc_mutex_unlock(sc_mutex *mutex) { #ifndef NDEBUG assert(sc_mutex_held(mutex)); atomic_store_explicit(&mutex->locker, 0, memory_order_relaxed); #endif int r = SDL_UnlockMutex(mutex->mutex); #ifndef NDEBUG if (r) { LOGC("Could not lock mutex: %s", SDL_GetError()); abort(); } #else (void) r; #endif } sc_thread_id sc_thread_get_id(void) { return SDL_ThreadID(); } #ifndef NDEBUG bool sc_mutex_held(struct sc_mutex *mutex) { sc_thread_id locker_id = atomic_load_explicit(&mutex->locker, memory_order_relaxed); return locker_id == sc_thread_get_id(); } #endif bool sc_cond_init(sc_cond *cond) { SDL_cond *sdl_cond = SDL_CreateCond(); if (!sdl_cond) { LOG_OOM(); return false; } cond->cond = sdl_cond; return true; } void sc_cond_destroy(sc_cond *cond) { SDL_DestroyCond(cond->cond); } void sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { int r = SDL_CondWait(cond->cond, mutex->mutex); #ifndef NDEBUG if (r) { LOGC("Could not wait on condition: %s", SDL_GetError()); abort(); } atomic_store_explicit(&mutex->locker, sc_thread_get_id(), memory_order_relaxed); #else (void) r; #endif } bool sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { sc_tick now = sc_tick_now(); if (deadline <= now) { return false; // timeout } uint32_t ms = SC_TICK_TO_MS(deadline - now); int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { LOGC("Could not wait on condition with timeout: %s", SDL_GetError()); abort(); } atomic_store_explicit(&mutex->locker, sc_thread_get_id(), memory_order_relaxed); #endif assert(r == 0 || r == SDL_MUTEX_TIMEDOUT); return r == 0; } void sc_cond_signal(sc_cond *cond) { int r = SDL_CondSignal(cond->cond); #ifndef NDEBUG if (r) { LOGC("Could not signal a condition: %s", SDL_GetError()); abort(); } #else (void) r; #endif } void sc_cond_broadcast(sc_cond *cond) { int r = SDL_CondBroadcast(cond->cond); #ifndef NDEBUG if (r) { LOGC("Could not broadcast a condition: %s", SDL_GetError()); abort(); } #else (void) r; #endif } scrcpy-1.21/app/src/util/thread.h000066400000000000000000000026651415124136000167120ustar00rootroot00000000000000#ifndef SC_THREAD_H #define SC_THREAD_H #include "common.h" #include #include #include "tick.h" /* Forward declarations */ typedef struct SDL_Thread SDL_Thread; typedef struct SDL_mutex SDL_mutex; typedef struct SDL_cond SDL_cond; typedef int sc_thread_fn(void *); typedef unsigned sc_thread_id; typedef atomic_uint sc_atomic_thread_id; typedef struct sc_thread { SDL_Thread *thread; } sc_thread; typedef struct sc_mutex { SDL_mutex *mutex; #ifndef NDEBUG sc_atomic_thread_id locker; #endif } sc_mutex; typedef struct sc_cond { SDL_cond *cond; } sc_cond; bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata); void sc_thread_join(sc_thread *thread, int *status); bool sc_mutex_init(sc_mutex *mutex); void sc_mutex_destroy(sc_mutex *mutex); void sc_mutex_lock(sc_mutex *mutex); void sc_mutex_unlock(sc_mutex *mutex); sc_thread_id sc_thread_get_id(void); #ifndef NDEBUG bool sc_mutex_held(struct sc_mutex *mutex); # define sc_mutex_assert(mutex) assert(sc_mutex_held(mutex)) #else # define sc_mutex_assert(mutex) #endif bool sc_cond_init(sc_cond *cond); void sc_cond_destroy(sc_cond *cond); void sc_cond_wait(sc_cond *cond, sc_mutex *mutex); // return true on signaled, false on timeout bool sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline); void sc_cond_signal(sc_cond *cond); void sc_cond_broadcast(sc_cond *cond); #endif scrcpy-1.21/app/src/util/tick.c000066400000000000000000000010651415124136000163610ustar00rootroot00000000000000#include "tick.h" #include sc_tick sc_tick_now(void) { // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed // in microseconds to store PTS without precision loss. // // As an alternative, SDL_GetPerformanceCounter() and // SDL_GetPerformanceFrequency() could be used, but: // - the conversions (avoiding overflow) are expansive, since the // frequency is not known at compile time; // - in practice, we don't need more precision for now. return (sc_tick) SDL_GetTicks() * 1000; } scrcpy-1.21/app/src/util/tick.h000066400000000000000000000007631415124136000163720ustar00rootroot00000000000000#ifndef SC_TICK_H #define SC_TICK_H #include "common.h" #include typedef int64_t sc_tick; #define PRItick PRIi64 #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes #define SC_TICK_TO_US(tick) (tick) #define SC_TICK_TO_MS(tick) ((tick) / 1000) #define SC_TICK_TO_SEC(tick) ((tick) / 1000000) #define SC_TICK_FROM_US(us) (us) #define SC_TICK_FROM_MS(ms) ((ms) * 1000) #define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) sc_tick sc_tick_now(void); #endif scrcpy-1.21/app/src/v4l2_sink.c000066400000000000000000000240011415124136000162600ustar00rootroot00000000000000#include "v4l2_sink.h" #include #include "util/log.h" #include "util/str.h" /** Downcast frame_sink to sc_v4l2_sink */ #define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink) static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVOutputFormat * find_muxer(const char *name) { #ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API void *opaque = NULL; #endif const AVOutputFormat *oformat = NULL; do { #ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API oformat = av_muxer_iterate(&opaque); #else oformat = av_oformat_next(oformat); #endif // until null or containing the requested name } while (oformat && !sc_str_list_contains(oformat->name, ',', name)); return oformat; } static bool write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) { AVStream *ostream = vs->format_ctx->streams[0]; uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t)); if (!extradata) { LOG_OOM(); return false; } // copy the first packet to the extra data memcpy(extradata, packet->data, packet->size); ostream->codecpar->extradata = extradata; ostream->codecpar->extradata_size = packet->size; int ret = avformat_write_header(vs->format_ctx, NULL); if (ret < 0) { LOGE("Failed to write header to %s", vs->device_name); return false; } return true; } static void rescale_packet(struct sc_v4l2_sink *vs, AVPacket *packet) { AVStream *ostream = vs->format_ctx->streams[0]; av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); } static bool write_packet(struct sc_v4l2_sink *vs, AVPacket *packet) { if (!vs->header_written) { bool ok = write_header(vs, packet); if (!ok) { return false; } vs->header_written = true; return true; } rescale_packet(vs, packet); bool ok = av_write_frame(vs->format_ctx, packet) >= 0; // Failing to write the last frame is not very serious, no future frame may // depend on it, so the resulting file will still be valid (void) ok; return true; } static bool encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) { int ret = avcodec_send_frame(vs->encoder_ctx, frame); if (ret < 0 && ret != AVERROR(EAGAIN)) { LOGE("Could not send v4l2 video frame: %d", ret); return false; } AVPacket *packet = vs->packet; ret = avcodec_receive_packet(vs->encoder_ctx, packet); if (ret == 0) { // A packet was received bool ok = write_packet(vs, packet); av_packet_unref(packet); if (!ok) { LOGW("Could not send packet to v4l2 sink"); return false; } } else if (ret != AVERROR(EAGAIN)) { LOGE("Could not receive v4l2 video packet: %d", ret); return false; } return true; } static int run_v4l2_sink(void *data) { struct sc_v4l2_sink *vs = data; for (;;) { sc_mutex_lock(&vs->mutex); while (!vs->stopped && !vs->has_frame) { sc_cond_wait(&vs->cond, &vs->mutex); } if (vs->stopped) { sc_mutex_unlock(&vs->mutex); break; } vs->has_frame = false; sc_mutex_unlock(&vs->mutex); sc_video_buffer_consume(&vs->vb, vs->frame); bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); if (!ok) { LOGE("Could not send frame to v4l2 sink"); break; } } LOGD("V4l2 thread ended"); return 0; } static 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, }; bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs); if (!ok) { return false; } ok = sc_video_buffer_start(&vs->vb); if (!ok) { goto error_video_buffer_destroy; } ok = sc_mutex_init(&vs->mutex); if (!ok) { goto error_video_buffer_stop_and_join; } ok = sc_cond_init(&vs->cond); if (!ok) { goto error_mutex_destroy; } 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; } const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO); if (!encoder) { LOGE("Raw video encoder not found"); return false; } vs->format_ctx = avformat_alloc_context(); if (!vs->format_ctx) { LOG_OOM(); return false; } // contrary to the deprecated API (av_oformat_next()), av_muxer_iterate() // returns (on purpose) a pointer-to-const, but AVFormatContext.oformat // still expects a pointer-to-non-const (it has not be updated accordingly) // vs->format_ctx->oformat = (AVOutputFormat *) format; #ifdef SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL vs->format_ctx->url = strdup(vs->device_name); if (!vs->format_ctx->url) { LOG_OOM(); goto error_avformat_free_context; } #else strncpy(vs->format_ctx->filename, vs->device_name, sizeof(vs->format_ctx->filename)); #endif AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder); if (!ostream) { LOG_OOM(); goto error_avformat_free_context; } ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; ostream->codecpar->codec_id = encoder->id; ostream->codecpar->format = AV_PIX_FMT_YUV420P; ostream->codecpar->width = vs->frame_size.width; ostream->codecpar->height = vs->frame_size.height; int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE); if (ret < 0) { LOGE("Failed to open output device: %s", vs->device_name); // ostream will be cleaned up during context cleaning goto error_avformat_free_context; } vs->encoder_ctx = avcodec_alloc_context3(encoder); if (!vs->encoder_ctx) { 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->pix_fmt = AV_PIX_FMT_YUV420P; vs->encoder_ctx->time_base.num = 1; vs->encoder_ctx->time_base.den = 1; if (avcodec_open2(vs->encoder_ctx, encoder, NULL) < 0) { LOGE("Could not open codec for v4l2"); goto error_avcodec_free_context; } vs->frame = av_frame_alloc(); if (!vs->frame) { LOG_OOM(); goto error_avcodec_close; } vs->packet = av_packet_alloc(); if (!vs->packet) { LOG_OOM(); goto error_av_frame_free; } vs->has_frame = false; vs->header_written = false; vs->stopped = false; LOGD("Starting v4l2 thread"); ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); if (!ok) { LOGC("Could not start v4l2 thread"); goto error_av_packet_free; } LOGI("v4l2 sink started to device: %s", vs->device_name); return true; error_av_packet_free: av_packet_free(&vs->packet); error_av_frame_free: av_frame_free(&vs->frame); error_avcodec_close: avcodec_close(vs->encoder_ctx); error_avcodec_free_context: avcodec_free_context(&vs->encoder_ctx); error_avio_close: avio_close(vs->format_ctx->pb); error_avformat_free_context: avformat_free_context(vs->format_ctx); error_cond_destroy: sc_cond_destroy(&vs->cond); error_mutex_destroy: sc_mutex_destroy(&vs->mutex); error_video_buffer_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); return false; } static void sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { sc_mutex_lock(&vs->mutex); vs->stopped = true; sc_cond_signal(&vs->cond); sc_mutex_unlock(&vs->mutex); sc_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); avcodec_close(vs->encoder_ctx); avcodec_free_context(&vs->encoder_ctx); avio_close(vs->format_ctx->pb); avformat_free_context(vs->format_ctx); sc_cond_destroy(&vs->cond); sc_mutex_destroy(&vs->mutex); sc_video_buffer_destroy(&vs->vb); } static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { return sc_video_buffer_push(&vs->vb, frame); } static bool sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) { struct sc_v4l2_sink *vs = DOWNCAST(sink); return sc_v4l2_sink_open(vs); } static void sc_v4l2_frame_sink_close(struct sc_frame_sink *sink) { struct sc_v4l2_sink *vs = DOWNCAST(sink); sc_v4l2_sink_close(vs); } static bool sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_v4l2_sink *vs = DOWNCAST(sink); return sc_v4l2_sink_push(vs, frame); } bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, struct sc_size frame_size, sc_tick buffering_time) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); 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, .push = sc_v4l2_frame_sink_push, }; vs->frame_sink.ops = &ops; return true; } void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) { free(vs->device_name); } scrcpy-1.21/app/src/v4l2_sink.h000066400000000000000000000015061415124136000162720ustar00rootroot00000000000000#ifndef SC_V4L2_SINK_H #define SC_V4L2_SINK_H #include "common.h" #include "coords.h" #include "trait/frame_sink.h" #include "video_buffer.h" #include "util/tick.h" #include struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait struct sc_video_buffer vb; AVFormatContext *format_ctx; AVCodecContext *encoder_ctx; char *device_name; struct sc_size frame_size; sc_tick buffering_time; sc_thread thread; sc_mutex mutex; sc_cond cond; bool has_frame; bool stopped; bool header_written; AVFrame *frame; AVPacket *packet; }; bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, struct sc_size frame_size, sc_tick buffering_time); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); #endif scrcpy-1.21/app/src/video_buffer.c000066400000000000000000000146151415124136000171160ustar00rootroot00000000000000#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) { LOG_OOM(); return NULL; } vb_frame->frame = av_frame_alloc(); if (!vb_frame->frame) { LOG_OOM(); 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) { sc_frame_buffer_destroy(&vb->fb); return false; } ok = sc_cond_init(&vb->b.queue_cond); if (!ok) { sc_mutex_destroy(&vb->b.mutex); sc_frame_buffer_destroy(&vb->fb); return false; } ok = sc_cond_init(&vb->b.wait_cond); if (!ok) { 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); LOG_OOM(); 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); } scrcpy-1.21/app/src/video_buffer.h000066400000000000000000000031731415124136000171200ustar00rootroot00000000000000#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 scrcpy-1.21/app/tests/000077500000000000000000000000001415124136000146575ustar00rootroot00000000000000scrcpy-1.21/app/tests/test_adb_parser.c000066400000000000000000000052111415124136000201630ustar00rootroot00000000000000#include "common.h" #include #include "adb_parser.h" static void test_get_ip_single_line() { 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_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.12.34")); } static void test_get_ip_single_line_without_eol() { 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_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.12.34")); } static void test_get_ip_single_line_with_trailing_space() { 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_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.12.34")); } static void test_get_ip_multiline_first_ok() { 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_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.1.2")); } static void test_get_ip_multiline_second_ok() { 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_from_output(ip_route, sizeof(ip_route)); assert(ip); assert(!strcmp(ip, "192.168.1.3")); } static void test_get_ip_no_wlan() { 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_from_output(ip_route, sizeof(ip_route)); assert(!ip); } static void test_get_ip_truncated() { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "\n"; char *ip = sc_adb_parse_device_ip_from_output(ip_route, sizeof(ip_route)); assert(!ip); } int main(int argc, char *argv[]) { (void) argc; (void) argv; 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_no_wlan(); test_get_ip_truncated(); } scrcpy-1.21/app/tests/test_buffer_util.c000066400000000000000000000031611415124136000203710ustar00rootroot00000000000000#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; } scrcpy-1.21/app/tests/test_cbuf.c000066400000000000000000000030641415124136000170040ustar00rootroot00000000000000#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; } scrcpy-1.21/app/tests/test_cli.c000066400000000000000000000112721415124136000166340ustar00rootroot00000000000000#include "common.h" #include #include #include "cli.h" #include "options.h" static void test_flag_version(void) { struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, .version = false, }; char *argv[] = {"scrcpy", "-v"}; bool ok = scrcpy_parse_args(&args, 2, argv); assert(ok); assert(!args.help); assert(args.version); } static void test_flag_help(void) { struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, .version = false, }; char *argv[] = {"scrcpy", "-v"}; bool ok = scrcpy_parse_args(&args, 2, argv); assert(ok); assert(!args.help); assert(args.version); } static void test_options(void) { struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, .version = false, }; char *argv[] = { "scrcpy", "--always-on-top", "--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" "--port", "1234:1236", "--push-target", "/sdcard/Movies", "--record", "file", "--record-format", "mkv", "--serial", "0123456789abcdef", "--show-touches", "--turn-screen-off", "--prefer-text", "--window-title", "my device", "--window-x", "100", "--window-y", "-1", "--window-width", "600", "--window-height", "0", "--window-borderless", }; bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); assert(ok); const struct scrcpy_options *opts = &args.opts; assert(opts->always_on_top); assert(opts->bit_rate == 5000000); assert(!strcmp(opts->crop, "100:200:300:400")); assert(opts->fullscreen); assert(opts->max_fps == 30); assert(opts->max_size == 1024); assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->record_filename, "file")); assert(opts->record_format == SC_RECORD_FORMAT_MKV); assert(!strcmp(opts->serial, "0123456789abcdef")); assert(opts->show_touches); assert(opts->turn_screen_off); 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); assert(opts->window_width == 600); assert(opts->window_height == 0); assert(opts->window_borderless); } static void test_options2(void) { struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, .version = false, }; char *argv[] = { "scrcpy", "--no-control", "--no-display", "--record", "file.mp4", // cannot enable --no-display without recording }; bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); assert(ok); const struct scrcpy_options *opts = &args.opts; assert(!opts->control); assert(!opts->display); assert(!strcmp(opts->record_filename, "file.mp4")); assert(opts->record_format == SC_RECORD_FORMAT_MP4); } static void test_parse_shortcut_mods(void) { struct sc_shortcut_mods mods; bool ok; ok = sc_parse_shortcut_mods("lctrl", &mods); assert(ok); assert(mods.count == 1); assert(mods.data[0] == SC_MOD_LCTRL); ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); assert(ok); assert(mods.count == 1); assert(mods.data[0] == (SC_MOD_LCTRL | SC_MOD_LALT)); ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); assert(ok); assert(mods.count == 2); assert(mods.data[0] == SC_MOD_RCTRL); assert(mods.data[1] == SC_MOD_LALT); ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); assert(ok); assert(mods.count == 3); assert(mods.data[0] == SC_MOD_LSUPER); assert(mods.data[1] == (SC_MOD_RSUPER | SC_MOD_LALT)); assert(mods.data[2] == (SC_MOD_LCTRL | SC_MOD_RCTRL | SC_MOD_RALT)); ok = sc_parse_shortcut_mods("", &mods); assert(!ok); ok = sc_parse_shortcut_mods("lctrl+", &mods); assert(!ok); ok = sc_parse_shortcut_mods("lctrl,", &mods); assert(!ok); } int main(int argc, char *argv[]) { (void) argc; (void) argv; test_flag_version(); test_flag_help(); test_options(); test_options2(); test_parse_shortcut_mods(); return 0; }; scrcpy-1.21/app/tests/test_clock.c000066400000000000000000000043651415124136000171650ustar00rootroot00000000000000#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; }; scrcpy-1.21/app/tests/test_control_msg_serialize.c000066400000000000000000000217531415124136000224670ustar00rootroot00000000000000#include "common.h" #include #include #include "control_msg.h" static void test_serialize_inject_keycode(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_INJECT_KEYCODE, .inject_keycode = { .action = AKEY_EVENT_ACTION_UP, .keycode = AKEYCODE_ENTER, .repeat = 5, .metastate = AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 14); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_KEYCODE, 0x01, // AKEY_EVENT_ACTION_UP 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0X05, // repeat 0x00, 0x00, 0x00, 0x41, // AMETA_SHIFT_ON | AMETA_SHIFT_LEFT_ON }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_text(void) { struct control_msg msg = { .type = 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); assert(size == 18); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_TEXT, 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); } 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'; 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); unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = 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); 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, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_DOWN, .pointer_id = UINT64_C(0x1234567887654321), .position = { .point = { .x = 100, .y = 200, }, .screen_size = { .width = 1080, .height = 1920, }, }, .pressure = 1.0f, .buttons = AMOTION_EVENT_BUTTON_PRIMARY, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 28); const unsigned char expected[] = { 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 }; 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, .inject_scroll_event = { .position = { .point = { .x = 260, .y = 1026, }, .screen_size = { .width = 1080, .height = 1920, }, }, .hscroll = 1, .vscroll = -1, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 21); const unsigned char expected[] = { CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 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, .back_or_screen_on = { .action = AKEY_EVENT_ACTION_UP, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 0x01, // AKEY_EVENT_ACTION_UP }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_notification_panel(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { CONTROL_MSG_TYPE_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, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_collapse_panels(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_get_clipboard(void) { struct control_msg msg = { .type = CONTROL_MSG_TYPE_GET_CLIPBOARD, .get_clipboard = { .copy_key = GET_CLIPBOARD_COPY_KEY_COPY, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_GET_CLIPBOARD, GET_CLIPBOARD_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, .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 == 27); const unsigned char expected[] = { 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 }; 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, .set_screen_power_mode = { .mode = SCREEN_POWER_MODE_NORMAL, }, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, 0x02, // 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, }; unsigned char buf[CONTROL_MSG_MAX_SIZE]; size_t size = control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { CONTROL_MSG_TYPE_ROTATE_DEVICE, }; assert(!memcmp(buf, expected, sizeof(expected))); } int main(int argc, char *argv[]) { (void) argc; (void) argv; test_serialize_inject_keycode(); test_serialize_inject_text(); test_serialize_inject_text_long(); test_serialize_inject_touch_event(); test_serialize_inject_scroll_event(); test_serialize_back_or_screen_on(); test_serialize_expand_notification_panel(); test_serialize_expand_settings_panel(); test_serialize_collapse_panels(); test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); return 0; } scrcpy-1.21/app/tests/test_device_msg_deserialize.c000066400000000000000000000040141415124136000225460ustar00rootroot00000000000000#include "common.h" #include #include #include "device_msg.h" #include static void test_deserialize_clipboard(void) { const unsigned char input[] = { DEVICE_MSG_TYPE_CLIPBOARD, 0x00, 0x00, 0x00, 0x03, // text length 0x41, 0x42, 0x43, // "ABC" }; struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); assert(r == 8); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); assert(!strcmp("ABC", msg.clipboard.text)); device_msg_destroy(&msg); } static void test_deserialize_clipboard_big(void) { unsigned char input[DEVICE_MSG_MAX_SIZE]; input[0] = DEVICE_MSG_TYPE_CLIPBOARD; input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; input[3] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x0000ff00u) >> 8; input[4] = DEVICE_MSG_TEXT_MAX_LENGTH & 0x000000ffu; memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); struct device_msg msg; ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); assert(r == DEVICE_MSG_MAX_SIZE); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.clipboard.text); assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); assert(msg.clipboard.text[0] == 'a'); device_msg_destroy(&msg); } static void test_deserialize_ack_set_clipboard(void) { const unsigned char input[] = { DEVICE_MSG_TYPE_ACK_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence }; struct device_msg msg; ssize_t r = 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)); } int main(int argc, char *argv[]) { (void) argc; (void) argv; test_deserialize_clipboard(); test_deserialize_clipboard_big(); test_deserialize_ack_set_clipboard(); return 0; } scrcpy-1.21/app/tests/test_queue.c000066400000000000000000000014501415124136000172060ustar00rootroot00000000000000#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; } scrcpy-1.21/app/tests/test_str.c000066400000000000000000000272711415124136000167030ustar00rootroot00000000000000#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]; sprintf(buf, "%ldk", LONG_MAX / 2000); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(ok); assert(value == LONG_MAX / 2000 * 1000); sprintf(buf, "%ldm", LONG_MAX / 2000); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(!ok); sprintf(buf, "%ldk", LONG_MIN / 2000); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(ok); assert(value == LONG_MIN / 2000 * 1000); sprintf(buf, "%ldm", LONG_MIN / 2000); ok = sc_str_parse_integer_with_suffix(buf, &value); assert(!ok); } static void test_strlist_contains(void) { assert(sc_str_list_contains("a,bc,def", ',', "bc")); assert(!sc_str_list_contains("a,bc,def", ',', "b")); assert(sc_str_list_contains("", ',', "")); assert(sc_str_list_contains("abc,", ',', "")); assert(sc_str_list_contains(",abc", ',', "")); assert(sc_str_list_contains("abc,,def", ',', "")); assert(!sc_str_list_contains("abc", ',', "")); assert(sc_str_list_contains(",,|x", '|', ",,")); assert(sc_str_list_contains("xyz", '\0', "xyz")); } static void test_wrap_lines(void) { const char *s = "This is a text to test line wrapping. The lines must be " "wrapped at a space or a line break.\n" "\n" "This rectangle must remains a rectangle because it is " "drawn in lines having lengths lower than the specified " "number of columns:\n" " +----+\n" " | |\n" " +----+\n"; // |---- 1 1 2 2| // |0 5 0 5 0 3| <-- 24 columns const char *expected = " This is a text to\n" " test line wrapping.\n" " The lines must be\n" " wrapped at a space\n" " or a line break.\n" " \n" " This rectangle must\n" " remains a rectangle\n" " because it is drawn\n" " in lines having\n" " lengths lower than\n" " the specified number\n" " of columns:\n" " +----+\n" " | |\n" " +----+\n"; char *formatted = sc_str_wrap_lines(s, 24, 4); assert(formatted); assert(!strcmp(formatted, expected)); free(formatted); } static void test_truncate(void) { char s[] = "hello\nworld\n!"; size_t len = sc_str_truncate(s, sizeof(s), "\n"); assert(len == 5); assert(!strcmp("hello", s)); char s2[] = "hello\r\nworkd\r\n!"; len = sc_str_truncate(s2, sizeof(s2), "\n\r"); assert(len == 5); assert(!strcmp("hello", s)); char s3[] = "hello world\n!"; len = sc_str_truncate(s3, sizeof(s3), " \n\r"); assert(len == 5); assert(!strcmp("hello", s3)); char s4[] = "hello "; len = sc_str_truncate(s4, sizeof(s4), " \n\r"); assert(len == 5); assert(!strcmp("hello", s4)); } 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() { 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_truncate(); test_index_of_column(); test_remove_trailing_cr(); return 0; } scrcpy-1.21/app/tests/test_strbuf.c000066400000000000000000000017141415124136000173720ustar00rootroot00000000000000#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; } scrcpy-1.21/assets/000077500000000000000000000000001415124136000142375ustar00rootroot00000000000000scrcpy-1.21/assets/screenshot-debian-600.jpg000066400000000000000000001276201415124136000206510ustar00rootroot00000000000000JFIFC     C   X  S A  @}ADH( lxN *Ǧۧ%@ c.'%JecT^f.`  F5JjHcAX XDJ"XD&C:c+]`5qN\32WwO=zh1`A@X"Eռ=>5JK^[9wN;^ΰZi18mϗZK:+r6@S%-DD ɻy6lu5sj)-n5 Fק`S c )`D++aQW:+CV׵ϯ0NR1c*""Fj$DAbD(nvܰ/qp©e#%0 @%DX""(Un [cW e9uk@c!,V$V$b+-@Q,H,rQ7 .{cV1N5nݼoF , 1 D%!j#7ԺrجcWS4cƍABK bDP#XXgQs].@̯=VH5uyh Aԅ-,ԩ`Ea"V$ DlI7 3+ˣp-5zh XcIA4Ԩ"E`#,+ c#s޸?ѲPՙN}@)Ɠێ1Ռz KNT%Q+bE`X%A%zzf2uQɣti4E)t^0># cV0K=JsP+$eĂX,"*VκvmϷgfN{̆64ItuGw.hSFT]}KNӤ.Foqo˾K;E5}`iXg~{/ =4罹k~$1cM_Vw|w^v7z<-a>z=_H^*D^g>H+@iW:ˆtn~Ⱥ5ss@ O7wƍ%Wz2zZ_|}&W;;.|ïc P\zz30ZJ> et+r%ݹ꾏+$˱Ƃ==a׊ ۼo̳X?y;m+_?ޏ%(Bhw2'ńrǷ=O4i$hU4O÷iFkNSxk9<797w+M6fUVz|Xi0P/.;Lv;1#٬t<ۧapڞ.]3\Cy~峰2^7+E.\kE|ա5EyWli;&˲TRѤo'-=xtN]^;&zu>sX}p߯ơ8D\grJ樜{B+\'q4,*P)Tv1 .sKt$Ȕ5b"}\);k3I;E`eHJ@T)Pj1@qw. 2Q2D'+=o>lw2lNZi5YTBPhX@9t32d!SRZ qNv+Y3D%n6IRJ!1:y$ԉfh f9g-)sG)\fZ̙s+a5FX,fƁ+P;;?Y-I*$HP(RW瞞2h;܈3Psbi,TF:.sExfDɎ%R/ylW iK=美-l;#55c)`Qj H v48pQ@fi2DU)nv^:phz˹MLcQV&9L4;B( 6fJ&2D-ykSjpN9׫v;pϣy~o!9>ssNX7 2L4h@)ӆ:!fdrJkM\W͡\;ӎuzF~~x~]OO侦/Z@d )۾}o>wl]MzO(Ye&dԆB5Euv<ۮIƀ2[v^=]MzW&̖i)e$V \/=/RWEY5G9OI\0.ѐ .Mʥ$8h(BpDa$H,65L?]q=smEa1QFWS~tɤ!A6 )hkӝ)FDE""5/~oXsIU7LN%RԡmkAxק:~QHDD$q̓ID)wy= $@cAo~.4"y2CHΒV\k=D)bE"XMAZbmO`4h v4h%ߢ<}2rH(5i!^5l#QHزEQMAH@Le@3^"D*=1VP8ߢH"E@Qi )I0%c@~= "7%dQAx{H32,"BT6.:VI ;]է|zǬRfR,QGcPE@Y߯9w{~o3o(|z~_>?wz=z{Ce-w[a%*翥sqνA+ʃ-)x}h}xz|fO73Ӝk"zz3ci_{EOG2WY|tO@g?t_-dCۯ͠~S^o ƒ]r>;:f5?_?ֿ~sY`$W~כ>>ɾvq}X2_}s|sYcZ]c|߷u7yHU*9i佷o׳>OqKnM?5n5-rkm;W [| տ;7Z?,վ׿kkoyyo5w3u/[#I,vV8kDu#3aOoO anbuYIM4lM`g Ⱦz=m[&>o??Fu~VƃTu_=YDcKtc,!}8k%21f1DHQbQ3T&  `s^k%2Ć%W:?S@7h"h3EH! FA `] k(Ծ܂11CMrs߼~+Pz'kz+/*:jwM亇P~ [߻^^edI#P!jK뮽yo;s@Ho_[HHy_N7>p ^to^~qx/>G_?%h^cO>-Ľ/u@+XX7Z{??uw59 ʿ/rΊ#yRNgl ~\?_?g`?[mP_RĽ{̼5YhLm֒v9]SK_uߩpW&yky|tf_=ܼ^O!5@S??x#?_~'=8חy8tƘ`>]QjW>~?/}^[-wB|t ^^j'_?4`)W0뚁W*R^)t~Ժx(t=+zs4%p"J`Yz5!]锤]~Mxt⯵Sh힦j>)5sV2E@^'8.~ʂ"(}c܆%+Z +z|(X)"Whٗ9Z$]kWA@Ssbr'7Z(}F1Y}o9fkZvI ЬS$U5 #0g"vTʜ5( uUW&z?&"LQv*DbsII*BP,UKDovz+_U_ku ҏnCkd+VdbbF9?%pzzDk]UkP+w|9ە)Z4RP#nVo)W]:Kw7k,j!+w|@+Vi;5TB")w|`줡%kGUs^oxddgz {]'3-B<2222(4w|~KC!_k֥P$oG;iNPlWk! S B4E )k~O Fqtc`rQ(猯?1FLumz%:RdJ骬rũ3-5lnWoj4Z~ڰ f"믦|y3<{] 2fQo_kG1kju߶-냏%kO"n_1rϗ<:MC['zڬrA̫R5 r%\dgB_E`^iIPhDURw"E3nH)˪&Փږt;^[E2,}Tu7 %Q~|i 8B]? XuL738Q)$ZJw\mitpՔF?v-oX%WuKX۾׉KW 5B9mQ@(5\23##!J.ƻ qGISZ 4]zI#&%c\epz&ET,MOIQw. [Uƫ]h)Gnζ|+]pdB,!HƱ###<22fN"swH? y:H>M^ZT`iA}cϝ cg Z9)9|~:2Nb-J_sd{g狟uHBPkAERbAEثԭK+#<30} tjHZC(q<ǔ8Py3&a<0FHy"aL<0@yO0aaQQQ5^FFFxFiFiFiFiFiFiFiFiFiᑞ9G<!1 0@AQ"2Pa3Rq#$BS5`?Ϣ`v))))))))))))))))))))))))))))))))))))+ Ga[L9-c߄VDҾF29^"E-*D>R HDHDHDHDHDHLIDHDHDHY,sd#$g=g=g=f \MɰCtpRZe A@'s+'nYS> D 7tzl.Kpז%9v繩D2B͓/Ub4UT˰3wG.7tzl>LͰ3wG67tz>Lψ,,swTR}ə9ۧŇQӟaf{d-Aڋ*/kq{X+½>'3~F@3ӡs7¡xwwkJ4ͰD]CaJ7SE+Zň$.."Pn桔 R<7znU,_>wTIDvÙMS]ںWby!E.[sNu:L2_]Cq˰ȉJ>kv 7f.w ˰ҩ\ '@- \Qin05n#E1n8du"; '#5T 4*V MTQF\T[MKyޔ)B_$\)hyf?'Lyy"FqATb?nĚۨٷpltԧ]H'e+tZU8*7U:z.aU苋;nc/r;nc;\wGLmwf;+Hvw;R|.NE wܷ~sTcsN0aB~,&̈́ 7Wtnn\4C^NQIeMv!sw$p A4To&8:poI4Z[XtMvV0eqů3w$~80䕱"q8(s2;w$u 7<a:AS뿳*mA[`WW SQ7~ݤQ5v ߰?&n3whz.{YEѼ489O;fTCiR+&r*U4fTmDˇ{Ch~C3-b5#CV\cuTT P'(.n[]X[\%{Wf#32Қ5gfI$ Ł #&F 6ad O-2Mu)ψR뛿df;Kj !qt'e'8JYaprlol1Pнd^>e!:hj+<чdՎlj;ښ8W0.~qy$G@t^0Chn>ʑo@Bk?G?0mO[L j-ܛcJlS4V/ 6u`~Q[ #(:j5l2>KlcIgh4Sp)F m?'.'}}}TEP)DX\7V0&QR2h$/l?+د+9JiE]Yjic_v eSl'K%e6YǼpA/X8#w#xې{B71,A#-Z/ooow';-6fbn3YyYl̖:ZlsOOUìN?f7&7bekpxPٲƉ|GЫ%e6R&;v H-M6POHBsZBe3V4,Y_![-hx NCcvETb*Nk=k+D&{E0 SZ-rM^o~L;ٿ6gB Xv;:tʃOnϛ5X ,Ab XBU T*PBU T*PZ*hZ-E=1 !0@AP"Qa#2q34BRSC`r?6VᲶV޼Vrg+1YVrf+1YVbg+9YVrg+9YVrMR+ā{ەvYՑ#VFd #V@5d Y5d #V@ Y@,d YPhYE,e (YG`pǚ^Kt>l?)߈/)ȋ{녹ͰsCMhnXj`F==3ю=GN?F:vycaF:-BOFv!V8m_~]N w^z Ku[榼;N@o#]FEo[]i7~k|+v]`Dg+#mq|G޺X)-pV (,䭙# ǔяn.)N Odf,L$6jY 1av(V^xMnh Ը#:0̭w#C;v^tcwd 6 9p0pQX8(Ǟ8\YS#HVLB.)Q C8(颚0HRQ M`n`qڭ2C (R:F,k;N :qNh^˘\;:Ua[;N>#giGӧ~;N/Fv^}vy )9g.ӰM/#F\.R*F[~vcFyNܮQ:=͢=8X)TԹtvcs'2߇}&\HCŔU- }:vNI=r<&\Fܺ-iw 0U>Ÿ\G !צcE8; tbBfҪ/UУ~jm;|髝y=ve`0HQF.k"%mkot"7̄`1SO77Y;tuw-U|-f*"SB{UaҖp|+eӰ:vvN^t#GUclscIY sl7.6DOp:vM(BbT`m4 X3*z&I͜sKWqN^؜cVik`dxR6<8j>Ԇo7~Kt6l)D쯆lQ)9}N<\s+jjcٟen/b*!ܤaO+)z]9iQ0=(955_\uRM^"'Z(&vߋFҖklpUPRUKk^VU+ِF̎_G[nCzEԷe쬇,M!<=Qxr melӟϊ-GY~l9od!~QNY L`t#超7ҾPU?f-!3lн[kj "{9H)̮oMY(bAmBt.\G6至1ߜ*Z(v{~RvU1G Gm/NNev,uxυ}SۓtFFDҺ{Æ5S x)5RS˾a70>+?^޸N?ԭ>ⱏ?41F Am/NLmf6YVUZa]_kUHԯC`m_bWGؕ%~G ͯ|Q *oB4J^0:v ;ˠxNwW3/\C@꾉GEk𷠝;0\ vm~jٝ;~3`=tDӂAdphTM]ʲ䷎6C]n>qN〛,7 ¸W ⧅I9}UdEJPrW-5\w7꣑ 6k'{ >8F.Qk1'i6iƠL5(a/ l-R#(hىR=ٕ5h8;](gy=j]U5Arv1ra" y~6 dQEiQ0ҋ\ڌ^l/o;:ZNtqG*za<f[HEc?tXcT-KhQ  .%/-J16679Jn5*'ϒ wg6`]NҚ疊9bg%t,S|aJܥEYSjN\ ["l4q3lTds –I{PPCfKOp:v-7ji=Sm2XbM+'o+,O'''P0򣉱7ՏB*W*qON_M!1"AQqr 02@a#PR34BbCc$Ss5`%6?#1c1c1cU1Q:G`EU'ac1c1c1c1c, ;k B\PP G᥏:ÞDZ*CS 0HX@>gom*K~HȖҚkWdއHQ ̩QJ\Hȫ **ibЮ(d1[ 3,PɌc ) I:-G3ca@Ǖ ͥn,BfVm(sX(zE閯hHeOzKD!S (6dͤeaRYHf1^X[nO)y?Ta_|;&5Mʶ5U:2+.B\';bC.VnڄjfO ч 8ĸo<պ Vٯ>bMci]? mԔ8E%Xs)55~I &:Q1i6m*v,΢:9zkȆmN10ҙy8bNˆC ,m,RH8ȆIZl#Y v(iVi,Wk/0C0֟H?(}i {>;!YlQydE 󘦴NŞtg/$-f\ #h ābݛ$k ,qwTYm p̬2yiG}ğMtPϭ(Sާ (bqPmaN:EYeAmK'5h`N#"$sA:|foVFN*4U I[Bpѥ{c [jnJ)Eeжᣲ8 6i4׼Nh+um0Ώq8/X,-H QM,kiKoL:Rn6ο!@jرܳ ڰxw%({[O*%(\#֯Gv'T)H4)ըy_3\CK~ks' f?-y1Ϸ%?c59Ƒ*p}@4Ӗc:hyxckᕮ1ϲ-y ZQ $aYlvx>;Ԃ䢡+U/1UCžOpLb g G#\cI'heQa){=XyᵶɊ*#kإjmIBQZeDQBUoYAdks'Rklǩ8 JU^el*ਅtE='[X h/.u%"S;7UN 3x—.+3=o^EhAKIJq_vUQ\ì[Uc[<$J8S[&>3L}ngOD. #\ccīWZe1}RΤ~;RHZ:>b yv(lS6Ф:א>q>fy#ŝZ\i? S:ʦEћlyH,yS 5LY))FoHU5P?ˑ11y䓵l=m7ΑJ] =eB8q#cAKGRA<՚}SHV! RC˜ #a1/6_Y 4 @Gҿu~yW?]GҿuCJ59'NufRd CHӤm_HQAJa4[Gtpd&:>LJW˩ +x00g:8 gE]JgKoo@U B_a}EI%8Ӫ R5h3JڈaTE͡x%i&:V_٥jw 9dks pmՁ F6p1q.{w@3t*aeDb"&'_GUZ{P*NaVe5JWTpTq0D[5myY9Y`~a0QRa6GҨ`UHF*_՗ᄯd:5Ur@!{,q~˸^EZMJʕL D.jS c=8wP.J7Ytz\*:Ia"1-h;-z;' yH7Zs/ }c>c[uۆʑvB/#` ;H!n'9P!})$nBRTqdBBeD\iY JI[Hj6E{t(( o6\=`8 CwR&,<_JL%j8 p @*¸eks &DĔ+moaPjCɯ90,AluZVNĦrYHzs:OԩN DS-fmSRp. '_ٿ[4q6I:IX*-(pLMK;͗iŅfȮiQ}-&8IPqC@f*vg*ӎ) Y]Ub7ŒęaexEVJNH:%/j1,V4eQEh $6+KҤI<\u "ե-'VK4o/$P'BaDg^ZRDr_2rv9WBCNz*7UeYߨ9v p:F0q~״wc\w#v n=jEϴfnq+}ޕؾ9(R˪l/m<P_AmoF1www;7'@9 Di *E.C26iX1 @ەoR~0@m % }/,3 ^ON+X֑Q EIHżO_<(S%sٟuҝ􉀑Ao"_uâC :GEeGڌ1<ԹeJdoxn@Zq`[5 :CMRL"b\6UeHQ2LtYf_N_$4H9*UUM]Q-O(eUHN}$,JJ5iBq%$o%9&w I)PF_`KKd$IA6j-af6Fmi%wA]E, FF'jCTz jmfU?i'x/ iո bͰ2l0W;/zKL,*6+}ݒ$ |M9rZl|⬕fBF8v@ol+-R;f m脦Щa\P#|C#wRӭ1mWjU[Ia,T p}6M!XBA`RUiDXZ&*M-r9,hg^ Xp71M9u$"u 4V᠁+)i=٭Mm԰o1[!.r0M[b@RUQ7-!v0T* jr7<7uj i^pR4S: G򔝝PA)P njaمmue][m,[Dzҏtum6*qUfNc7<wv+-YMl>ȸģ61R$APGq݃=%ΩW/q FnG!0.ֳgT{Z]N)g6CؗU65xO|K$|xiƔ0?GI#G]ǀNM&ʾ-6ONK&FI!U +^ر6?i8Uʥᮉy 90lGzyRX#pQ[..Qî>F)TjQ5F]Lc1c1c1c1c1a&*!1 AQaq0@P?! OOF<zDF 0`E0`F 0`TTTTTTTTTTTT`!AVdRT*G#ʑHy'uH9Df7sx7|g5sEF0'b1O/NX=hsm5,w%qH;gq ccc2~_6t5Oy$NH !>}}~P*]m؊pxmXMe%/ rxtKf$dd%DZR! p#Ss΋L$Rx]kIz=+jOұo۟UY;W-rʖu**vbii=X8G B!4BhN*1cF1aB`bM&1cяG B!BjA0a߆i?;Ռc1c!B`&-Wz=1c7TtZ.7x1cձYDD& <;+֔F1cձ)B>=R)t)JRF6666666661D!BpB >)JR)JRFCf(ccc 67qkiDta_<+^IpZ! ~RJR.XAoJRlllllllaţ#]>]79 <5wD! ~R)JRA)J0FlouPٮg3ѱT۽ GM~R)JSF{# PcR]O\;W6e 3uodҭMAέCbm[>OX* X666^=׃f-t2SJќ-sL!B U)JR)w40Mrl0B)aʖ{,ʏPj|ԭsPe|o*J!tycB^l49ݠcz^Ok:$>ſe<Է6y }qnK_,Ȯp-B\A"Q)2WXK. 6hq(Չ[=s}CʉWøtvf4ө͌{N0C!ǰbkj/plz[+A| [Fw5C&/D={y_Z!8cdХt)D L~+;A+y`åȢ{ WMdn(Й27wuT7t0oW7Oإq3?erݾ|W_)Apnq| JR)J(21EȞ͢75{w/>2G' /s.BwAU=١H6B4nϴH : %U\=۷\xOLD$!4y)JRD =c"|6# USIa\Arcs+-a[{`e,Q4K zCdNU(=$<`nuauBy"gy6+I# ;couАqB/\~l! td)KIr9(ӝXkyKmaq4^]QQt+ {܎}5 < 4.OM2Jxۣ}3grBqh )JRDJiu#DKTRK<i;ˈRj; B19JRL\N{)` &|Y:P.FIEL}+Z/BNd's&-n-rqN܆+aVCI-s} "(䵖fiXb]2vg {hJća_nd!4BHAFXRtAK?`Na)AQ#BՌe..7hHHAhMI-!c?bBBBBD 8B9 D-c=zR HHBBHPB=^_3.D A-!HJD- F>CьނO=DhBHc1>BBBދ'XTA!!,[",p <؉#DQۛ:zϸBhm#IXHA"!$SOAtWH=mlm ݘ1.5ēcG#m<@ cI,AH!4b^LQpuIEfKέجXbJt_+щ-h˦ !! h5aۙil!(K\16Tj6>5QDz-cMz BD.:l32llcz2,i WS*Iw5UYXi!8a?t 4{̈́!p`kWTw/ oЄ&&_pJ61lllce 6Qb:EeILOdpzyqz$ W4mBDƆv2ztL"oCe(1s1ߨY(zy F|@О)DRR)F0Y¹]+lݛOyށ&0J'.!J1-aMsNk~?>yПt]1QoFąfolĿ$- !JRBllcee>|kx +RҔl(FI8F1|' Q2`؍H{g8ɂyQ0пJ>|]ԤXӅz$[ Q2LBQ3i }azzݱfERUll<v]4 KJ}9W0#2$Fj2q>fh mp|?wjk蜨܁n+Ѣ.(zLizJRRBCq½ ?cڿZBw&"$$bd SdwND+~=& lVku ?+t 6?D\3.Ϩ?u^o흞')N>߰م*R%# J#؆sziE.#$h$g>kf~0ckp݆l7[rʞժg"M?GgK]DJЊaT4&оG"kp?+A/ :<^mV#$v]u =w4[.uiB# av>g/Uыb,s7oǑl'Q_mo-6+7>%]eru$bmJ;QI=9x#,<ӣ"3F,e;zOA$6|еyQxg PLdgcZkHM6os>0?lۜ/ wOGyq˵#3y}}:# n|% w;iNO9JRB  05@w==4CHYī~ȇHM!XgЌ'EӫFMuF<13 uvxk9F:mƌvHTIEC}øu.'r)V3XԳFi=fgH䥷T>LHyN):+ijtmh$e.ʾu++S%x-`[g3<Nv) JJa+~/p0|e탹&- F$l:)I|5n7:y739Pc7L7j4|oAf%BbW#n6T@22=Hq|$iIz"i4g|| 9&It_IlI-^]=E{JyKqJR2D)J]>4%J2k\QOsSOw 6'<{.ܐ]d潵טGTY$ij'R穟Xc=42L&݆mowInZҴyX)К6HOqt Jݕ}=觳IGnѣ} a;jf)J]lz #?+$g^'&;АӉܕ#/L`IK$%ͧkК6#f4L4/vW&"$A5/wtZ$IߡS50>h4[]z9燆7VBg\=[:&D3ĆtГxhZRl9r;,6^vE8JR)tRz 60S u6;mg#+_7ו~짢I{j9t@R|ųIY纔=BZ$s*K`R,6S 6a3m8qG`Jd-}Px žuL90h0c L@߁E\MؐW#)[(pE%ASpMpD߱ bU IzI{j5)_ċTzм ] d+O3 N̿h[{4)KG O#f1^33~v6<5#b`FˬF蚋فz]POw,! 6$pCl}T3WOne0Kܥ)JR~|OG8|Oa2)JR~şu *^P'R6C?Hy&#Didߢ?&S)JRK÷tGwy*0ex"oAlo"j.Ndc]dc ylqzRK(0 gk9.),M_Cd%mxǶĨvDkv%X4'KҔlCN9x'&R)xnLϑgsw>GpwGwGww_'w~Gu|w|pgxJR)xB_3XԥJR)JR)JR)JR)JR)JR)JR e-7-I%^D$ mlI- @i@%6mm$^I$I&$I$A!$N:גIdY%oK2mbeIl"mٿERz[m!$%,;mAI$Z->[Sd flPiVL~{N>GiDn~i%Z[MEpnlw(oL&[g:ݨF߷8`i][?l11[jUo)WP0oQhdmJ]޴K,LѤ/mat.۴4H?_/mLS$Ĕ_Oom}&fyg~'o3[XݩEg;'_}nN7k\t?@ά}VOk\ѷN yg^}-$!5?{$BI(|7{;>wsݩ>6)xeKaKn_ohb)/h:\"o;L%UӳBńD"z smekd-v;vͶQ!M*bmRݛ_&z@mܟmmrHeOz%mE/g rRzO5bdvmddnv.$] %'öۮ[h괲F4g)s!\[i‹M'%|}/3yEdղ xJ)&Rnyo4-RHi]vypM'7>{?ό ɅD:L/˶ͣJ _`l{]}ؼDe*%f{dmmn1-.-&dm- i4燺i?Ke~mIM?dv~yai%|߃' s-2Jo7)rB{}dQ-Q~I.{|'̏~S@wm$Ij)li{eDi 6.NHiT,M6{sMJ[S6H9 ms} i$fLSI`i&}bѵb#)OkMi,6ܒU-g'XI^II=刀 7;<)-ݨ D&I$ II I$I$I$I$HKmmmmmmd*!1 Aa0Qq@P?A C!C  dC!d2)p>N838N838N8^N/'qy8^N/'ΰ_߁CM4YuRA[xlvۺ*6IT)3>BKrx9_29̾u5:9<lG:G2G299<o7'a bd>}o?w<FyGI 9A_h8:/:f$ޫ\%`Te ^Ļ aY{y}y` AAX H¦LCYuƩ֔ ^{D4InHkyIS.\r?I$M%M&I4WQSE?uONIԺ (<ҹ$$k5] ю #ZGF,AV4`DR+@ <99Lh7 G}V$4"V)KDi:g8jDF H4$Z G(>Ȥ '+^Q)Vgj"5fHBMC&zPOr\ ?C'ܕiɜsX<-bBdWK$R5aE"sFd&#>ҩm%,y+3dC9e=;P`N䁉!b#b#Xi,H˂CEa-N0&nI?"Fu( E /ȍ Y[,˞X0CMaX2F$1?@ @[ؒ!q %*oHPXF*\7lQ4 T 5)Wh. ^?"SqMlCdӆ SфdM|\HAzH 4EQ(2Vvx$dM˂ up².ąc^rp@6E$%5H TW1MH$dHdnFԲHӽ,)i$" *441 ]eqbhP~p I5qF6c$UXGA- Gx!`fAbu#zER7eӁI#RPcA HH 1!$bF1T!TA(B 4CtSH`1b!DR Hv1 !B0Q" ttcD*! [Zp+H@b:1tBD!*\h3yaR"1cC!$$$$:>Oj==1tdD NL,$$%}\#&X @CEHI YAaOuG(HtKF.t¦BA M! 6& F**ERZ c#X $$!!(QќTB"(>$*cӂ¦u#:AR(;M9HƮ5,1C&lwՄz=tA D- $%D0.\Y)'f6qĴ`dkF"A K`TĜ#!Wh H   HKq jE&)0B1; #A @$! Rs=.\&*DE K2Cӊ0T9Ћ HZ0ayR a"/`B !H W/V:l6?G3pJf=qIЄmW8/F< FCFĶ)D[5{2 *S_Rs+k5KDs&E aV J#j%.2\"# GKRXf ! fac`P _$мև5W "RN;50<N㧐*=i(]_[ ӟ07nŵU0}ŷ~}W~rǦ{WI~ cK $&5Gյ_R.z:d(ЬFp(3tٙʈ?[r:~od>U=!!5r1y=ؑ#K<BYl?/Dq~.p*nlv1I$XсV"2upD$ٯ_owm[$N.6Hdڻ8Nu4N 8hK~%:KŨ!eLs-=y5~of!IYz("!b!d'iuF֥\jդagӫ1 I$(Əwa-K)vV$RiH!܅Jz0'CUcBKvo$(m HWo[G?3\vUCKJvYdibRԍ[ *mݷӺʒ<4LuQv!tNТ[ϳ3Y2$HI0u F*Hk~9ie=DRjͿ e;{Ps@$=;̒~ r0 '{GwXс?Flc%~=b(c'#9ɦ5(vYMPf%vsԂ]FLv#kK##\E鏝027XaF Ad}(Ӄ/fqVu`)l"pIBIG3dtpFaF( ZH#Yf'LI"2UPUUT"h$H"DRijMc$Ƌ.\r˗.\r˗.\r˗.\r˗.\r˗.\r˗/+!1 0AaQq@P`?(UQE (/(88b::9mQEQiqРGv;|֫w;gl;gl흳v;gl0 Wu1o?d@vtpŽ:A :TSuΩ:Tsuι:kӥ~GUQ:8fHZ 4 L`(rcDd!  ~, 1qmL@ aNaKeQlD KP$ 6sТֶ& q&db/?C yyyyyyx7 [MsA㖎8!Z t3#htfa(w/\Gq\z dz -C2 0m4PC˜qDZ^W*\sdEnazmKP̏М;=y1e!C][bUdC2HɨJy N=$Euq̎B@c P qA8` DNDad!(GGWC3T!h@` 810x T 0׹L=h90(LqBtR.6f_a 8 `,1SH`Lc[X#Eᰎ m9r`q6 ʀ00NJtr87-T'53#f/(xNL8D$0ho0@Am9(@pҩ8 ^=e #0 Y*(m繚 (D5z\Vw-UE a{j8ㆇ=K= K"ÆhA?'E ^P>`cC-^Tp=vfD4:yAqS{Y`52✠H1dztpUᆎਇH==]KġT =@cse sENaњ-N];8IǬS/CbJ+8 E^-!І<钣AҨiWL nY 1(E!^Bf*熪ՍY8 saDv`ǜ7 .61Шvpo,.a35dӌ=/pE BLڣt ,*`#΁sEE:=mY(mG5`sEǰ:8B `9 8(b rN F CLELh9|8WH% o@_"4 V[ vHDlK`x$$Qv3옵bhJ 0GkX'+W ibh$ 1E`PLs4<f^&H~@zаGF͛ WD}sh%6*4\  , O/Qc|roq^4C2 @/"\#L(N&Ccvp}{r!p} 10K @p$c'NeqBtfh''ģf&XGCAO?A#sRdbP0 ńE0aftN3d!/_A3 np2c;c^Ghp4B)〼Lc1uͰjZr mqX f3h석p4 "EsDYMq(pUBIzjTQl!Q  ([BwAE@^5GbEZBAs g:3Bu 7'CobAK &a f(-tw3 T!C+گAeE9bf`Ԑ @2?B| 3uPeb @8pM(d2E4h#Dcxظ̂~aL. Ef_Jo hBs.}xُ|@䐈bd(S&ŶL@q3GpxbG€sL@Qw ! w@?/`18 #aPK@no`$J=U?Q wBșHK|(P\ pUYj8 +2FЌ9} S8 vpޙ(=n r^ J7`ߴa47GFBBF4aPY:@ʎ ȱI0G/|0_zd*@"^T#NE]z bZZzF@OHD9$@&-@m`1ǪCVEo/u S:ތb`l1{KgPN Z2Yzp;j+K@\!Ȁ]^8KlF|@BzNqKBZZ-Up?)!1AQaq 0P?OOOOCWNw_NӸvӴ>u;i};Nw_NӺv~Z`(Ha@qJX k4XNwNӼvӴ?ンu;ߧi;NwNӽvӴ~{2կ ;FLeE)Gl8,2e&e a΢gp;sYx}~n0oFE25(ܯ̏A첵Q'؜$3m|(newZ+;&0s7AdAê0_>UG3 qCb^iڍݬ@P%a[mLR-zPD4.,PbWiۏ[bAv=ءuyPa NTNi0Jܽx\I_R e\i:p4U%4_@]17*fbb_4n(og@NL"ƕ$hBl¼ö *x  %)"7)0n.(W̳,ZkAkXv;$]|dV8 (7C`KHb!=o ^myF w}Q1oQA~%DԽ* Q_T6]wM;:#0.U-;Џ,*:!vAܼ1/5;Q N1;d-^m7A w,;CB`X&$^\RQN*)|n +յ}I[5I] 7zPa5!%kK qz60o. dC++ GȟZ I g^6|Ybvx-tzwzwAkvr>b jҊ]r"` $uBb5r(.+-j.*z4#䂧 . hR*M"&XvNֵ?4K{/ZR A];ϧv分!Dڷvliqz"Qt\(C _'?0&q^'î h_$HtrH'vѓäU|aaO*jg7Yv>p\ža{F)@`@41*z 8KmCV۰VĠ]}Rd: ϏH߭k uȿJC= YXmu<Xqn1T毤Fl3HWUǐR^Y[p\),%7.aǾ UliXa;K=xzX,v}X=/33M>Pns*J 0J"pO]ݝ=hGvvNqA9Ol-dHzCgb{CBHfnӈ`bK givK.v jTB:T1R\-CcQNcLb-{!$0)\e*R9*9\8.! &D@sP . (X]@ͫ BѩXe.2ZtR0Hg+p#37I^'#Dqs*qc MJԻ2F6[0Eh71K&/SħxӤ?[{J>fUWEzH7M 7cw .Λ2A]PfƋTph6h,*$fp }L5ͦNɺ;of0űj,`oAoE * ֲ_Xdc1>pլԤMڜ4nѲnv4[tA5ݣLۖC %F;ü&}Opߣk8MߦZ j7ЯΌf.appLwxs}!$i9MlSZ 7#+DXJ v Xm >h+}0 no#vE> 5 ރf^Cx1C8ų4wy 5C(-.JŊl/cns`x }S7UYAJ5f[KϢto҅=cT1}FONZAsAlCUށMvk[ΎmR{WPüwF#PZ\QE(贮N@{;"%AH{1n$UߤR.[``ރpkKstg¢EM#D#n4+&șBTs8Bt(Yo#7h?C# o)QUIK(*Zأ38-hVn˟ADEOruR˒԰C FvaP]zlup@PXc% 0Q49xZe92h@q!.uG2:Rn+#Z LUqLdAt=Uya2=c ݔb^j{|j/0EXM[2 oƃ|>Uh#z:ll0FfTc䴸%+Z zpSpӦ&a@3C-hI^n")]E*#|~e2Ki⍎%*WZ h7ߣm)0- BՉpA ` 3:O* T"gĴ-LR͇Ԭz iBgJI!2?R}i@?Y@]~ DZSn z`(\ؙ]1иP^~"{, VxC1asޣ^@ߦA@ߡC3]h']#&32hNTU/QuTOx4"sb,eHA;|Xp3 z!"P쉠փsrmxXwJ%ʦCtvWþxQwLS Ƨj:f1HFv:,$|n6-,;h0ewUUG_:j!:J ǥ2Gm(}!9xGeG7cJ R)Q`qHi GIר윌,UrU@4=6o9l^T+N8ͽ/%xH DaN`xPlvbaiY-(-=#ìBiĔߊ ! 0cu>WFNRQodSv6ǡr1߫}t[X-.J.ПZF4L F R ެ0pucӶ2q@ߣc:ܺ%1^qE9՟/F J5@} )u!P״$p7gkPDu[;C:;Gs:{̲ܝ߲W0M+2lvДcݾ)AlH' n (˯o^,э prOsE8NŃǣZP5fKP)h>ɇ[W&sk`ސ39A+X;rʵtz# =p(pdfdlɿIbPJ5Hhidf9!x" K(Vy ̭!>%}^,œ> :P N. i;ADƅ;tw8S-o5|ۦuL,u ]WR]cdke%1 Q*LBI7eGuTfЂ؃)e,dk@4ʪ@Ă'4W<xQVcO NjtUGu9H::f|*]-P/<" Aټkf}EgcĪh !Ȇ.1l;#DBZ]; -ew-lD8`j `gFH[r(- q*${Fi%3{'I8 `O gedV!@SB <o^@-@0 -ۡspJ7 ibh` *WG]Vgi)T0f4hnB*%07 Jy"x!N!6]2ú0:/6E`XUªULtQCuCYһ\a4$cs@gX@U&ټ2C;"hHԡ m2wvh@ϙ+ހ2whϙRT [pX؈/@if<o&IvQ_ Gh-p@^DHuА+7ת1Cm|!-:Ow{88$#+ys.J@b.jNOit\XV$a4#䋖6[!kaGF>&<1A#Bݡ9BTKP6ሲKѼhH $KnŠlZ3̍mU,"D ]E*^P2ctFY-̷YyvgfDމq# ;ĸ Z01C{5'AUeb IZ2RL()~%; [8G~0e9kpKVZ0Z"q7E6{zx UJzl ." D + I%9ZZz*9‚+΀i"8GCn?т3NTX#wKʁDY9cѣjڬ3. U#k@`A6Vz$a@&b<L?&v9EC7%vUb{aRr|NII]z! &W]ОPDhױJr?b'DV1cAT4C A3` /3E1.odi̊ Ip-xvN"^@WDv򚇝/Ba՛̨*h#*zzA%m rbTX 3Qڄ%b4c<.g3{8^ea-) s:)؍ ]APdbHqcscјDJG7C[6}0,a័hbz?QV֧-A sK-ncCFR+M!mpn9akپ(Qn6a z&)߸|BnhmfՊȤ6NӴ(i7I>N`T>` >%ĨNP“wúc܀'|.f1rp̴7g؇c#w$ZA19øv"YhA%8>ZNh(8Yw2a~M2Tw|&;CT̂|Z h%y^ CiLw#ͬĈ (w|Qn5ޢ@T75g[+L!tihHN+cا6(@̥vO:}D[8_t?0=e:.+v)U]N<<.VFVy+_-m a;3+fl LJ)~*B61od!Sɒm#8z1ܘAyw{AG6kCAa(5c=x(k7*[)܃IUo;AD0Vk?: @2{03kuP@n414BVBߘBn8rx8:n(FZ=qo #Kw;0.u+1*i6}֜s1_8HW ܞpg<:دJnnxLt:v= D/D4T0} s)cM?CZoRKGm^J|hVD}mh! _y}dfnfq`ݫD]Ibr〷ȝ]cDxh^K}͚D5rQ.9Us<ʽxD\[DWh֏[': ފ,Ė PU|K.NS]%xbN5$_uX#.D}F{˾i Zty͖ŶczcѦ(ûoB06 +fBz/mѷhOHp |4堝ĺܾ7+1+h u (MHK Q.0QiCB +%_y<މqz4C9F}Hc?͚-q׍ӣzzeh  @OY-e2"FC|DGĻqK~+~꿉}7?O?S+{7?_.oOS>Nd^4G$輴[m)SIN%:Jt)SIN%:Jt)SIN%:Jt)SIN%:Jt)S&<2scrcpy-1.21/build.gradle000066400000000000000000000011621415124136000152140ustar00rootroot00000000000000// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:7.0.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:deprecation" } } task clean(type: Delete) { delete rootProject.buildDir } scrcpy-1.21/config/000077500000000000000000000000001415124136000142025ustar00rootroot00000000000000scrcpy-1.21/config/android-checkstyle.gradle000066400000000000000000000012031415124136000211320ustar00rootroot00000000000000apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { toolVersion = '9.0.1' } task checkstyle(type: Checkstyle) { description = "Check Java style with Checkstyle" configFile = rootProject.file("config/checkstyle/checkstyle.xml") source = javaSources() classpath = files() ignoreFailures = true } def javaSources() { def files = [] android.sourceSets.each { sourceSet -> sourceSet.java.each { javaSource -> javaSource.getSrcDirs().each { if (it.exists()) { files.add(it) } } } } return files } scrcpy-1.21/config/checkstyle/000077500000000000000000000000001415124136000163405ustar00rootroot00000000000000scrcpy-1.21/config/checkstyle/checkstyle.xml000066400000000000000000000134701415124136000212250ustar00rootroot00000000000000 scrcpy-1.21/cross_win32.txt000066400000000000000000000007271415124136000156570ustar00rootroot00000000000000# apt install mingw-w64 mingw-w64-tools [binaries] name = 'mingw' c = 'i686-w64-mingw32-gcc' cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' pkgconfig = 'i686-w64-mingw32-pkg-config' [host_machine] system = 'windows' 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' scrcpy-1.21/cross_win64.txt000066400000000000000000000007451415124136000156640ustar00rootroot00000000000000# apt install mingw-w64 mingw-w64-tools [binaries] name = 'mingw' c = 'x86_64-w64-mingw32-gcc' cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' pkgconfig = 'x86_64-w64-mingw32-pkg-config' [host_machine] system = 'windows' 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' scrcpy-1.21/data/000077500000000000000000000000001415124136000136465ustar00rootroot00000000000000scrcpy-1.21/data/icon.png000066400000000000000000000146021415124136000153070ustar00rootroot00000000000000PNG  IHDR\rfIIDATx te3c" B""JyB=d_vpaqd (B ;FpIw^:U]9s<Iw׷98@AAAAAAAAAAAAAAAAAAAAAAAAAru[pWŮ*ZkkkO}b_vp)&z8aw'.!K <,d]Sgw ,~r2R"!X6,?:aU >1"VP3γ1q6=ܗ&ǀy.ݹxkkkkk F=< ڡhgL}R{GS /3%ND,z\=D  Y7$z*Z 5$5S8G~r_" ZR|i\S1>E)@'4kJ5ftpMA@BM\: )j                          A``c:E'@rYB=Sc<$\# z<^ΈYչ4a_[F$ДTz]kwqG䅵5j4D:F{+)m?Wa/ewdCZoSkF-Ryn-3gqYLwǴ)mw0do>\ |(0h(mtWҌV+vS~&CLH/Q1&tKb1`gv[hrȋgPpyˊ4(\bتCSDZm,m!̋fɪxɮBσHAhuC+%\.ѴS-Z(K|$kqoʮ0ahi}-?<}' x+LGK/`ۤdq̸U~v~"s߉9]7o'U%?&역T4ЖH/f `A0h o7:wF 9C7*~>hoٗ 4z[S0`Z6^i>ɸP`vZ&Ecޫxm`[ܛ0%-a dze7,4`䂤F~LOKv`nw,k47c~M 4`̮0{664Zé=^hD8`S8  `?br~.,1W)^soߡ-?k7~ܼA_ ~K{$Zی*-Mm\tXls0<}wF1 ]<;nt}s.E%W/RKn'ғU(nğr\lt!R^Uf<|i&9̃Fl]"ۤ_;MJRCƿ P jS@归}~pRpdR J_$ɒdÏR]y&o\HׄT/gy*}a(=~ @mI7,9B{00o'AFt4 P]?3(&}I$Z|6_mPx\6 'GxX}/9;0` wqR Eo[q?hՃMwYl:G` t( К1ҫ?n%'CʬJS?>mmPUmZx< vg90m3Z0hYg8'<{xG ]j7),O]y6зGvgbΉ11rifι7@ktbЗ~uOʏ{$ߋ]ca7!Bį`ߕLs xT 5 fd@de&*OM=OXE.<_70oMTJgX- fx"qHmTۚgy¯a<8F{F|'6o z=Dއc<{!8 @ 8rv66Bncn.Ɩ7a0{xit' bӿ- W1A `ܻG?ͮ,Iw-(Cn_Ǥ5>~I _ `{bOcV7b*` 1{ٯ9N  ?N!i'n_7Vf>6omC h>;pҡD- ]\di;I ħʼn>[KjX !ئBrs[+s>S&Qd6)~6]%Nm|0 <$u8 <~0A<L* R LJ Cb~'IoH  "\$P$^`\N (8\0UH6DeSXl@P 7$P<@e @^BEru J_q5 ()թ3 `VxK+ ( \Fd@/90 4gd2ꑰ uti:@s.Pp$P4 j> ?$ }Ēc @ׁ#WP=>8vJX@ x@ Abp( i0:P߼ ؁V]_GRJHO~b*,,@8v,d4d>;98l$ *ƾzt !S(l~~!áf%_C\_PKO3( m"בzPSc7QῑP $yf8^m*2%$C! Bsџx?bDTP %ҟv9 \A!hP0(Q4; X и7޽{Fh\ xH$&& \'A B[۷D6mܐ__W9 w4aW^F]|P?Kw 8p)[ݓ_ 2JMM ڳgk(/]X6qMXg?g0!ݻwý2fdd1ua=&2RftR]_,~jN9mސ$^pT.vb AAAAAAAAAAAAAAAAAAAAA "eoIENDB`scrcpy-1.21/data/icon.svg000066400000000000000000000110241415124136000153150ustar00rootroot00000000000000 scrcpy-1.21/data/scrcpy-console.bat000066400000000000000000000001321415124136000172750ustar00rootroot00000000000000@echo off scrcpy.exe %* :: if the exit code is >= 1, then pause if errorlevel 1 pause scrcpy-1.21/data/scrcpy-noconsole.vbs000066400000000000000000000003241415124136000176610ustar00rootroot00000000000000strCommand = "cmd /c scrcpy.exe" For Each Arg In WScript.Arguments strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" Next CreateObject("Wscript.Shell").Run strCommand, 0, false scrcpy-1.21/gradle.properties000066400000000000000000000013331415124136000163110ustar00rootroot00000000000000# Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true scrcpy-1.21/gradle/000077500000000000000000000000001415124136000141735ustar00rootroot00000000000000scrcpy-1.21/gradle/wrapper/000077500000000000000000000000001415124136000156535ustar00rootroot00000000000000scrcpy-1.21/gradle/wrapper/gradle-wrapper.jar000066400000000000000000001625061415124136000212770ustar00rootroot00000000000000PK A META-INF/PK Am>=@?META-INF/MANIFEST.MFMLK-. K-*ϳR03-IM+I, dZ)%*%rrPK Aorg/PK A org/gradle/PK Aorg/gradle/wrapper/PK A%Ӧ/org/gradle/wrapper/BootstrapMainStarter$1.classRn@=Ӹu1Ey@_Iik6U@.,RuL!G 6 (ı ]t1ssչ_x󸳈ᾏM|4isZ}.kތ=ed/cΥei0c =<Ob+7.PmB%M -wźl6i<Z293#ұwںl hx8FEv~ė9P%pw8.R3S ѵAc+jgQEoN SzBzj\: zQ^oPK Ai,$ -org/gradle/wrapper/BootstrapMainStarter.classVSWsٰ,1*BI/VV(-m.!nvfjﭽ< %2ӗv;IIbٳ]7>1IF2^ C0%cA0#aVF d\{2Ǽd\UqBNPp]R2"!c7%|؉^|$A aQʋ .aI~ ee41rtSwN3+ U U䳕"E(ak{wuT-.Z黶9rhu48C, -펖64:n=n/whviX iFZ$K]hO&VoFWc-MDqhƛ:UTT;#RNvRzH$['tk=e&śoh86lsܕP=֣ ed,X: ׶`Զl"zϼNE/wۼ-j=𒶨y1/ [\f UevS/7,xBY|͙ZI,¶Cw\o4 Hn"#RGw Nb(Utm{ۣ*z*ch #9_xWchyG}ը/CCASs/A݇~8# ER7q%B# ܤ@-8q;Dv{K0*GcM%8NMUK;I1D %ĞↄDvNަDgy|px#UcU]@}iP8T(j@ 1*B qg)&r⼁1 3D\>CGKH/|aA2fJe\㥋WE+p_,^qFE ,G'mN3OB {C^6mW\ :i{ELqߖ5e8+ :NK+Avu?QiZnYZ|]/M22'J2Cfhs)hn9ʎ2e(!Z ]o/唠\j-V},OL:^:7>r(sEUXuhJpSq pgfl,xUڒet=N-b6[؇ GO aa qĢE.aIe݆6>u鉪b(g em\.x!?NIo^ENS7jmj jhx*ܢl1U`dW|--r*ƯP瓹k)]0m+9[RZaˡu9C!mަuR3tmf91t5ˡ5$0Ƀ4Kzxj,ջPoZJ[F$ xF|EH_эC꺍+\v9D{Lj쪿!>f5[2 ƴ4?@L.d$+M:7tKXQvÏ8q& vxIn=d ]:G{ZSP'Ovciǔ΁}OEǻ!s+UzXb|\;oOڣ1}:iOM9nH5{IlI~j3伙7KXŔ̓Ϟ(Ɲ䰓 ܩFQ J, ]J΢Paĥa;l=L핃qnJv܉s/Ũ=F~y^#,ܤBMvXTq,*fQeɪvv`~$]g3V궎\\c9EӉ$ ,XXV_K8/hD* UT1qwrU$G5![?0mJa!1i&;@۾Da<}x30SmS ^(h[JM ϐ]/X[:-#{-\;->dI# {S#3Zܱlb`*^R%a9hQ : oit[rXx]Kzϒ#oHĒ2`ɠcZ ~)5m qJX̋ M۩P6eqCNX򠜴!9%![bqKU⒵d-USq5Lp8% =<_S|͠DhNI-YWBʒ3BxlwOgh 8z/Q,T cY't=p #Ҕ7lFYpZb1LmKEtAa]%GsjaOv1yLJ% )yĒMmX[MK\IܱL xmY0<S <z%K[IAHQϵ Gm{mvO9<$}}T.7kZ'y|m}*4u'm4'P*=5i Y@rݥڐ])/*-[jX mgs jrX}T[m k77BS;ESK.ȓ2`[[ݼ/w{5M<]9~C>E]tQ(8vUWjZW~?̝?¹c_+OϮo52XŊY 2XijzfdtAO`mঠ';)TolWQ BC۫*s fY| eqϠzCʪiY͓Uϼ5i$f_AAreU-/̽K ڂ0 [o;^Z]ʠxq3TkMbǽ\G {v%u";#;eF'9K3p?w$> 8atEK/#4;:i 훤".hjb6$?8x>=g3 $i~zq?gU7_BJl UH%.gP);dYQQY(:[ ߇*G"$vxZD:AO  pΡҝAtW|U0}c'\LjvM}`\s:^2q\6UۙہA9| gtf,&ڷσ_ҾyZ8E™rW99:xd=5دxPK AyL1org/gradle/wrapper/DownloadProgressListener.classu @E+jDE@ۖEERI#3c[>4v9OĪ.r%J[M DŽ,]8kߟ_PU:]3aG\^&at"-EeY˛8\3K tЎBNk؁PK A!9| 3org/gradle/wrapper/ExclusiveFileAccessManager.classWsWYګu|iVMNZ*;JlBNKƎ[t-vjSB[(P`x 3L$x0 }0+9%L=sݿOt]o3;6lKqoF3VB>%lW !8$` Dղ3"x D]AمEU WxcLQV$Cn.O#&A7zx N5@T3(zY4} ] ҳ4?Evqݏ<̏ LNp8>K.1Dg[A|4nkhLpSESv\c~]+蘭vvR5dw=#r<\KT8lo7`.ՐS WĮT U4]~T76v7BZ H ҫ@'c Klϓ31cDQDDwp>Pzp}@8B gmγ*& _$W +kr}FA?~(AZ8:ưq8 ӛ"؅Qc? z5)qjsDPh hF9҈u>c~iMȈvz5HM $?b)nIP$)SݳhѠǧB<q0.Ҷxd(WWw@M߉߭5S< a-sc-gONQM} (1#j R`뛿̀imP/QWM:;"+) Qgc@'Q)f! Ip @"pHwbo8=N%Ťډ;RN 8C?ӕ7oWy+V b3Bųl9ܭ9**$U<~PΫx^Kx{TJZ\/SrB+UtɯRj+akZu ^x 7)xxxX, *xw]rxE?.*>`XR0F|HeqE]LJ冏QPI| g<"9?+!I>//"؎ i?}GwvuDNh걭Xr`V3_ PޖH{[.˲g_W{o:Zbc,7  Z~JĒƮtaTҌ,gRK"54`q錥 VS3=v$%+03|dGn cY@"o-j#f6m VY&협2M-Fފ!JXg4kj5 =/2j&OHnX[\6sZǶC1!ґӟREtk_ڎśNk4"n HezE!,r_M~ֈmrI9 n)\™Jr^PPڬB2Kqf-Inڷkr{%mlmS663o,==Srr^7V'j[&F!T{̴5YrVHL6L2nG`#(DB7-P2&s Nsٖ |YjOS1X; ?5?V ̢CԩD ?B/+ 1kYb63R1Gs%qB{(,2\;Nxau%䋂g^W4Ox l؏!!vB`gSPyw\|w޼=yi7u4WqS.9,8V8s\[>$7{TiWGPY:NFQ7 e?_Fe_Yz2A8FIQrgA(uN#p$9&F9J)2:r@0G>̒RMŀmC|iL u1J/ٴb.%׎rB9,,G oE)2[;nV,ܣtԻ7f:5`jdؐ0[|g[qU:!lɪ6r g6[ aaO<0ùuPNYpiᬎ;Ug$~cO_-|{{ u)Gg>' ݋n9ȠS=.ƲPS9̵qw.prZ1b͊YS=Da(<,`*KPK A"org/gradle/wrapper/IDownload.classE 0  ^b* A{(l|JTf $_uX! ai'$|T:/ ?&NJ@XaXQГ7&zDVʙ&6X`-6L͉veLCz~@F@Y%H')9~<~WxL߰"Ni avyܲ'; WQ?/D\V+mĪM˩NjJjǢN4 55e-$| ¤iuMGOhSBWt%+i/ƌFKL\гva:Pљ^hw4*\kQ^\)6jkdQ|5\$5gʧ@yЊp6`&ƚewRr,oIښRe~멶\sngBs`2ҎKu1j4ZIWU|WP1PjYmlj 949U[:ycc)GܧvN45gjּyΚ !BRgyq)P1.(e%OWUNqMm;֚guhwX pg/0aŧM[˦mi9lHvX(65c ()X,J|{vg>BT᫪DTvjt7s1`t.Uz{̈́ze%ؒK)y9X튢u,5N V>]E#+K%TӗN >+*)P%ӃM,r{z77~;|n Q]]D]  Qfj-VI@wĚAO#=[`=eYDYBG7S2_ ӻV}\6T/z&pyg"D]թXB~i'>H6?e]G1< ˧LhYQbWfI+" & kX|!]ۗx+Z 3N`yte%XtLϚpp}';Ƞ.zG?lnyjOඖ@,d+/;m.PǶ-">G=dk-&ⱶϝ:ul ?g5 t%R}u2ei=[Ȳ"!&n▉Q\3a"e@XgX#-Ctۛ#a&QT Q z"H4H;4&('wJ:4OwYJ5N1. \ 0dy60uC.eQǨH~Zp&C%$,FٜB2LfL6=sT+еOТ't!uk*ߣ 34U xVȋI_=f%s &3lxѫwv+e/u6W.U1}d6PK Avp.- org/gradle/wrapper/Install.classY |T?'ax,0lH +aGb$%, K߼1 ]ZjV]肭$h*Z[jmk}~~M羙$y޻s?{-i/KS Lp rH< ϐ l>Sp ೤qq|_qIKti\\Rz[-xq ^:^&˥Y!t*exukA~VFDuBX"^A UXlIl[N٪q=;cgj]Ҵyޥn-|~O6_}#n6iVjljN&sqvs({fecp"|88)*NRG uUo<{y Ry~@4A H'I#X97씅k(IӮl1ӊEIhnP4O8-ljN2M퉔ūZz}ŠQ$?nE"1+!i6:c dQ:5!%T7K2R]"3?D +u_ F;#qlcSR j%Mo\5%Z.LR+u,>c[.NFq1 Y^F$!a9_2rS13=J,o)7*Ʈ@?'նDw_NbZXesy˿CIZgGM$YWzi[sq[Npe%zđ j11Q9.RT.ihnEPwH'6(aJBdvgt֝J(y(Kt+v6j{7'׳vW7 F\86h$;^]𵻶TnR-:ڢ$zuR,?ۉ ÷@%  ^CE1|]^< иA ϧGH?BʏVVqeGhaN'?ڹ)tdGoZ9<I"&2zԛAut=zD$ ϵ4iMeDW{C~*j?'4,*+>HEw:sd/RrnpVʹnOA"ip 2`n}nf3gsK3Vbm㸣ߍFL;JM4|f4yVx'ӷվBoޭ42맙+[Zy 䬃CIK@E?YpͩyV !iQ> 8JsOgyo!E4og'];'dwBIt6 ϥ(BP@bxIQZ@12y$fYTJQNVn @녉/W&I /ax~ Sv .ZN_n504 =_+ٯCtdnkt' X6~jT]C&^ё(8רM0•4xD2xTy/-`:@S JSi(;1Rs+*Gyy RAh ,S O~z_ != {\V$ڬCQ6\.{>L5e[)!&Z .Y\S 7xѱWԊվAZ y?#!If)g3ȵ*dҷ+;xSC=RI橷練>( s5=u|ፃ QW-iiZǚ&hRyli'9@zEH|/Vv ~M« Z-7.|((ɲnL;<漬s# gIL1~?f =bIcm~%C6=@ÃTR^MT⿟Z<z_<@9y|8rPZ?H[nMT-Ձ(lub%hȏbmMiv^SwStWh]pA?G`G?]O6hzހ$pME{=ݏ(MՀY-`0Xo\F?'Qcɓ8a_x 1fDyp݉ I)E@u\J[iP{S|s ho?< =PFA Bz. z n ciTmԐP8FKDB$;'z1]DtKbjzqZ~jo,/0#vsY9e:MT̊Bz&+UV0[G&Y)@VZDW֛9t!H 4WKX֫/fZBF`1^ŘS۔sJY?(?ooy֛`'KD%AAŲ&(v1'fHnӃt j16@Q-ҕ R4OɢPo.oA:l 'RL./k&/MA~O:A%X >$CWy-ױO{+2՟5x|2j?c-I|g}=5]ˡ " i8;NUņV5zP,y , Q|fb>b&pjrMfL+*Uwp5s6S^JXS>4'ZL8?g/ZCYUkjwj, [D*Kmtq*a'i1K,co W~ %B!4O-eGvg`ӝ"VT>uayq?^@˙\NX~U̹v? | qi3ou$-`fc<]99@S&;;eM*Z|<`?4dckqǐ{0#bvtUwxQ; 6m!W\5]1,)@F\s ˵w/'.Hqϰ3:NWfu-Ce"!lU#;m"x*@obw=Cj[$B# %qcI![3̔JgX&ovHIFi QV(Iؔ)r /,*ꊧP*qw(P&n_ݖ7ۛPK AJJ'8org/gradle/wrapper/PathAssembler$LocalDistribution.classQJ@=wj7EA(*(mۥnI٦`|(qv[Ī ̙3sf@Y0mf]̻X`pk\&poy @bW*IŐޕLsMh)50R8jT:㕐2~9+q/$W0fj%Zd8bHb[-A-XՃPw7B'<ڧ Sk? ~]$_S[GӸ %[!Ep=Xb(_ 1*BՃJCT;_qiXOLZ|- Fd ]PM~$ ov`?ghYIAk #eV4cxf"ТxoM@^R'X&@0+Nla)ק:#]PK Atx7&org/gradle/wrapper/PathAssembler.classV=cKy<^ 6vP@dIM2]o `FDD1$viKiBiBW˷M-;3l!{sw7p p?;0 MU\H*PAa/aJ;Kv%ۛ!5Am%-lce:i=(:BbW]S }B]6K& *? $%Mj\.WyOY xH#iXƓxJ*e<,2~WdQ[bJՔS~NAI&KbU"/z-~'20O>~PdO2ʄԊVL V@ilQ nkeuןCSS*DuÎSF5}ñ^Ɍ3mrMl.nn m2)ҝu4Se5RrRF5+tm83=[~xF `Stk/ֲ"ϦttRff;>k-VD/<<"JirPh)WeSpoDL޽&ӹqy|r?r 9&`$*B˴L`gn7_7/sWwrr;MP,."bQqՄUK{ |˲ղyӠ[Y7(=ހv 8 Uu~3IP~\Տ( ##x$SyLS>#ɺoM(EF\CbiLo%D5d XЙ6U+\C6:i%Wa @C=5 أ٣Jơ6B5U "Dc PF&\.zݤ\ :,wPz CR;-ӷ4Ef G I!Ҥ VOĉ`mWqó _g:ƆVK9#]Q۶͔nv-mνv^g-*6`Ja#Y" GAN l-|,g+[ *DŧL縥 |+||+Xyvy)'1MnBʶ6w{/(X,(~> Wٹ## I=(DWF$v t$0;/~*xBDV` -n|/#)yeǤ2FIi&^ʫ,=A5ٶTmMfRص/}6-ƷbU $egRevX|3%_ ๡~cg5hOW -a.5T cO_ {LP; e3y؆:J{3Pw@y42ESp~Gw"03V;LM`q=4/Z>u`iN]xG7Y κcA=#7? I4lv14$ ĨnqR!9KM-z9BԯrW)5" ,9z|~_=/!~L08WIH-_{M F1WU> =SpL<`ݖ_bqWPK A=?-org/gradle/wrapper/WrapperConfiguration.classmOAgJK|i " HULCߐ-H&~*M$&3w e&33_:a`@EG xL<2O<%xN8mPk35 QOM5)!Bw~+ 1sM(/;0/w^9sܰ nZd,8t|ZkZMlONlڍiv,.smns =NN8u ѽq$&L*A=HL)BCO*A4dF r_ s )%Ƞ 2)AeE, $!X yw y%^}93 2y~Oȸ`;)H^V0YW^а;98147243Ot KNj-;ޞ-#;[N3}Ht e]rbbAqy1ypȖmGCN$*= 8%g9ݲ =/az629uȐ Nl=qԤ1.aCi[Z:k?̝`iafRݘizoX4֔r!vSF`N3M-1z_arTQ5c!2xݮ\)&* ԉ^-ޫcd {Zn! ]$E锞s=' r|˝4g9yJ YE n ts@fJBU(E2DK2t!cj'-BFY&v\vwdLmz^B6i5~g#M ;{,5;UkUD u=*$$)-^QI?dIe ͞P#1F/Q0'i?e6!#(莄1eL%|%y,dKܢ"|j¡SRqm5U;^GQ˚h&}#Y܎I7w<}EA'LQQWMUόD2 }ʉx>1COG)7raWTN{Rӓ|88aAXTYoe݂zfuytR{:v:Z|`G_8檫x8󨊣~̕7:+x@-I :bqJ޻q(Kq6۷bj &};dm?JpE)@a ڦŕfhr%Q2&xglL->lDM6uFgºߏ C!̣Bh^GЮ hϳ_F~;=U7Ӿ˳_0kZcz8Ivzv0ixUñp,sϢfa\s oе(C BbjJ%%@,,V.ZX@3#8ꚑRM`:BBNcX@ xe,^F88isO[`IQfQ?b^ Ϣa ItV9gw"3X6;kcs9['_@cp qvC,bW/\ťl 9OC(.H']T !ٞ ތīS]hDLyW!p 5 kN=NRnp؜ch⟐ϔ4=c/-ePK A׃X ;org/gradle/cli/AbstractPropertiesCommandLineConverter.classV[WUN20 HԄKKIQJ;L0fOZZ_tA}N.i2Y9};vO6Q0e|`|`w,'? H` = V>Bx&c.cC i?]cЗ1-ci8"qB[׊kcrƔmeBhЋfb~MVd8i ޞf'$ F]p䙶0fv}-QԬB"9U ёѻE#0ff[-PvT4V5%\X3LXQgIWW]p0?:ĒVJFOMA`i ҮqA1\W@ \A܊A)5"ɓ48,FhJ+g ^zKŽa@7e2ZfYTRQxuWx8R E10ogm74Ӂr_q9\WOw/jhz"#z,Zi.ZFkP*؏$}iB+ c na]bᶂ1|( 2>R1>aʠSz5 xA$>UDP~.*8 %>Ä;g+ZEkߌM' g6-0 : NVϟ=!!{LX`e2zFI3U\%Ne5CC]Z]AၺmѸYCYbZ.+&%b `)s4 "(({j^<Q*>\{#%N׃\&9I~r)C09ymjV!:|k"%*5&@TZ[AkI )Zb=Nʆr2j s噜(B`4JY:TCaecU*b2vTzCɘgKEԷNR9r,m:BM}V{"O.6u1᳠~-g~hWKt/Ovs }+ =]hY,l$ ֊7 4O? gԻ h;_!ѸwiPCEӞEVh`ll ,[m'GT&p DAnE!i/8<~[$"V8zb3%+>g*h/V!r'em3=:Irn wN0!؊u2aIPK AD&3org/gradle/cli/CommandLineParser$AfterOptions.classmOA{-r- >W+p"D!$MFxag9.D%2^rҒcffwn׷fqۀQiu4h IStLaTzj6Pɓ5&yյXOlե}G8]x2Crɫ< +M[>.YN w\:JIi0dmxBBr卆Mu0r$fu~^F+[.5k՗-o0$z<AekͨS bVgcg>v@]7|- ƪ-+CG@ϔ0 jE7 7ckމW7xؒ1f00yd835?UѶ= q~ :5hhkmq_2wC^2 \ G@I]TGi4gL,~AbT 4"A;`ȑm:C٤3qhboPK AMu <org/gradle/cli/CommandLineParser$BeforeFirstSubCommand.classVrT串:I I|skiK^rը,Ӿ[2xHnbN0Ϲٳ=_~02Nc K2ȑ!aYFø ǪP&Cu!%6>1u(68 woz|a,oٕLVzF5VSawU9^6Lùps_u Wݢd֬ml4jeݾ$-Mn!M$2 -[au(7m2(MSתjڢ/xWCiX;PBNg, YtT)}3%l1T3,zoKf=H9 T_1$wbW5t?fk5s$]%4C Y Gp|@EYx.~H@]*[qrj;;kG柮G&İSEQɪz|q|ypj( mja8 ˔Ww[E^lq6v(=WN##rNYS.8~rWßK*b; Q"QiD@r0M[Ҭ$?!HvCIqf.Ɇ1JᮨoZ }4{JEFG?3x/J#'Gtr)Ipg{(EOJT).>Gd/-GWZo=sP#`K3Q[F]F]pՅڇbYh@~R (!ijJ3n8B# gHWܱa6:Nx*9g\.{b ׹ڸi^E` N$,SDgqc)/hRPK A*ZMForg/gradle/cli/CommandLineParser$CaseInsensitiveStringComparator.classS]OA=wPRˇ"UhAx1MLԏvn/`|EkC9{Ng?&6 Q)`,95=K&lfc*!|m2]Bt%a||ߓzGy)vV&+"M?~"u$ۑV~Q Nnx" %6m܏#nԍ֛uB_Є"[f)ְm*ȧHjfp\p ߩLbs E"Ld'h@Kӟ0}1/d MXEnqsYX'>9|r9\=wP5oy$L̑iftcye}ly:E1k-LYn3q2\uM&:c#2Oȭ㿈(&J`R؁p6k0,X\*wM/PK A|R&=org/gradle/cli/CommandLineParser$KnownOptionParserState.classXwU6ͤK*EqDt;VkEE+. ɐ3uf҂HQpCqP![>u\|I\@-V< gZY͓5ͨ-4Τjת 窬m͵l Znpi S5꿗$뻦 b^)Std%fsTCP`g^ t H@'؄Nr"#^hNoGQ7AUߑt$ YFRk|yaC@gpPn'I<:\(=Ěp,᝛aұV6dhd F^wމ& aࠑW~qpN߽̾2BI 7!?PK A$ľ<org/gradle/cli/CommandLineParser$MissingOptionArgState.classmOPw*LD ! ·'`0W̚o;oB~?ܶK˒s~퟿~Œr:'I*(uZG 7ٌqeМ-rD_K.]Sz3Bjs5]#ņu,nV4\k0#%rw[cU:3 WZ S69^RzxZsu-˲V;TJ?nI/GQw|m.UORn!eCw&à# ֻ @~+:0r`#:L:՞ͭ1𦯉)[PR2q\U%7ǖڒ#63Vt201,K`=org/gradle/cli/CommandLineParser$OptionAwareParserState.classUn@=q$6\%)MK@)p)E E*oĮO x$ J|Mݒ*A./ޝgf퟿PĽ8ȩ0*wTzc\ *L+(cVAQu4ܮ,dȖQ/5̂Ѱ OpC{i\e˶; ` CdթQIh5xƫ 7*\X.Cɶg9Zŵu6jI,s]SMj%} P녲',4!E]&8c$${]pG짍oRNK%ti!ӘЏSnbX[XPa sLf +~a[f:~Z)9hOS csmqr |FdUԩ0L-=ƽ_qkޥvrl0.ϥF|x!JӚ'vt!}rOYY,T6$a|iZ ̋dKTTȇC(Z7D!eSZ2B#oo+5UcCݢ{ B*^v#8GNi$y6+1E83$r]oPK A%̻7org/gradle/cli/CommandLineParser$OptionComparator.classTmOP~Q: 2@.,Ydďe4kI?/~#ƨ_ N6).]}s=~ E%d0#0+bNUJ"E,IHs;'"V8|USk"z6L7eA(;íi;- Vn)L!{䛎]vG Wlp˖y!P~`j2U6uyN_z`QWkH-T T|%|4y`P?UKjwMkwuz-֤$/0L[3xѪM7-O7 IRiu;?ڑ~Sb@ZaBhΟ\ia+6eYgљ{"!c /&q va[hǂZ00 }m j]{a=eўB$>P}4s2|=ѧQ*te躣K.@ >rd d$S3gH|$%!zp'a!xD"2{9 O蹀Sp HswnL8trI"!y3Ⱥ2GYv>i<2<l4!M{nV  #fdY"5>{}FCNS#ZOqϣHINQ2(R8˦Sqv[lB|'9乎OE;McBһ-!%."{N'MzzY%B65[m~kETBZƓq5<]/h}LѾ>McGMHx)PK A`M~U2org/gradle/cli/CommandLineParser$ParserState.classSoPN) s97ս&MLf.а.vF'_|(- AGmdQC-6xbaB{I/zI4\jϓdbs ?4iF^H(*tU?'tUav/Pcgw9`2r;$uTyP/#}@3I8c)O;B / Z &Wti2qfYC!zPڕh&ߗP2egvnӁ$$T@|*>A%W|):T Џ3_qI_q3!Źt^= xc @C!:Z_ eYq>8YMHEFw@@9\"!vCz1v񫮥x&҃k$}ˌf!uTsj]p99+1:*4*4fE!./1Y ?PK A=l)&org/gradle/cli/CommandLineParser.classYi`\őJ7zzeٲ=`l1S>K6XzFψ8B``d MͲaك= !l~ޛPwOwUuUuO{YT:jW5ױh/>)s3 O >ס yRa#||1ׄW%GģcxS5E]%_fFeu֤Pt|e&cuq8&&䞬JO|7#P"tM$(L8?=cq3֔&LMssOA0eAQC(j]Z-um 3f5if ≦mkG0JKͶh̚dcu;13cM0[1׬_ݸa5u֭X( ^N1U&VQQ[٥=IҨ]k G-\FګmWEc`kجn V"E9:Gw6v1|yqe41P<-l҉H(H[Rs,Jؽc[-`,~;DG|Kglloi5M% 9.bI+v"[FURo;R`FN3#mL= 4P%$lu * cLate+r"nuUCiظ̀J]B\ kX5Pj:)Bp(^Hi%n Zkn ݒ_>N &3A32g R4d Z8?lyOm'ˆf@hՌ%sƞUu63iʭ ŕ%p77DL^aE9͚V\EW p vX8)4iFf(Ma}.l?F,N@6 7w3§E5Fp J-fn1R^:t[BYԙd3R÷M΄A-Vvmϼ.(SYnZQ_;S[Zx|ڜ3KFR^9fpUg sPY1O/ }MX k#IRr ܊[ ܀ ܬFD- ;&-i{OP<1Mڹt\͐׍ku;5nHXv!]r1% 0Ґt *1T]Bحt-씸!ǐK2C.+%ª2C!W*or\MxղZmU3Wq\lE,rRz[UV3[r!7(G(Wr|ېjCn*%*Ų!wYX2u]4yӇY <'kP͝%w't3luY9LُC'dR{23$* 0d\/!{^C/hr7ܟŞkCPɈ: \lȃnяEb`*ƹc&{>Vi%=f ٽwbeic^Rĉ^Éww` OF3<,dn>g?_y2|% zX>"Nd1VfY72pLbh1s}6=JqŴ*9DuXUǻ?}e[ւeN(q%B=:FsWz~-)u+Z v{p,|{`8E< r_n۷Cc^{[܉բzv 46<[]kt6SNo;:ԃCqֻ[c`|ݽ(~zvV7\e"z>LP_kp6#+Lh1I`E4c/]AinعnV0=tziH_r3 %?~u'\vB A 7o}mh~ ǡWs _&%@,yő<ހ M3y⼃N wnW})m^YAhO YL+,l"G˱7<7hb>xi!_ph 9Q}( a`F˜Zoeۋ局v}r8i7N"eK{1\NIAU[k|6e)=LgitE:Rt=("4RA~?(N{,$)>ȡ)Y>gGK;Ȯ,J͡%TZX֋rE\ދ O܍€?i.UMj[W\V *Rv"uGYxm;>,.ӇK֑lwlfe/NoE'QbWb#`cm3< ؊_"K *#`x)hbtH):Ll&evH'҅.M^Q~;9˸T7C\)5?z9sr*hp?ۻ0=Q֜F {twtc1~DkUa!t0F!SE/wXS'Md_ø8缁羑gM'9K (=SX-,3,-N=uS9Pi Qc"%p;°s`qx<6V~,z*-wFG|:%f)XJXBV4[J^ ϑ^lT{ pg9܅*+/#ߒZJNxy*uìR6+|*,&VUV} 5^ƇbiҕbYЇ5y\$h:4"i4hpm7H8T ^Rx 0K%kAg鵓r7s~!o_ǜ9`9_K_$,+ J;~);܆%!Myxw#){9%qG2wR0 /r5{,5qBH\ d^pKPΓ7xY+b܋7X:yxY絲F园ɯS-18gO{zSe yNva`aZx 5mCd"QLtLq:ɛ ec%,3w]}sS}~e?2<_p媲>lJ0ݩjA^i uoQEpê8~?oSw/-Oijwg޶K/UGy^}B,ZJ,;;ɢxbf9rKa2J|ߋs=ijS8TPqhQnsʃYQU@O~lm.I=oA`2/-'a*o}CӑMswōiV*,K:&U@cHb{V5kyE=!#&P== kOц?r]?1>gAy{8u ca*^H*R!Vg-yh.#fN>_{EV43$ffN8Ѭ;ldO sN]wai ;{g>.a.=dzA#_;q eV)^?PK A>&org/gradle/cli/ParsedCommandLine.classWiwg~Fċ%vTMc[R8S)-LT(.Ph١li7 `rX;Ɵ}g$Kָ1G}.3ǽ?.v*`!*΢ .bufT,.+*WBM0#h'".T=^a}:|Fkt 0^7/㋸$__V0AVMa q[ ͼ1s#+3ɥ/E; f2n-C#2iҖ U0RҒKMfr)y(Q0r*T>c4yZq%sH˶YhM-C#\U08a~.k *)CxK )LAt\гkWYp#A͙ FA|Wݳw/뀽)xV5KԶx:ԕ|)#wOPccI(V^yU6KbѴ\j3TpUG(|VQH1M 7S8 RΊ|{Y#۶nQ/5^kKouO;=Fm,Rhq35$bwcLOFܙm Ln"9mG7Mhz*gJwrv詔DճEc YeyK?a{?՜qZ:fyȌYcm5 ^Xdkޏဂ6Y"|P[x[w= 1s=>4?RcfUDO38*$c/GW*S.5“~Un |ɘ_=0a 4 WU\0~pWLHZϖ:zypy*rK}jc븪֒[v.2}Ric9SVUPPIΝir+9gS QZyۇOBMkI'ƠG> =7rW=¶~̜gD1[f(O :6yP=𸟳J8Je2msQP0-^^Ц5[y?AC2?TjH܆mo"@1H1tXG1r}kL%װCРh&4o! ;]LHҰe(=Z[];7d?k'Wo A؅속=x5:ICab!Zcx(Dp1')1a8!N**&T=thT_P;='X/ FΙbX@u #Bwvp\}b9ûx?}dT}HH.o!syVOCI/a$qaxA1cx)R_J@{& (+}?Av3xG f97CTEB"]tF.'ޜ軅؜t<õ2i 1i_3%6 ;Aؓq6A-V4RgZlG orN$gdyE:+N:$̕ųxU '\5B%ǻ6 Z3]^ᜐk>lJ ;k>e'*6zEGe)A<],;[S;\Or=l8>Y ߀^9ي NLvczͭ&@S܇Қg\HVvWc .97{XŘ㰗Go )ȑ,I8sQHI 0$Abs9{`_i*^QUt  9%<' tEB^Krvs?`A^EAEXP QP dȹPK AytE,org/gradle/cli/ParsedCommandLineOption.classS]O@=ݯGePaeQ"hHV1YawRlIgN[`Y$ә{=?“4(H#b 冊$4-L%q[器+r⾊ [lpWAɶ(Ha1p#-g,C/{9*I Kxs UEJ%a:w^u"]a*s!"k~E쳟}V_*bTG;-%bXo"p(=@&_2w%^*$APPS =1D:!䉩If%ޱn05W(vbRD $"xj2Dϧxr7k> !V.p].GӱD\8"y RPK A\vB| :org/gradle/cli/ProjectPropertiesCommandLineConverter.classKO@D|?Pâu#Q+$C;1m  JW&.(1D,9vo/[@yl汕G)v }FHWkwLS!]nY7ZK:̿cJDZRysV;H+-)nkS#cruLXgh|BjFYDΏ%L%񎅎*_?ֈ:("<ڄbJՍ ؊tf^*K ߵ XUVi01k p8wZ8T0g?PaΛm=C Ss | 1\Zq-}C_JEˉjE+ w'PK A 8=|9org/gradle/cli/SystemPropertiesCommandLineConverter.classJ@ثmjE5BPąR/P~ӑ$&BJW 'iAY3͜l "lYlE <& d@HgL{:rRs:C*X4NĬQ ۴;hZ3a ѽG!]Gv7S"5eb o}ɸGtFMz9y~X{()spL`7e.KV, TXxɢfDTEGPWJmh~49AjxѰ sh gԙn85].FԒs9Q΢*s/@Ug J*ce+s+1 $p6/t-,;h-.Z >kZPK A?gradle-cli-classpath.properties+(JM.)**+MPK A%gradle-cli-parameter-names.propertiesPK A AMETA-INF/PK Am>=@?)META-INF/MANIFEST.MFPK AAorg/PK A Aorg/gradle/PK AAorg/gradle/wrapper/PK A%Ӧ/org/gradle/wrapper/BootstrapMainStarter$1.classPK Ai,$ -#org/gradle/wrapper/BootstrapMainStarter.classPK AhQ}#org/gradle/wrapper/Download$1.classPK Az`9Ap org/gradle/wrapper/Download$DefaultDownloadProgressListener.classPK AVUPL4org/gradle/wrapper/Download$ProxyAuthenticator.classPK A䣏x!rorg/gradle/wrapper/Download.classPK AyL1org/gradle/wrapper/DownloadProgressListener.classPK A!9| 3 org/gradle/wrapper/ExclusiveFileAccessManager.classPK A,y-'org/gradle/wrapper/GradleUserHomeLookup.classPK Ac67 *h*org/gradle/wrapper/GradleWrapperMain.classPK A"W4org/gradle/wrapper/IDownload.classPK A9lV"A5org/gradle/wrapper/Install$1.classPK A $|-=org/gradle/wrapper/Install$InstallCheck.classPK Avp.- @org/gradle/wrapper/Install.classPK A:o4Uorg/gradle/wrapper/Logger.classPK AJJ'8+Xorg/gradle/wrapper/PathAssembler$LocalDistribution.classPK Atx7&Zorg/gradle/wrapper/PathAssembler.classPK A| 0aorg/gradle/wrapper/SystemPropertiesHandler.classPK A=?-forg/gradle/wrapper/WrapperConfiguration.classPK AG (iorg/gradle/wrapper/WrapperExecutor.classPK Ae #rgradle-wrapper-classpath.propertiesPK A)Osgradle-wrapper-parameter-names.propertiesPK AAsorg/gradle/cli/PK A?<S1sorg/gradle/cli/AbstractCommandLineConverter.classPK A׃X ;Rvorg/gradle/cli/AbstractPropertiesCommandLineConverter.classPK A}yGK1{org/gradle/cli/CommandLineArgumentException.classPK Ag)|org/gradle/cli/CommandLineConverter.classPK ASf g&}org/gradle/cli/CommandLineOption.classPK A튯(?org/gradle/cli/CommandLineParser$1.classPK A$f{K ;*org/gradle/cli/CommandLineParser$AfterFirstSubCommand.classPK AD&3Έorg/gradle/cli/CommandLineParser$AfterOptions.classPK AMu <org/gradle/cli/CommandLineParser$BeforeFirstSubCommand.classPK A*ZMForg/gradle/cli/CommandLineParser$CaseInsensitiveStringComparator.classPK A|R&=Ȓorg/gradle/cli/CommandLineParser$KnownOptionParserState.classPK A$ľ<org/gradle/cli/CommandLineParser$MissingOptionArgState.classPK ATK>=org/gradle/cli/CommandLineParser$OptionAwareParserState.classPK A%̻7org/gradle/cli/CommandLineParser$OptionComparator.classPK AfC8org/gradle/cli/CommandLineParser$OptionParserState.classPK AE3org/gradle/cli/CommandLineParser$OptionString.classPK AgAqx=org/gradle/cli/CommandLineParser$OptionStringComparator.classPK A`M~U2org/gradle/cli/CommandLineParser$ParserState.classPK ApX k?Corg/gradle/cli/CommandLineParser$UnknownOptionParserState.classPK A=l)&}org/gradle/cli/CommandLineParser.classPK A>&org/gradle/cli/ParsedCommandLine.classPK AytE,org/gradle/cli/ParsedCommandLineOption.classPK A\vB| :org/gradle/cli/ProjectPropertiesCommandLineConverter.classPK A 8=|9org/gradle/cli/SystemPropertiesCommandLineConverter.classPK A?gradle-cli-classpath.propertiesPK A%gradle-cli-parameter-names.propertiesPK66Fscrcpy-1.21/gradle/wrapper/gradle-wrapper.properties000066400000000000000000000003121415124136000227010ustar00rootroot00000000000000distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists scrcpy-1.21/gradlew000077500000000000000000000132041415124136000143100ustar00rootroot00000000000000#!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # 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 # # https://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. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" scrcpy-1.21/gradlew.bat000066400000000000000000000057601415124136000150620ustar00rootroot00000000000000@rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega scrcpy-1.21/install_release.sh000077500000000000000000000012371415124136000164450ustar00rootroot00000000000000#!/usr/bin/env bash set -e BUILDDIR=build-auto PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.20/scrcpy-server-v1.20 PREBUILT_SERVER_SHA256=b20aee4951f99b060c4a44000ba94de973f9604758ef62beb253b371aad3df34 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server echo "[scrcpy] Verifying prebuilt server..." echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check echo "[scrcpy] Building client..." rm -rf "$BUILDDIR" meson "$BUILDDIR" --buildtype release --strip -Db_lto=true \ -Dprebuilt_server=scrcpy-server cd "$BUILDDIR" ninja echo "[scrcpy] Installing (sudo)..." sudo ninja install scrcpy-1.21/meson.build000066400000000000000000000005741415124136000151050ustar00rootroot00000000000000project('scrcpy', 'c', version: '1.21', meson_version: '>= 0.48', default_options: [ 'c_std=c11', 'warning_level=2', 'b_ndebug=if-release', ]) if get_option('compile_app') subdir('app') endif if get_option('compile_server') subdir('server') endif run_target('run', command: ['scripts/run-scrcpy.sh']) scrcpy-1.21/meson_options.txt000066400000000000000000000014411415124136000163720ustar00rootroot00000000000000option('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")') scrcpy-1.21/prebuilt-deps/000077500000000000000000000000001415124136000155145ustar00rootroot00000000000000scrcpy-1.21/prebuilt-deps/.gitignore000066400000000000000000000000501415124136000174770ustar00rootroot00000000000000* !/.gitignore !/Makefile !/prepare-dep scrcpy-1.21/prebuilt-deps/Makefile000066400000000000000000000032021415124136000171510ustar00rootroot00000000000000.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 scrcpy-1.21/prebuilt-deps/prepare-dep000077500000000000000000000017651415124136000176570ustar00rootroot00000000000000#!/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" scrcpy-1.21/release.mk000066400000000000000000000122621415124136000147110ustar00rootroot00000000000000# This makefile provides recipes to build a "portable" version of scrcpy for # Windows. # # Here, "portable" means that the client and server binaries are expected to be # anywhere, but in the same directory, instead of well-defined separate # locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). # # In particular, this implies to change the location from where the client push # the server to the device. .PHONY: default clean \ test \ build-server \ prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ release GRADLE ?= ./gradlew TEST_BUILD_DIR := build-test 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 RELEASE_DIR := release-$(VERSION) release: clean test build-server zip-win32 zip-win64 mkdir -p "$(RELEASE_DIR)" cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)" cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)" cd "$(RELEASE_DIR)" && \ sha256sum "scrcpy-server-$(VERSION)" \ "scrcpy-win32-$(VERSION).zip" \ "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt @echo "Release generated in $(RELEASE_DIR)/" clean: $(GRADLE) clean rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" test: [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ meson "$(TEST_BUILD_DIR)" -Db_sanitize=address ) ninja -C "$(TEST_BUILD_DIR)" $(GRADLE) -p server check build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: -$(MAKE) -C prebuilt-deps prepare-win32 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 ) 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 ) 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 data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" 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)/" 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 data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" 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)/" zip-win32: dist-win32 cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ zip -r "../$(WIN32_TARGET)" . zip-win64: dist-win64 cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ zip -r "../$(WIN64_TARGET)" . scrcpy-1.21/release.sh000077500000000000000000000000371415124136000147140ustar00rootroot00000000000000#!/bin/bash make -f release.mk scrcpy-1.21/run000077500000000000000000000010561415124136000134710ustar00rootroot00000000000000#!/usr/bin/env bash # Run scrcpy generated in the specified BUILDDIR. # # This provides the same feature as "ninja run", except that it is possible to # pass arguments to scrcpy. # # Syntax: ./run BUILDDIR if [[ $# = 0 ]] then echo "Syntax: $0 BUILDDIR " >&2 exit 1 fi BUILDDIR="$1" shift if [[ ! -d "$BUILDDIR" ]] then echo "The build dir \"$BUILDDIR\" does not exist." >&2 exit 1 fi SCRCPY_ICON_PATH="data/icon.png" \ SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ "$BUILDDIR/app/scrcpy" "$@" scrcpy-1.21/scripts/000077500000000000000000000000001415124136000144245ustar00rootroot00000000000000scrcpy-1.21/scripts/run-scrcpy.sh000077500000000000000000000001571415124136000170730ustar00rootroot00000000000000#!/usr/bin/env bash SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" scrcpy-1.21/server/000077500000000000000000000000001415124136000142435ustar00rootroot00000000000000scrcpy-1.21/server/.gitignore000066400000000000000000000001301415124136000162250ustar00rootroot00000000000000*.iml .gradle /local.properties /.idea/ .DS_Store /build /captures .externalNativeBuild scrcpy-1.21/server/build.gradle000066400000000000000000000013011415124136000165150ustar00rootroot00000000000000apply plugin: 'com.android.application' android { compileSdkVersion 31 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 31 versionCode 12100 versionName "1.21" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.13.1' } apply from: "$project.rootDir/config/android-checkstyle.gradle" scrcpy-1.21/server/build_without_gradle.sh000077500000000000000000000047451415124136000210140ustar00rootroot00000000000000#!/usr/bin/env bash # # This script generates the scrcpy binary "manually" (without gradle). # # Adapt Android platform and build tools versions (via ANDROID_PLATFORM and # ANDROID_BUILD_TOOLS environment variables). # # Then execute: # # BUILD_DIR=my_build_dir ./build_without_gradle.sh set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=1.21 PLATFORM_VERSION=31 PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0} BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" CLASSES_DIR="$BUILD_DIR/classes" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" echo "Platform: android-$PLATFORM" echo "Build-tools: $BUILD_TOOLS" echo "Build dir: $BUILD_DIR" rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy" << EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java" package com.genymobile.scrcpy; public final class BuildConfig { public static final boolean DEBUG = $SCRCPY_DEBUG; public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME"; } 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" \ android/content/IOnPrimaryClipChangedListener.aidl echo "Compiling java sources..." cd ../java javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/wrappers/*.java echo "Dexing..." cd "$CLASSES_DIR" if [[ $PLATFORM_VERSION -lt 31 ]] then # use dx "$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \ --output "$BUILD_DIR/classes.dex" \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/wrappers/*.class echo "Archiving..." cd "$BUILD_DIR" jar cvf "$SERVER_BINARY" classes.dex rm -rf classes.dex classes else # use d8 "$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \ --output "$BUILD_DIR/classes.zip" \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/wrappers/*.class cd "$BUILD_DIR" mv classes.zip "$SERVER_BINARY" rm -rf classes fi echo "Server generated in $BUILD_DIR/$SERVER_BINARY" scrcpy-1.21/server/meson.build000066400000000000000000000022061415124136000164050ustar00rootroot00000000000000# It may be useful to use a prebuilt server, so that no Android SDK is required # to build. If the 'prebuilt_server' option is set, just copy the file as is. prebuilt_server = get_option('prebuilt_server') if prebuilt_server == '' custom_target('scrcpy-server', # gradle is responsible for tracking source changes build_by_default: true, build_always_stale: true, output: 'scrcpy-server', command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')], console: true, install: true, install_dir: 'share/scrcpy') else if not prebuilt_server.startswith('/') # relative path needs some trick prebuilt_server = meson.source_root() + '/' + prebuilt_server endif custom_target('scrcpy-server-prebuilt', input: prebuilt_server, output: 'scrcpy-server', command: ['cp', '@INPUT@', '@OUTPUT@'], install: true, install_dir: 'share/scrcpy') endif scrcpy-1.21/server/proguard-rules.pro000066400000000000000000000013571415124136000177460ustar00rootroot00000000000000# Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile scrcpy-1.21/server/scripts/000077500000000000000000000000001415124136000157325ustar00rootroot00000000000000scrcpy-1.21/server/scripts/build-wrapper.sh000077500000000000000000000014571415124136000210550ustar00rootroot00000000000000#!/usr/bin/env bash # Wrapper script to invoke gradle from meson set -e # Do not execute gradle when ninja is called as root (it would download the # whole gradle world in /root/.gradle). # This is typically useful for calling "sudo ninja install" after a "ninja # install" if [[ "$EUID" == 0 ]] then echo "(not invoking gradle, since we are root)" >&2 exit 0 fi PROJECT_ROOT="$1" OUTPUT="$2" BUILDTYPE="$3" # gradlew is in the parent of the server directory GRADLE=${GRADLE:-$PROJECT_ROOT/../gradlew} if [[ "$BUILDTYPE" == debug ]] then "$GRADLE" -p "$PROJECT_ROOT" assembleDebug cp "$PROJECT_ROOT/build/outputs/apk/debug/server-debug.apk" "$OUTPUT" else "$GRADLE" -p "$PROJECT_ROOT" assembleRelease cp "$PROJECT_ROOT/build/outputs/apk/release/server-release-unsigned.apk" "$OUTPUT" fi scrcpy-1.21/server/src/000077500000000000000000000000001415124136000150325ustar00rootroot00000000000000scrcpy-1.21/server/src/main/000077500000000000000000000000001415124136000157565ustar00rootroot00000000000000scrcpy-1.21/server/src/main/AndroidManifest.xml000066400000000000000000000001671415124136000215530ustar00rootroot00000000000000 scrcpy-1.21/server/src/main/aidl/000077500000000000000000000000001415124136000166675ustar00rootroot00000000000000scrcpy-1.21/server/src/main/aidl/android/000077500000000000000000000000001415124136000203075ustar00rootroot00000000000000scrcpy-1.21/server/src/main/aidl/android/content/000077500000000000000000000000001415124136000217615ustar00rootroot00000000000000scrcpy-1.21/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl000066400000000000000000000013651415124136000304420ustar00rootroot00000000000000/** * Copyright (c) 2008, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.content; /** * {@hide} */ oneway interface IOnPrimaryClipChangedListener { void dispatchPrimaryClipChanged(); } scrcpy-1.21/server/src/main/aidl/android/view/000077500000000000000000000000001415124136000212615ustar00rootroot00000000000000scrcpy-1.21/server/src/main/aidl/android/view/IRotationWatcher.aidl000066400000000000000000000014411415124136000253420ustar00rootroot00000000000000/* //device/java/android/android/hardware/ISensorListener.aidl ** ** Copyright 2008, The Android Open Source Project ** ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. ** You may obtain a copy of the License at ** ** http://www.apache.org/licenses/LICENSE-2.0 ** ** Unless required by applicable law or agreed to in writing, software ** distributed under the License is distributed on an "AS IS" BASIS, ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ** See the License for the specific language governing permissions and ** limitations under the License. */ package android.view; /** * {@hide} */ interface IRotationWatcher { oneway void onRotationChanged(int rotation); } scrcpy-1.21/server/src/main/java/000077500000000000000000000000001415124136000166775ustar00rootroot00000000000000scrcpy-1.21/server/src/main/java/com/000077500000000000000000000000001415124136000174555ustar00rootroot00000000000000scrcpy-1.21/server/src/main/java/com/genymobile/000077500000000000000000000000001415124136000216075ustar00rootroot00000000000000scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/000077500000000000000000000000001415124136000231125ustar00rootroot00000000000000scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/CleanUp.java000066400000000000000000000147641415124136000253200ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Parcel; import android.os.Parcelable; import android.util.Base64; import java.io.File; import java.io.IOException; /** * Handle the cleanup of scrcpy, even if the main process is killed. *

* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process). */ public final class CleanUp { public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; // A simple struct to be passed from the main process to the cleanup process public static class Config implements Parcelable { public static final Creator CREATOR = new Creator() { @Override public Config createFromParcel(Parcel in) { return new Config(in); } @Override public Config[] newArray(int size) { return new Config[size]; } }; private static final int FLAG_DISABLE_SHOW_TOUCHES = 1; private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2; private static final int FLAG_POWER_OFF_SCREEN = 4; private int displayId; // Restore the value (between 0 and 7), -1 to not restore // private int restoreStayOn = -1; private boolean disableShowTouches; private boolean restoreNormalPowerMode; private boolean powerOffScreen; public Config() { // Default constructor, the fields are initialized by CleanUp.configure() } protected Config(Parcel in) { displayId = in.readInt(); restoreStayOn = in.readInt(); byte options = in.readByte(); disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0; restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0; powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(displayId); dest.writeInt(restoreStayOn); byte options = 0; if (disableShowTouches) { options |= FLAG_DISABLE_SHOW_TOUCHES; } if (restoreNormalPowerMode) { options |= FLAG_RESTORE_NORMAL_POWER_MODE; } if (powerOffScreen) { options |= FLAG_POWER_OFF_SCREEN; } dest.writeByte(options); } private boolean hasWork() { return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen; } @Override public int describeContents() { return 0; } byte[] serialize() { Parcel parcel = Parcel.obtain(); writeToParcel(parcel, 0); byte[] bytes = parcel.marshall(); parcel.recycle(); return bytes; } static Config deserialize(byte[] bytes) { Parcel parcel = Parcel.obtain(); parcel.unmarshall(bytes, 0, bytes.length); parcel.setDataPosition(0); return CREATOR.createFromParcel(parcel); } static Config fromBase64(String base64) { byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); return deserialize(bytes); } String toBase64() { byte[] bytes = serialize(); return Base64.encodeToString(bytes, Base64.NO_WRAP); } } private CleanUp() { // not instantiable } public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen) throws IOException { Config config = new Config(); config.displayId = displayId; config.disableShowTouches = disableShowTouches; config.restoreStayOn = restoreStayOn; config.restoreNormalPowerMode = restoreNormalPowerMode; config.powerOffScreen = powerOffScreen; if (config.hasWork()) { startProcess(config); } else { // There is no additional clean up to do when scrcpy dies unlinkSelf(); } } private static void startProcess(Config config) throws IOException { String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", SERVER_PATH); builder.start(); } private static void unlinkSelf() { try { new File(SERVER_PATH).delete(); } catch (Exception e) { Ln.e("Could not unlink server", e); } } public static void main(String... args) { unlinkSelf(); try { // Wait for the server to die System.in.read(); } catch (IOException e) { // Expected when the server is dead } Ln.i("Cleaning up"); Config config = Config.fromBase64(args[0]); if (config.disableShowTouches || config.restoreStayOn != -1) { ServiceManager serviceManager = new ServiceManager(); Settings settings = new Settings(serviceManager); if (config.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.restoreStayOn != -1) { Ln.i("Restoring \"stay awake\""); try { settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn)); } catch (SettingsException e) { Ln.e("Could not restore \"stay_on_while_plugged_in\"", e); } } } if (Device.isScreenOn()) { if (config.powerOffScreen) { Ln.i("Power off screen"); Device.powerOffScreen(config.displayId); } else if (config.restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); } } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/CodecOption.java000066400000000000000000000062461415124136000261730ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.util.ArrayList; import java.util.List; public class CodecOption { private String key; private Object value; public CodecOption(String key, Object value) { this.key = key; this.value = value; } public String getKey() { return key; } public Object getValue() { return value; } public static List parse(String codecOptions) { if (codecOptions.isEmpty()) { return null; } List result = new ArrayList<>(); boolean escape = false; StringBuilder buf = new StringBuilder(); for (char c : codecOptions.toCharArray()) { switch (c) { case '\\': if (escape) { buf.append('\\'); escape = false; } else { escape = true; } break; case ',': if (escape) { buf.append(','); escape = false; } else { // This comma is a separator between codec options String codecOption = buf.toString(); result.add(parseOption(codecOption)); // Clear buf buf.setLength(0); } break; default: buf.append(c); break; } } if (buf.length() > 0) { String codecOption = buf.toString(); result.add(parseOption(codecOption)); } return result; } private static CodecOption parseOption(String option) { int equalSignIndex = option.indexOf('='); if (equalSignIndex == -1) { throw new IllegalArgumentException("'=' expected"); } String keyAndType = option.substring(0, equalSignIndex); if (keyAndType.length() == 0) { throw new IllegalArgumentException("Key may not be null"); } String key; String type; int colonIndex = keyAndType.indexOf(':'); if (colonIndex != -1) { key = keyAndType.substring(0, colonIndex); type = keyAndType.substring(colonIndex + 1); } else { key = keyAndType; type = "int"; // assume int by default } Object value; String valueString = option.substring(equalSignIndex + 1); switch (type) { case "int": value = Integer.parseInt(valueString); break; case "long": value = Long.parseLong(valueString); break; case "float": value = Float.parseFloat(valueString); break; case "string": value = valueString; break; default: throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type); } return new CodecOption(key, value); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Command.java000066400000000000000000000021071415124136000253330ustar00rootroot00000000000000package 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; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java000066400000000000000000000115771415124136000267150ustar00rootroot00000000000000package com.genymobile.scrcpy; /** * Union of all supported event types, identified by their {@code type}. */ public final class ControlMessage { public static final int TYPE_INJECT_KEYCODE = 0; public static final int TYPE_INJECT_TEXT = 1; public static final int TYPE_INJECT_TOUCH_EVENT = 2; public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_EXPAND_SETTINGS_PANEL = 6; public static final int TYPE_COLLAPSE_PANELS = 7; public static final int TYPE_GET_CLIPBOARD = 8; public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_ROTATE_DEVICE = 11; 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 buttons; // MotionEvent.BUTTON_* private long pointerId; private float pressure; private Position position; private int hScroll; private int vScroll; private int copyKey; private boolean paste; private int repeat; private long sequence; private ControlMessage() { } public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_KEYCODE; msg.action = action; msg.keycode = keycode; msg.repeat = repeat; msg.metaState = metaState; return msg; } public static ControlMessage createInjectText(String text) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TEXT; msg.text = text; return msg; } public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_TOUCH_EVENT; msg.action = action; msg.pointerId = pointerId; msg.pressure = pressure; msg.position = position; msg.buttons = buttons; return msg; } public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; msg.position = position; msg.hScroll = hScroll; msg.vScroll = vScroll; return msg; } public static ControlMessage createBackOrScreenOn(int action) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_BACK_OR_SCREEN_ON; msg.action = action; return msg; } public static ControlMessage 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; } /** * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants */ public static ControlMessage createSetScreenPowerMode(int mode) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_SET_SCREEN_POWER_MODE; msg.action = mode; return msg; } public static ControlMessage createEmpty(int type) { ControlMessage msg = new ControlMessage(); msg.type = type; return msg; } public int getType() { return type; } public String getText() { return text; } public int getMetaState() { return metaState; } public int getAction() { return action; } public int getKeycode() { return keycode; } public int getButtons() { return buttons; } public long getPointerId() { return pointerId; } public float getPressure() { return pressure; } public Position getPosition() { return position; } public int getHScroll() { return hScroll; } public int getVScroll() { return vScroll; } public int getCopyKey() { return copyKey; } public boolean getPaste() { return paste; } public int getRepeat() { return repeat; } public long getSequence() { return sequence; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java000066400000000000000000000163151415124136000300330ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; 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_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 GET_CLIPBOARD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9; private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k 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]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); public ControlMessageReader() { // invariant: the buffer is always in "get" mode buffer.limit(0); } public boolean isFull() { return buffer.remaining() == rawBuffer.length; } public void readFrom(InputStream input) throws IOException { if (isFull()) { throw new IllegalStateException("Buffer full, call next() to consume"); } buffer.compact(); int head = buffer.position(); int r = input.read(rawBuffer, head, rawBuffer.length - head); if (r == -1) { throw new EOFException("Controller socket closed"); } buffer.position(head + r); buffer.flip(); } public ControlMessage next() { if (!buffer.hasRemaining()) { return null; } int savedPosition = buffer.position(); int type = buffer.get(); ControlMessage msg; switch (type) { case ControlMessage.TYPE_INJECT_KEYCODE: msg = parseInjectKeycode(); break; case ControlMessage.TYPE_INJECT_TEXT: msg = parseInjectText(); break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: msg = parseInjectTouchEvent(); break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: msg = parseInjectScrollEvent(); break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: msg = parseBackOrScreenOnEvent(); break; case ControlMessage.TYPE_GET_CLIPBOARD: msg = parseGetClipboard(); break; case ControlMessage.TYPE_SET_CLIPBOARD: msg = parseSetClipboard(); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: msg = parseSetScreenPowerMode(); break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: msg = ControlMessage.createEmpty(type); break; default: Ln.w("Unknown event type: " + type); msg = null; break; } if (msg == null) { // failure, reset savedPosition buffer.position(savedPosition); } return msg; } private ControlMessage parseInjectKeycode() { if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) { return null; } int action = 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() { if (buffer.remaining() < 4) { return null; } int len = buffer.getInt(); if (buffer.remaining() < len) { return null; } int position = buffer.position(); // Move the buffer position to consume the text buffer.position(position + len); return new String(rawBuffer, position, len, StandardCharsets.UTF_8); } private ControlMessage parseInjectText() { String text = parseString(); if (text == null) { return null; } return ControlMessage.createInjectText(text); } private ControlMessage parseInjectTouchEvent() { if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) { return null; } int action = 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); int buttons = buffer.getInt(); return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons); } private ControlMessage parseInjectScrollEvent() { if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) { return null; } Position position = readPosition(buffer); int hScroll = buffer.getInt(); int vScroll = buffer.getInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); } private ControlMessage parseBackOrScreenOnEvent() { if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { return null; } int action = toUnsigned(buffer.get()); return ControlMessage.createBackOrScreenOn(action); } private ControlMessage parseGetClipboard() { if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { return null; } int copyKey = toUnsigned(buffer.get()); return ControlMessage.createGetClipboard(copyKey); } private ControlMessage parseSetClipboard() { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { return null; } long sequence = buffer.getLong(); boolean paste = buffer.get() != 0; String text = parseString(); if (text == null) { return null; } return ControlMessage.createSetClipboard(sequence, text, paste); } private ControlMessage parseSetScreenPowerMode() { if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) { return null; } int mode = buffer.get(); 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 static int toUnsigned(short value) { return value & 0xffff; } private static int toUnsigned(byte value) { return value & 0xff; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Controller.java000066400000000000000000000305641415124136000261100ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class Controller { private static final int DEFAULT_DEVICE_ID = 0; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; private final boolean clipboardAutosync; private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private boolean keepPowerModeOff; public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) { this.device = device; this.connection = connection; this.clipboardAutosync = clipboardAutosync; initPointers(); sender = new DeviceMessageSender(connection); } private void initPointers() { for (int i = 0; i < PointersState.MAX_POINTERS; ++i) { MotionEvent.PointerProperties props = new MotionEvent.PointerProperties(); props.toolType = MotionEvent.TOOL_TYPE_FINGER; MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); coords.orientation = 0; coords.size = 0; pointerProperties[i] = props; pointerCoords[i] = coords; } } public void control() throws IOException { // on start, power on the device if (!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); } while (true) { handleEvent(); } } public DeviceMessageSender getSender() { return sender; } private void handleEvent() throws IOException { ControlMessage msg = connection.receiveControlMessage(); switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: if (device.supportsInputEvents()) { injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: if (device.supportsInputEvents()) { injectText(msg.getText()); } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: if (device.supportsInputEvents()) { injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons()); } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: if (device.supportsInputEvents()) { injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll()); } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: if (device.supportsInputEvents()) { pressBackOrTurnScreenOn(msg.getAction()); } break; case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: Device.expandNotificationPanel(); break; case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: Device.expandSettingsPanel(); break; case ControlMessage.TYPE_COLLAPSE_PANELS: Device.collapsePanels(); break; case ControlMessage.TYPE_GET_CLIPBOARD: getClipboard(msg.getCopyKey()); break; case ControlMessage.TYPE_SET_CLIPBOARD: setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: if (device.supportsInputEvents()) { int mode = msg.getAction(); boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { keepPowerModeOff = mode == Device.POWER_MODE_OFF; Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); } } break; case ControlMessage.TYPE_ROTATE_DEVICE: Device.rotateDevice(); break; default: // do nothing } } 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, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { String decomposed = KeyComposition.decompose(c); char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c}; KeyEvent[] events = charMap.getEvents(chars); if (events == null) { return false; } for (KeyEvent event : events) { if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } return true; } private int injectText(String text) { int successCount = 0; for (char c : text.toCharArray()) { if (!injectChar(c)) { Ln.w("Could not inject char u+" + String.format("%04x", (int) c)); continue; } successCount++; } return successCount; } private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); if (point == null) { Ln.w("Ignore touch event, it was generated for a different device size"); return false; } int pointerIndex = pointersState.getPointerIndex(pointerId); if (pointerIndex == -1) { Ln.w("Too many pointers for touch event"); return false; } Pointer pointer = pointersState.get(pointerIndex); pointer.setPoint(point); pointer.setPressure(pressure); pointer.setUp(action == MotionEvent.ACTION_UP); int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerCount == 1) { if (action == MotionEvent.ACTION_DOWN) { lastTouchDown = now; } } else { // secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex if (action == MotionEvent.ACTION_UP) { action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); } else if (action == MotionEvent.ACTION_DOWN) { action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT); } } // Right-click and middle-click only work if the source is a mouse boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; if (source != InputDevice.SOURCE_MOUSE) { // Buttons must not be set for touch events buttons = 0; } MotionEvent event = MotionEvent .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, int hScroll, int vScroll) { long now = SystemClock.uptimeMillis(); Point point = device.getPhysicalPoint(position); if (point == null) { // ignore event return false; } MotionEvent.PointerProperties props = pointerProperties[0]; props.id = 0; MotionEvent.PointerCoords coords = pointerCoords[0]; coords.x = point.getX(); coords.y = point.getY(); 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, 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); } }, 200, TimeUnit.MILLISECONDS); } private boolean pressBackOrTurnScreenOn(int action) { if (Device.isScreenOn()) { return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } // Screen is off // Only press POWER on ACTION_DOWN if (action != KeyEvent.ACTION_DOWN) { // do nothing, return true; } if (keepPowerModeOff) { schedulePowerModeOff(); } return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } 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) { sender.pushClipboardText(clipboardText); } } } private boolean setClipboard(String text, boolean paste, long sequence) { boolean ok = device.setClipboardText(text); if (ok) { Ln.i("Device clipboard set"); } // 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.INJECT_MODE_ASYNC); } if (sequence != ControlMessage.SEQUENCE_INVALID) { // Acknowledgement requested sender.pushAckClipboard(sequence); } return ok; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java000066400000000000000000000104671415124136000274160ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.net.LocalServerSocket; import android.net.LocalSocket; import android.net.LocalSocketAddress; import java.io.Closeable; import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; public final class DesktopConnection implements Closeable { private static final int DEVICE_NAME_FIELD_LENGTH = 64; private static final String SOCKET_NAME = "scrcpy"; private final LocalSocket videoSocket; private final FileDescriptor videoFd; private final LocalSocket controlSocket; private final InputStream controlInputStream; private final OutputStream controlOutputStream; private final ControlMessageReader reader = new ControlMessageReader(); private final DeviceMessageWriter writer = new DeviceMessageWriter(); private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException { this.videoSocket = videoSocket; this.controlSocket = controlSocket; controlInputStream = controlSocket.getInputStream(); controlOutputStream = controlSocket.getOutputStream(); videoFd = videoSocket.getFileDescriptor(); } private static LocalSocket connect(String abstractName) throws IOException { LocalSocket localSocket = new LocalSocket(); localSocket.connect(new LocalSocketAddress(abstractName)); return localSocket; } public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException { LocalSocket videoSocket; LocalSocket controlSocket; if (tunnelForward) { LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); try { videoSocket = localServerSocket.accept(); // send one byte so the client may read() to detect a connection error videoSocket.getOutputStream().write(0); try { controlSocket = localServerSocket.accept(); } catch (IOException | RuntimeException e) { videoSocket.close(); throw e; } } finally { localServerSocket.close(); } } else { videoSocket = connect(SOCKET_NAME); try { controlSocket = connect(SOCKET_NAME); } catch (IOException | RuntimeException e) { videoSocket.close(); throw e; } } DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket); Size videoSize = device.getScreenInfo().getVideoSize(); connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); return connection; } public void close() throws IOException { videoSocket.shutdownInput(); videoSocket.shutdownOutput(); videoSocket.close(); controlSocket.shutdownInput(); controlSocket.shutdownOutput(); controlSocket.close(); } private void send(String deviceName, int width, int height) throws IOException { byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4]; 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); } public FileDescriptor getVideoFd() { return videoFd; } public ControlMessage receiveControlMessage() throws IOException { ControlMessage msg = reader.next(); while (msg == null) { reader.readFrom(controlInputStream); msg = reader.next(); } return msg; } public void sendDeviceMessage(DeviceMessage msg) throws IOException { writer.writeTo(msg, controlOutputStream); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Device.java000066400000000000000000000275561415124136000251730ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; import android.content.IOnPrimaryClipChangedListener; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; import android.view.IRotationWatcher; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import java.util.concurrent.atomic.AtomicBoolean; public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; public static final int 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(); private static final Settings SETTINGS = new Settings(SERVICE_MANAGER); public interface RotationListener { void onRotationChanged(int rotation); } public interface ClipboardListener { void onClipboardTextChanged(String text); } private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); /** * Logical display identifier */ private final int displayId; /** * The surface flinger layer stack associated with this logical display */ private final int layerStack; private final boolean supportsInputEvents; public Device(Options options) { displayId = options.getDisplayId(); DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds(); throw new InvalidDisplayIdException(displayId, displayIds); } int displayInfoFlags = displayInfo.getFlags(); screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation()); layerStack = displayInfo.getLayerStack(); SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) { synchronized (Device.this) { screenInfo = screenInfo.withDeviceRotation(rotation); // notify if (rotationListener != null) { rotationListener.onRotationChanged(rotation); } } } }, displayId); if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager != null) { clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { @Override public void dispatchPrimaryClipChanged() { if (isSettingClipboard.get()) { // This is a notification for the change we are currently applying, ignore it return; } synchronized (Device.this) { if (clipboardListener != null) { String text = getClipboardText(); if (text != null) { clipboardListener.onClipboardTextChanged(text); } } } } }); } else { Ln.w("No clipboard manager, copy-paste between device and computer will not work"); } } if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } // main display or any display on Android >= Q supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } } public synchronized ScreenInfo getScreenInfo() { return screenInfo; } public int getLayerStack() { return layerStack; } public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with a lock @SuppressWarnings("checkstyle:HiddenField") ScreenInfo screenInfo = getScreenInfo(); // read with synchronization // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); int reverseVideoRotation = screenInfo.getReverseVideoRotation(); // reverse the video rotation to apply the events Position devicePosition = position.rotate(reverseVideoRotation); Size clientVideoSize = devicePosition.getScreenSize(); if (!unlockedVideoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } Rect contentRect = screenInfo.getContentRect(); Point point = devicePosition.getPoint(); int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); return new Point(convertedX, convertedY); } public static String getDeviceName() { return Build.MODEL; } public static boolean supportsInputEvents(int displayId) { return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; } public boolean supportsInputEvents() { return supportsInputEvents; } public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); } if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) { return false; } return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode); } 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, 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, injectMode); } 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, 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, int injectMode) { return pressReleaseKeycode(keyCode, displayId, injectMode); } public static boolean isScreenOn() { return SERVICE_MANAGER.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } public synchronized void setClipboardListener(ClipboardListener clipboardListener) { this.clipboardListener = clipboardListener; } public static void expandNotificationPanel() { SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); } public static void expandSettingsPanel() { SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel(); } public static void collapsePanels() { SERVICE_MANAGER.getStatusBarManager().collapsePanels(); } public static String getClipboardText() { ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager == null) { return null; } CharSequence s = clipboardManager.getText(); if (s == null) { return null; } return s.toString(); } public boolean setClipboardText(String text) { ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager(); if (clipboardManager == null) { return false; } String currentClipboard = getClipboardText(); if (currentClipboard != null && currentClipboard.equals(text)) { // The clipboard already contains the requested text. // Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause // the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this // problem, do not explicitly set the clipboard text if it already contains the expected content. return false; } isSettingClipboard.set(true); boolean ok = clipboardManager.setText(text); isSettingClipboard.set(false); return ok; } /** * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { IBinder d = SurfaceControl.getBuiltInDisplay(); if (d == null) { Ln.e("Could not get built-in display"); return false; } return SurfaceControl.setDisplayPowerMode(d, mode); } public static boolean powerOffScreen(int displayId) { if (!isScreenOn()) { return true; } 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(); boolean accelerometerRotation = !wm.isRotationFrozen(); int currentRotation = wm.getRotation(); 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); // restore auto-rotate if necessary if (accelerometerRotation) { wm.thawRotation(); } } public static Settings getSettings() { return SETTINGS; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java000066400000000000000000000017141415124136000264640ustar00rootroot00000000000000package com.genymobile.scrcpy; public final class DeviceMessage { public static final int TYPE_CLIPBOARD = 0; public static final int TYPE_ACK_CLIPBOARD = 1; public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID; private int type; private String text; private long sequence; private DeviceMessage() { } public static DeviceMessage createClipboard(String text) { DeviceMessage event = new DeviceMessage(); event.type = TYPE_CLIPBOARD; event.text = text; return event; } public static DeviceMessage createAckClipboard(long sequence) { DeviceMessage event = new DeviceMessage(); event.type = TYPE_ACK_CLIPBOARD; event.sequence = sequence; return event; } public int getType() { return type; } public String getText() { return text; } public long getSequence() { return sequence; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java000066400000000000000000000026361415124136000276310ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.io.IOException; public final class DeviceMessageSender { private final DesktopConnection connection; private String clipboardText; private long ack; public DeviceMessageSender(DesktopConnection connection) { this.connection = connection; } public synchronized void pushClipboardText(String text) { clipboardText = text; notify(); } public synchronized void pushAckClipboard(long sequence) { ack = sequence; notify(); } public void loop() throws IOException, InterruptedException { while (true) { String text; long sequence; synchronized (this) { while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) { wait(); } text = clipboardText; clipboardText = null; sequence = ack; ack = DeviceMessage.SEQUENCE_INVALID; } if (sequence != DeviceMessage.SEQUENCE_INVALID) { DeviceMessage event = DeviceMessage.createAckClipboard(sequence); connection.sendDeviceMessage(event); } if (text != null) { DeviceMessage event = DeviceMessage.createClipboard(text); connection.sendDeviceMessage(event); } } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java000066400000000000000000000026161415124136000276630ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; public class DeviceMessageWriter { private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE]; private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); public void writeTo(DeviceMessage msg, OutputStream output) throws IOException { buffer.clear(); buffer.put((byte) msg.getType()); switch (msg.getType()) { case DeviceMessage.TYPE_CLIPBOARD: String text = msg.getText(); byte[] raw = text.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH); buffer.putInt(len); buffer.put(raw, 0, len); output.write(rawBuffer, 0, buffer.position()); break; case DeviceMessage.TYPE_ACK_CLIPBOARD: buffer.putLong(msg.getSequence()); output.write(rawBuffer, 0, buffer.position()); break; default: Ln.w("Unknown device message: " + msg.getType()); break; } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java000066400000000000000000000015451415124136000262030ustar00rootroot00000000000000package com.genymobile.scrcpy; public final class DisplayInfo { private final int displayId; private final Size size; private final int rotation; private final int layerStack; private final int flags; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; } public int getDisplayId() { return displayId; } public Size getSize() { return size; } public int getRotation() { return rotation; } public int getLayerStack() { return layerStack; } public int getFlags() { return flags; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/IO.java000066400000000000000000000025571415124136000242750ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; public final class IO { private IO() { // not instantiable } public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so // count the remaining bytes manually. // See . int remaining = from.remaining(); while (remaining > 0) { try { int w = Os.write(fd, from); if (BuildConfig.DEBUG && w < 0) { // w should not be negative, since an exception is thrown on error throw new AssertionError("Os.write() returned a negative value (" + w + ")"); } remaining -= w; } catch (ErrnoException e) { if (e.errno != OsConstants.EINTR) { throw new IOException(e); } } } } public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java000066400000000000000000000010741415124136000310270ustar00rootroot00000000000000package 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; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java000066400000000000000000000011421415124136000305200ustar00rootroot00000000000000package 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; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java000066400000000000000000000133311415124136000267320ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.util.HashMap; import java.util.Map; /** * Decompose accented characters. *

* For example, {@link #decompose(char) decompose('é')} returns {@code "\u0301e"}. *

* This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])} * KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}). *

* See diacritical dead key characters. */ public final class KeyComposition { private static final String KEY_DEAD_GRAVE = "\u0300"; private static final String KEY_DEAD_ACUTE = "\u0301"; private static final String KEY_DEAD_CIRCUMFLEX = "\u0302"; private static final String KEY_DEAD_TILDE = "\u0303"; private static final String KEY_DEAD_UMLAUT = "\u0308"; private static final Map COMPOSITION_MAP = createDecompositionMap(); private KeyComposition() { // not instantiable } public static String decompose(char c) { return COMPOSITION_MAP.get(c); } private static String grave(char c) { return KEY_DEAD_GRAVE + c; } private static String acute(char c) { return KEY_DEAD_ACUTE + c; } private static String circumflex(char c) { return KEY_DEAD_CIRCUMFLEX + c; } private static String tilde(char c) { return KEY_DEAD_TILDE + c; } private static String umlaut(char c) { return KEY_DEAD_UMLAUT + c; } private static Map createDecompositionMap() { Map map = new HashMap<>(); map.put('À', grave('A')); map.put('È', grave('E')); map.put('Ì', grave('I')); map.put('Ò', grave('O')); map.put('Ù', grave('U')); map.put('à', grave('a')); map.put('è', grave('e')); map.put('ì', grave('i')); map.put('ò', grave('o')); map.put('ù', grave('u')); map.put('Ǹ', grave('N')); map.put('ǹ', grave('n')); map.put('Ẁ', grave('W')); map.put('ẁ', grave('w')); map.put('Ỳ', grave('Y')); map.put('ỳ', grave('y')); map.put('Á', acute('A')); map.put('É', acute('E')); map.put('Í', acute('I')); map.put('Ó', acute('O')); map.put('Ú', acute('U')); map.put('Ý', acute('Y')); map.put('á', acute('a')); map.put('é', acute('e')); map.put('í', acute('i')); map.put('ó', acute('o')); map.put('ú', acute('u')); map.put('ý', acute('y')); map.put('Ć', acute('C')); map.put('ć', acute('c')); map.put('Ĺ', acute('L')); map.put('ĺ', acute('l')); map.put('Ń', acute('N')); map.put('ń', acute('n')); map.put('Ŕ', acute('R')); map.put('ŕ', acute('r')); map.put('Ś', acute('S')); map.put('ś', acute('s')); map.put('Ź', acute('Z')); map.put('ź', acute('z')); map.put('Ǵ', acute('G')); map.put('ǵ', acute('g')); map.put('Ḉ', acute('Ç')); map.put('ḉ', acute('ç')); map.put('Ḱ', acute('K')); map.put('ḱ', acute('k')); map.put('Ḿ', acute('M')); map.put('ḿ', acute('m')); map.put('Ṕ', acute('P')); map.put('ṕ', acute('p')); map.put('Ẃ', acute('W')); map.put('ẃ', acute('w')); map.put('Â', circumflex('A')); map.put('Ê', circumflex('E')); map.put('Î', circumflex('I')); map.put('Ô', circumflex('O')); map.put('Û', circumflex('U')); map.put('â', circumflex('a')); map.put('ê', circumflex('e')); map.put('î', circumflex('i')); map.put('ô', circumflex('o')); map.put('û', circumflex('u')); map.put('Ĉ', circumflex('C')); map.put('ĉ', circumflex('c')); map.put('Ĝ', circumflex('G')); map.put('ĝ', circumflex('g')); map.put('Ĥ', circumflex('H')); map.put('ĥ', circumflex('h')); map.put('Ĵ', circumflex('J')); map.put('ĵ', circumflex('j')); map.put('Ŝ', circumflex('S')); map.put('ŝ', circumflex('s')); map.put('Ŵ', circumflex('W')); map.put('ŵ', circumflex('w')); map.put('Ŷ', circumflex('Y')); map.put('ŷ', circumflex('y')); map.put('Ẑ', circumflex('Z')); map.put('ẑ', circumflex('z')); map.put('Ã', tilde('A')); map.put('Ñ', tilde('N')); map.put('Õ', tilde('O')); map.put('ã', tilde('a')); map.put('ñ', tilde('n')); map.put('õ', tilde('o')); map.put('Ĩ', tilde('I')); map.put('ĩ', tilde('i')); map.put('Ũ', tilde('U')); map.put('ũ', tilde('u')); map.put('Ẽ', tilde('E')); map.put('ẽ', tilde('e')); map.put('Ỹ', tilde('Y')); map.put('ỹ', tilde('y')); map.put('Ä', umlaut('A')); map.put('Ë', umlaut('E')); map.put('Ï', umlaut('I')); map.put('Ö', umlaut('O')); map.put('Ü', umlaut('U')); map.put('ä', umlaut('a')); map.put('ë', umlaut('e')); map.put('ï', umlaut('i')); map.put('ö', umlaut('o')); map.put('ü', umlaut('u')); map.put('ÿ', umlaut('y')); map.put('Ÿ', umlaut('Y')); map.put('Ḧ', umlaut('H')); map.put('ḧ', umlaut('h')); map.put('Ẅ', umlaut('W')); map.put('ẅ', umlaut('w')); map.put('Ẍ', umlaut('X')); map.put('ẍ', umlaut('x')); map.put('ẗ', umlaut('t')); return map; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Ln.java000066400000000000000000000043061415124136000243310ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.util.Log; /** * 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). */ public final class Ln { private static final String TAG = "scrcpy"; private static final String PREFIX = "[server] "; enum Level { VERBOSE, DEBUG, INFO, WARN, ERROR } private static Level threshold = Level.INFO; private Ln() { // not instantiable } /** * Initialize the log level. *

* Must be called before starting any new thread. * * @param level the log level */ public static void initLogLevel(Level level) { threshold = level; } public static boolean isEnabled(Level level) { return level.ordinal() >= threshold.ordinal(); } public static void v(String message) { if (isEnabled(Level.VERBOSE)) { Log.v(TAG, message); System.out.println(PREFIX + "VERBOSE: " + message); } } public static void d(String message) { if (isEnabled(Level.DEBUG)) { Log.d(TAG, message); System.out.println(PREFIX + "DEBUG: " + message); } } public static void i(String message) { if (isEnabled(Level.INFO)) { Log.i(TAG, message); System.out.println(PREFIX + "INFO: " + message); } } public static void w(String message, Throwable throwable) { if (isEnabled(Level.WARN)) { Log.w(TAG, message, throwable); System.out.println(PREFIX + "WARN: " + message); if (throwable != null) { throwable.printStackTrace(); } } } 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); if (throwable != null) { throwable.printStackTrace(); } } } public static void e(String message) { e(message, null); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Options.java000066400000000000000000000066361415124136000254230ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.graphics.Rect; import java.util.List; public class Options { private Ln.Level logLevel = Ln.Level.DEBUG; private int maxSize; private int bitRate = 8000000; private int maxFps; private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean sendFrameMeta = true; // send PTS so that the client may record properly private boolean control = true; private int displayId; private boolean showTouches; private boolean stayAwake; private List codecOptions; private String encoderName; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; public Ln.Level getLogLevel() { return logLevel; } public void setLogLevel(Ln.Level logLevel) { this.logLevel = logLevel; } public int getMaxSize() { return maxSize; } public void setMaxSize(int maxSize) { this.maxSize = maxSize; } public int getBitRate() { return bitRate; } public void setBitRate(int bitRate) { this.bitRate = bitRate; } public int getMaxFps() { return maxFps; } public void setMaxFps(int maxFps) { this.maxFps = maxFps; } public int getLockVideoOrientation() { return lockVideoOrientation; } public void setLockVideoOrientation(int lockVideoOrientation) { this.lockVideoOrientation = lockVideoOrientation; } public boolean isTunnelForward() { return tunnelForward; } public void setTunnelForward(boolean tunnelForward) { this.tunnelForward = tunnelForward; } public Rect getCrop() { return crop; } public void setCrop(Rect crop) { this.crop = crop; } public boolean getSendFrameMeta() { return sendFrameMeta; } public void setSendFrameMeta(boolean sendFrameMeta) { this.sendFrameMeta = sendFrameMeta; } public boolean getControl() { return control; } public void setControl(boolean control) { this.control = control; } public int getDisplayId() { return displayId; } public void setDisplayId(int displayId) { this.displayId = displayId; } public boolean getShowTouches() { return showTouches; } public void setShowTouches(boolean showTouches) { this.showTouches = showTouches; } public boolean getStayAwake() { return stayAwake; } public void setStayAwake(boolean stayAwake) { this.stayAwake = stayAwake; } public List getCodecOptions() { return codecOptions; } public void setCodecOptions(List codecOptions) { this.codecOptions = codecOptions; } public String getEncoderName() { return encoderName; } public void setEncoderName(String encoderName) { this.encoderName = encoderName; } public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) { this.powerOffScreenOnClose = powerOffScreenOnClose; } public boolean getPowerOffScreenOnClose() { return this.powerOffScreenOnClose; } public boolean getClipboardAutosync() { return clipboardAutosync; } public void setClipboardAutosync(boolean clipboardAutosync) { this.clipboardAutosync = clipboardAutosync; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Point.java000066400000000000000000000014251415124136000250500ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.util.Objects; public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Point point = (Point) o; return x == point.x && y == point.y; } @Override public int hashCode() { return Objects.hash(x, y); } @Override public String toString() { return "Point{" + "x=" + x + ", y=" + y + '}'; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Pointer.java000066400000000000000000000020141415124136000253720ustar00rootroot00000000000000package com.genymobile.scrcpy; public class Pointer { /** * Pointer id as received from the client. */ private final long id; /** * Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}. */ private final int localId; private Point point; private float pressure; private boolean up; public Pointer(long id, int localId) { this.id = id; this.localId = localId; } public long getId() { return id; } public int getLocalId() { return localId; } public Point getPoint() { return point; } public void setPoint(Point point) { this.point = point; } public float getPressure() { return pressure; } public void setPressure(float pressure) { this.pressure = pressure; } public boolean isUp() { return up; } public void setUp(boolean up) { this.up = up; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/PointersState.java000066400000000000000000000055041415124136000265650ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.view.MotionEvent; import java.util.ArrayList; import java.util.List; public class PointersState { public static final int MAX_POINTERS = 10; private final List pointers = new ArrayList<>(); private int indexOf(long id) { for (int i = 0; i < pointers.size(); ++i) { Pointer pointer = pointers.get(i); if (pointer.getId() == id) { return i; } } return -1; } private boolean isLocalIdAvailable(int localId) { for (int i = 0; i < pointers.size(); ++i) { Pointer pointer = pointers.get(i); if (pointer.getLocalId() == localId) { return false; } } return true; } private int nextUnusedLocalId() { for (int localId = 0; localId < MAX_POINTERS; ++localId) { if (isLocalIdAvailable(localId)) { return localId; } } return -1; } public Pointer get(int index) { return pointers.get(index); } public int getPointerIndex(long id) { int index = indexOf(id); if (index != -1) { // already exists, return it return index; } if (pointers.size() >= MAX_POINTERS) { // it's full return -1; } // id 0 is reserved for mouse events int localId = nextUnusedLocalId(); if (localId == -1) { throw new AssertionError("pointers.size() < maxFingers implies that a local id is available"); } Pointer pointer = new Pointer(id, localId); pointers.add(pointer); // return the index of the pointer return pointers.size() - 1; } /** * Initialize the motion event parameters. * * @param props the pointer properties * @param coords the pointer coordinates * @return The number of items initialized (the number of pointers). */ public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) { int count = pointers.size(); for (int i = 0; i < count; ++i) { Pointer pointer = pointers.get(i); // id 0 is reserved for mouse events props[i].id = pointer.getLocalId(); Point point = pointer.getPoint(); coords[i].x = point.getX(); coords[i].y = point.getY(); coords[i].pressure = pointer.getPressure(); } cleanUp(); return count; } /** * Remove all pointers which are UP. */ private void cleanUp() { for (int i = pointers.size() - 1; i >= 0; --i) { Pointer pointer = pointers.get(i); if (pointer.isUp()) { pointers.remove(i); } } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Position.java000066400000000000000000000032521415124136000255630ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.util.Objects; public class Position { private Point point; private Size screenSize; public Position(Point point, Size screenSize) { this.point = point; this.screenSize = screenSize; } public Position(int x, int y, int screenWidth, int screenHeight) { this(new Point(x, y), new Size(screenWidth, screenHeight)); } public Point getPoint() { return point; } public Size getScreenSize() { return screenSize; } public Position rotate(int rotation) { switch (rotation) { case 1: return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate()); case 2: return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize); case 3: return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate()); default: return this; } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Position position = (Position) o; return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize); } @Override public int hashCode() { return Objects.hash(point, screenSize); } @Override public String toString() { return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}'; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java000066400000000000000000000242041415124136000264760ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; import android.os.Build; import android.os.IBinder; import android.view.Surface; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class ScreenEncoder implements Device.RotationListener { private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private String encoderName; private List codecOptions; private int bitRate; private int maxFps; private boolean sendFrameMeta; private long ptsOrigin; public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; this.encoderName = encoderName; } @Override public void onRotationChanged(int rotation) { rotationChanged.set(true); } public boolean consumeRotationChange() { return rotationChanged.getAndSet(false); } public void streamScreen(Device device, FileDescriptor fd) throws IOException { Workarounds.prepareMainLooper(); if (Build.BRAND.equalsIgnoreCase("meizu")) { // // Workarounds.fillAppInfo(); } internalStreamScreen(device, fd); } private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException { MediaFormat format = createFormat(bitRate, maxFps, codecOptions); device.setRotationListener(this); boolean alive; try { do { MediaCodec codec = createCodec(encoderName); IBinder display = createDisplay(); ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); // include the locked video orientation Rect videoRect = screenInfo.getVideoSize().toRect(); // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); int layerStack = device.getLayerStack(); setSize(format, videoRect.width(), videoRect.height()); configure(codec, format); Surface surface = codec.createInputSurface(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); try { alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); } finally { destroyDisplay(display); codec.release(); surface.release(); } } while (alive); } finally { device.setRotationListener(null); } } private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); while (!consumeRotationChange() && !eof) { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; try { if (consumeRotationChange()) { // must restart encoding with new size break; } if (outputBufferId >= 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); if (sendFrameMeta) { writeFrameMeta(fd, bufferInfo, codecBuffer.remaining()); } IO.writeFully(fd, codecBuffer); } } finally { if (outputBufferId >= 0) { codec.releaseOutputBuffer(outputBufferId, false); } } } return !eof; } private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException { headerBuffer.clear(); long pts; if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { pts = NO_PTS; // non-media data packet } else { if (ptsOrigin == 0) { ptsOrigin = bufferInfo.presentationTimeUs; } pts = bufferInfo.presentationTimeUs - ptsOrigin; } headerBuffer.putLong(pts); headerBuffer.putInt(packetSize); headerBuffer.flip(); IO.writeFully(fd, headerBuffer); } private static MediaCodecInfo[] listEncoders() { List result = new ArrayList<>(); MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS); for (MediaCodecInfo codecInfo : list.getCodecInfos()) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) { result.add(codecInfo); } } return result.toArray(new MediaCodecInfo[result.size()]); } private static MediaCodec createCodec(String encoderName) throws IOException { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { return MediaCodec.createByCodecName(encoderName); } catch (IllegalArgumentException e) { MediaCodecInfo[] encoders = listEncoders(); throw new InvalidEncoderException(encoderName, encoders); } } MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); Ln.d("Using encoder: '" + codec.getName() + "'"); return codec; } private static void setCodecOption(MediaFormat format, CodecOption codecOption) { String key = codecOption.getKey(); Object value = codecOption.getValue(); if (value instanceof Integer) { format.setInteger(key, (Integer) value); } else if (value instanceof Long) { format.setLong(key, (Long) value); } else if (value instanceof Float) { format.setFloat(key, (Float) value); } else if (value instanceof String) { format.setString(key, (String) value); } Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value); } private static MediaFormat createFormat(int bitRate, int maxFps, List codecOptions) { MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 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) { setCodecOption(format, option); } } return format; } private static IBinder createDisplay() { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" .equals(Build.VERSION.CODENAME)); return SurfaceControl.createDisplay("scrcpy", secure); } private static void configure(MediaCodec codec, MediaFormat format) { codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } private static void setSize(MediaFormat format, int width, int height) { format.setInteger(MediaFormat.KEY_WIDTH, width); format.setInteger(MediaFormat.KEY_HEIGHT, height); } private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); } } private static void destroyDisplay(IBinder display) { SurfaceControl.destroyDisplay(display); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java000066400000000000000000000136521415124136000260170ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.graphics.Rect; public final class ScreenInfo { /** * Device (physical) size, possibly cropped */ private final Rect contentRect; // device size, possibly cropped /** * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. *

* However, it does not include the locked video orientation. */ private final Size unlockedVideoSize; /** * Device rotation, related to the natural device orientation (0, 1, 2 or 3) */ private final int deviceRotation; /** * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) */ private final int lockedVideoOrientation; public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { this.contentRect = contentRect; this.unlockedVideoSize = unlockedVideoSize; this.deviceRotation = deviceRotation; this.lockedVideoOrientation = lockedVideoOrientation; } public Rect getContentRect() { return contentRect; } /** * Return the video size as if locked video orientation was not set. * * @return the unlocked video size */ public Size getUnlockedVideoSize() { return unlockedVideoSize; } /** * Return the actual video size if locked video orientation is set. * * @return the actual video size */ public Size getVideoSize() { if (getVideoRotation() % 2 == 0) { return unlockedVideoSize; } return unlockedVideoSize.rotate(); } public int getDeviceRotation() { return deviceRotation; } public ScreenInfo withDeviceRotation(int newDeviceRotation) { if (newDeviceRotation == deviceRotation) { return this; } // true if changed between portrait and landscape boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; Rect newContentRect; Size newUnlockedVideoSize; if (orientationChanged) { newContentRect = flipRect(contentRect); newUnlockedVideoSize = unlockedVideoSize.rotate(); } else { newContentRect = contentRect; newUnlockedVideoSize = unlockedVideoSize; } return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); } public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { int rotation = displayInfo.getRotation(); if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); if (crop != null) { if (rotation % 2 != 0) { // 180s preserve dimensions // the crop (provided by the user) is expressed in the natural orientation crop = flipRect(crop); } if (!contentRect.intersect(crop)) { // intersect() changes contentRect so that it is intersected with crop Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); contentRect = new Rect(); // empty } } Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); } private static String formatCrop(Rect rect) { return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } private static Size computeVideoSize(int w, int h, int maxSize) { // Compute the video size and the padding of the content inside this video. // Principle: // - scale down the great side of the screen to maxSize (if necessary); // - scale down the other side so that the aspect ratio is preserved; // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) w &= ~7; // in case it's not a multiple of 8 h &= ~7; if (maxSize > 0) { if (BuildConfig.DEBUG && maxSize % 8 != 0) { throw new AssertionError("Max size must be a multiple of 8"); } boolean portrait = h > w; int major = portrait ? h : w; int minor = portrait ? w : h; if (major > maxSize) { int minorExact = minor * maxSize / major; // +4 to round the value to the nearest multiple of 8 minor = (minorExact + 4) & ~7; major = maxSize; } w = portrait ? minor : major; h = portrait ? major : minor; } return new Size(w, h); } private static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } /** * Return the rotation to apply to the device rotation to get the requested locked video orientation * * @return the rotation offset */ public int getVideoRotation() { if (lockedVideoOrientation == -1) { // no offset return 0; } return (deviceRotation + 4 - lockedVideoOrientation) % 4; } /** * Return the rotation to apply to the requested locked video orientation to get the device rotation * * @return the (reverse) rotation offset */ public int getReverseVideoRotation() { if (lockedVideoOrientation == -1) { // no offset return 0; } return (lockedVideoOrientation + 4 - deviceRotation) % 4; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Server.java000066400000000000000000000301441415124136000252250ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.graphics.Rect; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; import java.io.IOException; import java.util.List; import java.util.Locale; public final class Server { private Server() { // not instantiable } private static void initAndCleanUp(Options options) { boolean mustDisableShowTouchesOnCleanUp = false; int restoreStayOn = -1; if (options.getShowTouches() || options.getStayAwake()) { Settings settings = Device.getSettings(); if (options.getShowTouches()) { try { String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); // If "show touches" was disabled, it must be disabled back on clean up mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue); } catch (SettingsException e) { Ln.e("Could not change \"show_touches\"", e); } } if (options.getStayAwake()) { int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; try { String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); try { restoreStayOn = Integer.parseInt(oldValue); if (restoreStayOn == stayOn) { // No need to restore restoreStayOn = -1; } } catch (NumberFormatException e) { restoreStayOn = 0; } } catch (SettingsException e) { Ln.e("Could not change \"stay_on_while_plugged_in\"", e); } } } try { CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose()); } catch (IOException e) { Ln.e("Could not configure cleanup", e); } } private static void scrcpy(Options options) throws IOException { Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); final Device device = new Device(options); List codecOptions = options.getCodecOptions(); Thread initThread = startInitThread(options); boolean tunnelForward = options.isTunnelForward(); try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName()); Thread controllerThread = null; Thread deviceMessageSenderThread = null; if (options.getControl()) { final Controller controller = new Controller(device, connection, options.getClipboardAutosync()); // asynchronous controllerThread = startController(controller); deviceMessageSenderThread = startDeviceMessageSender(controller.getSender()); device.setClipboardListener(new Device.ClipboardListener() { @Override public void onClipboardTextChanged(String text) { controller.getSender().pushClipboardText(text); } }); } try { // synchronous screenEncoder.streamScreen(device, connection.getVideoFd()); } catch (IOException e) { // this is expected on close Ln.d("Screen streaming stopped"); } finally { initThread.interrupt(); if (controllerThread != null) { controllerThread.interrupt(); } if (deviceMessageSenderThread != null) { deviceMessageSenderThread.interrupt(); } } } } private static Thread startInitThread(final Options options) { Thread thread = new Thread(new Runnable() { @Override public void run() { initAndCleanUp(options); } }); thread.start(); return thread; } private static Thread startController(final Controller controller) { Thread thread = new Thread(new Runnable() { @Override public void run() { try { controller.control(); } catch (IOException e) { // this is expected on close Ln.d("Controller stopped"); } } }); thread.start(); return thread; } private static Thread startDeviceMessageSender(final DeviceMessageSender sender) { Thread thread = new Thread(new Runnable() { @Override public void run() { try { sender.loop(); } catch (IOException | InterruptedException e) { // this is expected on close Ln.d("Device message sender stopped"); } } }); thread.start(); return thread; } private static Options createOptions(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 "log_level": Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH)); options.setLogLevel(level); break; case "max_size": int maxSize = Integer.parseInt(value) & ~7; // multiple of 8 options.setMaxSize(maxSize); break; case "bit_rate": int bitRate = Integer.parseInt(value); options.setBitRate(bitRate); break; case "max_fps": int maxFps = Integer.parseInt(value); options.setMaxFps(maxFps); break; case "lock_video_orientation": int lockVideoOrientation = Integer.parseInt(value); options.setLockVideoOrientation(lockVideoOrientation); break; case "tunnel_forward": boolean tunnelForward = Boolean.parseBoolean(value); options.setTunnelForward(tunnelForward); break; case "crop": Rect crop = parseCrop(value); options.setCrop(crop); break; case "send_frame_meta": boolean sendFrameMeta = Boolean.parseBoolean(value); options.setSendFrameMeta(sendFrameMeta); break; case "control": boolean control = Boolean.parseBoolean(value); options.setControl(control); break; case "display_id": int displayId = Integer.parseInt(value); options.setDisplayId(displayId); break; case "show_touches": boolean showTouches = Boolean.parseBoolean(value); options.setShowTouches(showTouches); break; case "stay_awake": boolean stayAwake = Boolean.parseBoolean(value); options.setStayAwake(stayAwake); break; case "codec_options": List codecOptions = CodecOption.parse(value); options.setCodecOptions(codecOptions); break; case "encoder_name": if (!value.isEmpty()) { options.setEncoderName(value); } break; case "power_off_on_close": boolean powerOffScreenOnClose = Boolean.parseBoolean(value); options.setPowerOffScreenOnClose(powerOffScreenOnClose); break; case "clipboard_autosync": boolean clipboardAutosync = Boolean.parseBoolean(value); options.setClipboardAutosync(clipboardAutosync); break; default: Ln.w("Unknown server option: " + key); break; } } return options; } private static Rect parseCrop(String crop) { if (crop.isEmpty()) { return null; } // 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 void suggestFix(Throwable e) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (e instanceof MediaCodec.CodecException) { MediaCodec.CodecException mce = (MediaCodec.CodecException) e; if (mce.getErrorCode() == 0xfffffc0e) { Ln.e("The hardware encoder is not able to encode at the given definition."); Ln.e("Try with a lower definition:"); Ln.e(" scrcpy -m 1024"); } } } if (e instanceof InvalidDisplayIdException) { InvalidDisplayIdException idie = (InvalidDisplayIdException) e; int[] displayIds = idie.getAvailableDisplayIds(); if (displayIds != null && displayIds.length > 0) { Ln.e("Try to use one of the available display ids:"); for (int id : displayIds) { Ln.e(" scrcpy --display " + id); } } } else if (e instanceof InvalidEncoderException) { InvalidEncoderException iee = (InvalidEncoderException) e; MediaCodecInfo[] encoders = iee.getAvailableEncoders(); if (encoders != null && encoders.length > 0) { Ln.e("Try to use one of the available encoders:"); for (MediaCodecInfo encoder : encoders) { Ln.e(" scrcpy --encoder '" + encoder.getName() + "'"); } } } } public static void main(String... args) throws Exception { Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { Ln.e("Exception on thread " + t, e); suggestFix(e); } }); Options options = createOptions(args); Ln.initLogLevel(options.getLogLevel()); scrcpy(options); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Settings.java000066400000000000000000000066341415124136000255660ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import java.io.IOException; public 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 final ServiceManager serviceManager; public Settings(ServiceManager serviceManager) { this.serviceManager = serviceManager; } 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 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 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 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; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/SettingsException.java000066400000000000000000000007401415124136000274350ustar00rootroot00000000000000package 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); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Size.java000066400000000000000000000020571415124136000246730ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.graphics.Rect; import java.util.Objects; public final class Size { private final int width; private final int height; public Size(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } public Size rotate() { return new Size(height, width); } public Rect toRect() { return new Rect(0, 0, width, height); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Size size = (Size) o; return width == size.width && height == size.height; } @Override public int hashCode() { return Objects.hash(width, height); } @Override public String toString() { return "Size{" + "width=" + width + ", height=" + height + '}'; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/StringUtils.java000066400000000000000000000012471415124136000262500ustar00rootroot00000000000000package com.genymobile.scrcpy; public final class StringUtils { private StringUtils() { // not instantiable } public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) { int len = utf8.length; if (len <= maxLength) { return len; } len = maxLength; // see UTF-8 encoding while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) { // the next byte is not the start of a new UTF-8 codepoint // so if we would cut there, the character would be truncated len--; } return len; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/Workarounds.java000066400000000000000000000073761415124136000263100ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.annotation.SuppressLint; import android.app.Application; import android.app.Instrumentation; import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Looper; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; public final class Workarounds { private Workarounds() { // not instantiable } @SuppressWarnings("deprecation") public static void prepareMainLooper() { // Some devices internally create a Handler when creating an input Surface, causing an exception: // "Can't create handler inside thread that has not called Looper.prepare()" // // // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' // on a null object reference" // Looper.prepareMainLooper(); } @SuppressLint("PrivateApi,DiscouragedPrivateApi") public 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(); appBindDataConstructor.setAccessible(true); Object appBindData = appBindDataConstructor.newInstance(); ApplicationInfo applicationInfo = new ApplicationInfo(); applicationInfo.packageName = "com.genymobile.scrcpy"; // appBindData.appInfo = applicationInfo; Field appInfoField = appBindDataClass.getDeclaredField("appInfo"); appInfoField.setAccessible(true); appInfoField.set(appBindData, applicationInfo); // activityThread.mBoundApplication = appBindData; Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); mBoundApplicationField.set(activityThread, appBindData); // Context ctx = activityThread.getSystemContext(); Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); Context ctx = (Context) getSystemContextMethod.invoke(activityThread); Application app = Instrumentation.newApplication(Application.class, ctx); // activityThread.mInitialApplication = app; Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); mInitialApplicationField.setAccessible(true); mInitialApplicationField.set(activityThread, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error Ln.d("Could not fill app info: " + throwable.getMessage()); } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/000077500000000000000000000000001415124136000247555ustar00rootroot00000000000000scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java000066400000000000000000000067661415124136000307260ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ActivityManager { private final IInterface manager; private Method getContentProviderExternalMethod; private boolean getContentProviderExternalMethodNewVersion = true; private Method removeContentProviderExternalMethod; public ActivityManager(IInterface manager) { this.manager = manager; } private Method getGetContentProviderExternalMethod() throws NoSuchMethodException { if (getContentProviderExternalMethod == null) { try { getContentProviderExternalMethod = manager.getClass() .getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class); } catch (NoSuchMethodException e) { // old version getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); getContentProviderExternalMethodNewVersion = false; } } return getContentProviderExternalMethod; } private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException { if (removeContentProviderExternalMethod == null) { removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class); } return removeContentProviderExternalMethod; } private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); Object[] args; if (getContentProviderExternalMethodNewVersion) { // new version args = new Object[]{name, ServiceManager.USER_ID, token, null}; } else { // old version args = new Object[]{name, ServiceManager.USER_ID, token}; } // ContentProviderHolder providerHolder = getContentProviderExternal(...); Object providerHolder = method.invoke(manager, args); if (providerHolder == null) { return null; } // IContentProvider provider = providerHolder.provider; Field providerField = providerHolder.getClass().getDeclaredField("provider"); providerField.setAccessible(true); Object provider = providerField.get(providerHolder); if (provider == null) { return null; } return new ContentProvider(this, provider, name, token); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) { Ln.e("Could not invoke method", e); return null; } } void removeContentProviderExternal(String name, IBinder token) { try { Method method = getRemoveContentProviderExternalMethod(); method.invoke(manager, name, token); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } } public ContentProvider createSettingsProvider() { return getContentProviderExternal("settings", new Binder()); } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java000066400000000000000000000116301415124136000310130ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.content.ClipData; import android.content.IOnPrimaryClipChangedListener; import android.os.Build; import android.os.IInterface; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ClipboardManager { private final IInterface manager; private Method getPrimaryClipMethod; private Method setPrimaryClipMethod; private Method addPrimaryClipChangedListener; public ClipboardManager(IInterface manager) { this.manager = manager; } private Method getGetPrimaryClipMethod() throws NoSuchMethodException { if (getPrimaryClipMethod == null) { 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); } } return getPrimaryClipMethod; } private Method getSetPrimaryClipMethod() throws NoSuchMethodException { if (setPrimaryClipMethod == null) { 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); } } return setPrimaryClipMethod; } private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); } return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); } else { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } } public CharSequence getText() { try { Method method = getGetPrimaryClipMethod(); ClipData clipData = getPrimaryClip(method, manager); if (clipData == null || clipData.getItemCount() == 0) { return null; } return clipData.getItemAt(0).getText(); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return null; } } public boolean setText(CharSequence text) { try { Method method = getSetPrimaryClipMethod(); ClipData clipData = ClipData.newPlainText(null, text); setPrimaryClip(method, manager, clipData); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); } else { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } } private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { if (addPrimaryClipChangedListener == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); } else { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); } } return addPrimaryClipChangedListener; } public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { try { Method method = getAddPrimaryClipChangedListener(); addPrimaryClipChangedListener(method, manager, listener); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java000066400000000000000000000154661415124136000307610ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.SettingsException; import android.annotation.SuppressLint; import android.os.Bundle; import android.os.IBinder; import java.io.Closeable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ContentProvider implements Closeable { public static final String TABLE_SYSTEM = "system"; public static final String TABLE_SECURE = "secure"; public static final String TABLE_GLOBAL = "global"; // See android/providerHolder/Settings.java private static final String CALL_METHOD_GET_SYSTEM = "GET_system"; private static final String CALL_METHOD_GET_SECURE = "GET_secure"; private static final String CALL_METHOD_GET_GLOBAL = "GET_global"; private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; private static final String CALL_METHOD_PUT_SECURE = "PUT_secure"; private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global"; private static final String CALL_METHOD_USER_KEY = "_user"; private static final String NAME_VALUE_TABLE_VALUE = "value"; private final ActivityManager manager; // android.content.IContentProvider private final Object provider; private final String name; private final IBinder token; private Method callMethod; private int callMethodVersion; private Object attributionSource; ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { this.manager = manager; this.provider = provider; this.name = name; this.token = token; } @SuppressLint("PrivateApi") private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { try { Class attributionSourceClass = Class.forName("android.content.AttributionSource"); callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; } catch (NoSuchMethodException | ClassNotFoundException e0) { // old versions try { callMethod = provider.getClass() .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 1; } catch (NoSuchMethodException e1) { try { callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 2; } catch (NoSuchMethodException e2) { callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class); callMethodVersion = 3; } } } } return callMethod; } @SuppressLint("PrivateApi") private Object getAttributionSource() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { if (attributionSource == null) { Class cl = Class.forName("android.content.AttributionSource$Builder"); Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID); cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME); attributionSource = cl.getDeclaredMethod("build").invoke(builder); } return attributionSource; } private Bundle call(String callMethod, String arg, Bundle extras) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { try { Method method = getCallMethod(); Object[] args; switch (callMethodVersion) { case 0: args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras}; break; case 1: args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; break; case 2: args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; break; default: args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras}; break; } return (Bundle) method.invoke(provider, args); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { Ln.e("Could not invoke method", e); throw e; } } public void close() { manager.removeContentProviderExternal(name, token); } private static String getGetMethod(String table) { switch (table) { case TABLE_SECURE: return CALL_METHOD_GET_SECURE; case TABLE_SYSTEM: return CALL_METHOD_GET_SYSTEM; case TABLE_GLOBAL: return CALL_METHOD_GET_GLOBAL; default: throw new IllegalArgumentException("Invalid table: " + table); } } private static String getPutMethod(String table) { switch (table) { case TABLE_SECURE: return CALL_METHOD_PUT_SECURE; case TABLE_SYSTEM: return CALL_METHOD_PUT_SYSTEM; case TABLE_GLOBAL: return CALL_METHOD_PUT_GLOBAL; default: throw new IllegalArgumentException("Invalid table: " + table); } } public String getValue(String table, String key) throws SettingsException { String method = getGetMethod(table); Bundle arg = new Bundle(); arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID); 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); } } 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.putString(NAME_VALUE_TABLE_VALUE, value); try { call(method, key, arg); } catch (Exception e) { throw new SettingsException(table, "put", key, value, e); } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java000066400000000000000000000027741415124136000305320ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.DisplayInfo; import com.genymobile.scrcpy.Size; import android.os.IInterface; public final class DisplayManager { private final IInterface manager; public DisplayManager(IInterface manager) { this.manager = manager; } public DisplayInfo getDisplayInfo(int displayId) { try { Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { return null; } Class cls = displayInfo.getClass(); // width and height already take the rotation into account int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo); int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo); int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); } catch (Exception e) { throw new AssertionError(e); } } public int[] getDisplayIds() { try { return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager); } catch (Exception e) { throw new AssertionError(e); } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java000066400000000000000000000040361415124136000302150ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.os.IInterface; import android.view.InputEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; 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 Method injectInputEventMethod; private static Method setDisplayIdMethod; public InputManager(IInterface manager) { this.manager = manager; } private Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } public boolean injectInputEvent(InputEvent inputEvent, int mode) { try { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } private static Method getSetDisplayIdMethod() throws NoSuchMethodException { if (setDisplayIdMethod == null) { setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class); } return setDisplayIdMethod; } public static boolean setDisplayId(InputEvent inputEvent, int displayId) { try { Method method = getSetDisplayIdMethod(); method.invoke(inputEvent, displayId); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Cannot associate a display id to the input event", e); return false; } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java000066400000000000000000000023401415124136000302060ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; 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) { 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"; isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; } public boolean isScreenOn() { try { Method method = getIsScreenOnMethod(); return (boolean) method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java000066400000000000000000000074461415124136000305260ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import android.annotation.SuppressLint; import android.os.IBinder; import android.os.IInterface; 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; public ServiceManager() { try { getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); } catch (Exception e) { throw new AssertionError(e); } } private IInterface getService(String service, String type) { try { IBinder binder = (IBinder) getServiceMethod.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); return (IInterface) asInterfaceMethod.invoke(null, binder); } catch (Exception e) { throw new AssertionError(e); } } public WindowManager getWindowManager() { if (windowManager == null) { windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); } return windowManager; } public DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); } return displayManager; } public InputManager getInputManager() { if (inputManager == null) { inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager")); } return inputManager; } public PowerManager getPowerManager() { if (powerManager == null) { powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); } return powerManager; } public StatusBarManager getStatusBarManager() { if (statusBarManager == null) { statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); } return statusBarManager; } public 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); } return clipboardManager; } public ActivityManager getActivityManager() { if (activityManager == null) { try { // On old Android versions, the ActivityManager is not exposed via AIDL, // so use ActivityManagerNative.getDefault() Class cls = Class.forName("android.app.ActivityManagerNative"); Method getDefaultMethod = cls.getDeclaredMethod("getDefault"); IInterface am = (IInterface) getDefaultMethod.invoke(null); activityManager = new ActivityManager(am); } catch (Exception e) { throw new AssertionError(e); } } return activityManager; } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java000066400000000000000000000067451415124136000310370ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.os.IInterface; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public 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) { this.manager = manager; } private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException { if (expandNotificationsPanelMethod == null) { 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; } private Method getExpandSettingsPanel() throws NoSuchMethodException { if (expandSettingsPanelMethod == null) { try { // Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/ expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class); } catch (NoSuchMethodException e) { // old version expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel"); expandSettingsPanelMethodNewVersion = false; } } return expandSettingsPanelMethod; } private Method getCollapsePanelsMethod() throws NoSuchMethodException { if (collapsePanelsMethod == null) { collapsePanelsMethod = manager.getClass().getMethod("collapsePanels"); } return collapsePanelsMethod; } public void expandNotificationsPanel() { try { Method method = getExpandNotificationsPanelMethod(); if (expandNotificationPanelMethodCustomVersion) { method.invoke(manager, 0); } else { method.invoke(manager); } } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } } public void expandSettingsPanel() { try { Method method = getExpandSettingsPanel(); if (expandSettingsPanelMethodNewVersion) { // new version method.invoke(manager, (Object) null); } else { // old version method.invoke(manager); } } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } } public void collapsePanels() { try { Method method = getCollapsePanelsMethod(); method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java000066400000000000000000000114761415124136000305620ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.annotation.SuppressLint; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.view.Surface; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi") public final class SurfaceControl { private static final Class CLASS; // see public static final int POWER_MODE_OFF = 0; public static final int POWER_MODE_NORMAL = 2; static { try { CLASS = Class.forName("android.view.SurfaceControl"); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } private static Method getBuiltInDisplayMethod; private static Method setDisplayPowerModeMethod; private SurfaceControl() { // only static methods } public static void openTransaction() { try { CLASS.getMethod("openTransaction").invoke(null); } catch (Exception e) { throw new AssertionError(e); } } public static void closeTransaction() { try { CLASS.getMethod("closeTransaction").invoke(null); } catch (Exception e) { throw new AssertionError(e); } } public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) { try { CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class) .invoke(null, displayToken, orientation, layerStackRect, displayRect); } catch (Exception e) { throw new AssertionError(e); } } public static void setDisplayLayerStack(IBinder displayToken, int layerStack) { try { CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack); } catch (Exception e) { throw new AssertionError(e); } } public static void setDisplaySurface(IBinder displayToken, Surface surface) { try { CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface); } catch (Exception e) { throw new AssertionError(e); } } 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); } } private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { if (getBuiltInDisplayMethod == null) { // the method signature has changed in Android Q // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); } } return getBuiltInDisplayMethod; } public static IBinder getBuiltInDisplay() { try { Method method = getGetBuiltInDisplayMethod(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); } // call getInternalDisplayToken() return (IBinder) method.invoke(null); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return null; } } private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException { if (setDisplayPowerModeMethod == null) { setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class); } return setDisplayPowerModeMethod; } public static boolean setDisplayPowerMode(IBinder displayToken, int mode) { try { Method method = getSetDisplayPowerModeMethod(); method.invoke(null, displayToken, mode); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } public static void destroyDisplay(IBinder displayToken) { try { CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken); } catch (Exception e) { throw new AssertionError(e); } } } scrcpy-1.21/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java000066400000000000000000000077431415124136000303750ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; import android.os.IInterface; 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 isRotationFrozenMethod; private Method thawRotationMethod; public WindowManager(IInterface manager) { this.manager = manager; } private Method getGetRotationMethod() throws NoSuchMethodException { if (getRotationMethod == null) { Class cls = manager.getClass(); try { // method changed since this commit: // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 getRotationMethod = cls.getMethod("getDefaultDisplayRotation"); } catch (NoSuchMethodException e) { // old version getRotationMethod = cls.getMethod("getRotation"); } } return getRotationMethod; } private Method getFreezeRotationMethod() throws NoSuchMethodException { if (freezeRotationMethod == null) { freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class); } return freezeRotationMethod; } private Method getIsRotationFrozenMethod() throws NoSuchMethodException { if (isRotationFrozenMethod == null) { isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen"); } return isRotationFrozenMethod; } private Method getThawRotationMethod() throws NoSuchMethodException { if (thawRotationMethod == null) { thawRotationMethod = manager.getClass().getMethod("thawRotation"); } return thawRotationMethod; } public int getRotation() { try { Method method = getGetRotationMethod(); return (int) method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return 0; } } public void freezeRotation(int rotation) { try { Method method = getFreezeRotationMethod(); method.invoke(manager, rotation); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } } public boolean isRotationFrozen() { try { Method method = getIsRotationFrozenMethod(); return (boolean) method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } public void thawRotation() { try { Method method = getThawRotationMethod(); method.invoke(manager); } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); } } public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { try { Class cls = manager.getClass(); try { // display parameter added since this commit: // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); } catch (NoSuchMethodException e) { // old version cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); } } catch (Exception e) { throw new AssertionError(e); } } } scrcpy-1.21/server/src/test/000077500000000000000000000000001415124136000160115ustar00rootroot00000000000000scrcpy-1.21/server/src/test/java/000077500000000000000000000000001415124136000167325ustar00rootroot00000000000000scrcpy-1.21/server/src/test/java/com/000077500000000000000000000000001415124136000175105ustar00rootroot00000000000000scrcpy-1.21/server/src/test/java/com/genymobile/000077500000000000000000000000001415124136000216425ustar00rootroot00000000000000scrcpy-1.21/server/src/test/java/com/genymobile/scrcpy/000077500000000000000000000000001415124136000231455ustar00rootroot00000000000000scrcpy-1.21/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java000066400000000000000000000074461415124136000272540ustar00rootroot00000000000000package com.genymobile.scrcpy; import org.junit.Assert; import org.junit.Test; import java.util.List; public class CodecOptionsTest { @Test public void testIntegerImplicit() { List codecOptions = CodecOption.parse("some_key=5"); Assert.assertEquals(1, codecOptions.size()); CodecOption option = codecOptions.get(0); Assert.assertEquals("some_key", option.getKey()); Assert.assertEquals(5, option.getValue()); } @Test public void testInteger() { List codecOptions = CodecOption.parse("some_key:int=5"); Assert.assertEquals(1, codecOptions.size()); CodecOption option = codecOptions.get(0); Assert.assertEquals("some_key", option.getKey()); Assert.assertTrue(option.getValue() instanceof Integer); Assert.assertEquals(5, option.getValue()); } @Test public void testLong() { List codecOptions = CodecOption.parse("some_key:long=5"); Assert.assertEquals(1, codecOptions.size()); CodecOption option = codecOptions.get(0); Assert.assertEquals("some_key", option.getKey()); Assert.assertTrue(option.getValue() instanceof Long); Assert.assertEquals(5L, option.getValue()); } @Test public void testFloat() { List codecOptions = CodecOption.parse("some_key:float=4.5"); Assert.assertEquals(1, codecOptions.size()); CodecOption option = codecOptions.get(0); Assert.assertEquals("some_key", option.getKey()); Assert.assertTrue(option.getValue() instanceof Float); Assert.assertEquals(4.5f, option.getValue()); } @Test public void testString() { List codecOptions = CodecOption.parse("some_key:string=some_value"); Assert.assertEquals(1, codecOptions.size()); CodecOption option = codecOptions.get(0); Assert.assertEquals("some_key", option.getKey()); Assert.assertTrue(option.getValue() instanceof String); Assert.assertEquals("some_value", option.getValue()); } @Test public void testStringEscaped() { List codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key"); Assert.assertEquals(1, codecOptions.size()); CodecOption option = codecOptions.get(0); Assert.assertEquals("some_key", option.getKey()); Assert.assertTrue(option.getValue() instanceof String); Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue()); } @Test public void testList() { List codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c"); Assert.assertEquals(5, codecOptions.size()); CodecOption option; option = codecOptions.get(0); Assert.assertEquals("a", option.getKey()); Assert.assertTrue(option.getValue() instanceof Integer); Assert.assertEquals(1, option.getValue()); option = codecOptions.get(1); Assert.assertEquals("b", option.getKey()); Assert.assertTrue(option.getValue() instanceof Integer); Assert.assertEquals(2, option.getValue()); option = codecOptions.get(2); Assert.assertEquals("c", option.getKey()); Assert.assertTrue(option.getValue() instanceof Long); Assert.assertEquals(3L, option.getValue()); option = codecOptions.get(3); Assert.assertEquals("d", option.getKey()); Assert.assertTrue(option.getValue() instanceof Float); Assert.assertEquals(4.5f, option.getValue()); option = codecOptions.get(4); Assert.assertEquals("e", option.getKey()); Assert.assertTrue(option.getValue() instanceof String); Assert.assertEquals("a,b=c", option.getValue()); } } scrcpy-1.21/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java000066400000000000000000000373231415124136000307300ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.view.KeyEvent; import android.view.MotionEvent; import org.junit.Assert; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; public class ControlMessageReaderTest { @Test public void testParseKeycodeEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); // The message type (1 byte) does not count Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @Test public void testParseTextEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = "testé".getBytes(StandardCharsets.UTF_8); dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals("testé", event.getText()); } @Test public void testParseLongTextEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TEXT); byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH]; Arrays.fill(text, (byte) 'a'); dos.writeInt(text.length); dos.write(text); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType()); Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText()); } @Test public void testParseTouchEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeLong(-42); // pointerId dos.writeInt(100); dos.writeInt(200); dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0xffff); // pressure dos.writeInt(MotionEvent.BUTTON_PRIMARY); byte[] packet = bos.toByteArray(); // The message type (1 byte) does not count Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(-42, event.getPointerId()); Assert.assertEquals(100, event.getPosition().getPoint().getX()); Assert.assertEquals(200, event.getPosition().getPoint().getY()); 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.getButtons()); } @Test public void testParseScrollEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT); dos.writeInt(260); dos.writeInt(1026); dos.writeShort(1080); dos.writeShort(1920); dos.writeInt(1); dos.writeInt(-1); byte[] packet = bos.toByteArray(); // The message type (1 byte) does not count Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType()); Assert.assertEquals(260, event.getPosition().getPoint().getX()); 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()); } @Test public void testParseBackOrScreenOnEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON); dos.writeByte(KeyEvent.ACTION_UP); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); } @Test public void testParseExpandNotificationPanelEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType()); } @Test public void testParseExpandSettingsPanelEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType()); } @Test public void testParseCollapsePanelsEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType()); } @Test public void testParseGetClipboardEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); 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(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType()); Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey()); } @Test public void testParseSetClipboardEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); 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); dos.write(text); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); 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()); } @Test public void testParseBigSetClipboardEvent() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD); byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH]; dos.writeLong(0x0807060504030201L); // sequence dos.writeByte(1); // paste Arrays.fill(rawText, (byte) 'a'); String text = new String(rawText, 0, rawText.length); dos.writeInt(rawText.length); dos.write(rawText); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType()); Assert.assertEquals(0x0807060504030201L, event.getSequence()); Assert.assertEquals(text, event.getText()); Assert.assertTrue(event.getPaste()); } @Test public void testParseSetScreenPowerMode() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); dos.writeByte(Device.POWER_MODE_NORMAL); byte[] packet = bos.toByteArray(); // The message type (1 byte) does not count Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); } @Test public void testParseRotateDevice() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); } @Test public void testMultiEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(0); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(1); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(0, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(1, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } @Test public void testPartialEvents() throws IOException { ControlMessageReader reader = new ControlMessageReader(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(KeyEvent.ACTION_UP); dos.writeInt(KeyEvent.KEYCODE_ENTER); dos.writeInt(4); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE); dos.writeByte(MotionEvent.ACTION_DOWN); byte[] packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); ControlMessage event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction()); Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode()); Assert.assertEquals(4, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); event = reader.next(); Assert.assertNull(event); // the event is not complete bos.reset(); dos.writeInt(MotionEvent.BUTTON_PRIMARY); dos.writeInt(5); // repeat dos.writeInt(KeyEvent.META_CTRL_ON); packet = bos.toByteArray(); reader.readFrom(new ByteArrayInputStream(packet)); // the event is now complete event = reader.next(); Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType()); Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction()); Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode()); Assert.assertEquals(5, event.getRepeat()); Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState()); } } scrcpy-1.21/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java000066400000000000000000000032471415124136000305570ustar00rootroot00000000000000package com.genymobile.scrcpy; import org.junit.Assert; import org.junit.Test; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; public class DeviceMessageWriterTest { @Test public void testSerializeClipboard() throws IOException { DeviceMessageWriter writer = new DeviceMessageWriter(); String text = "aéûoç"; byte[] data = text.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(DeviceMessage.TYPE_CLIPBOARD); dos.writeInt(data.length); dos.write(data); byte[] expected = bos.toByteArray(); DeviceMessage msg = DeviceMessage.createClipboard(text); bos = new ByteArrayOutputStream(); writer.writeTo(msg, bos); byte[] actual = bos.toByteArray(); 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); } } scrcpy-1.21/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java000066400000000000000000000023211415124136000271350ustar00rootroot00000000000000package com.genymobile.scrcpy; import org.junit.Assert; import org.junit.Test; import java.nio.charset.StandardCharsets; public class StringUtilsTest { @Test public void testUtf8Truncate() { String s = "aÉbÔc"; byte[] utf8 = s.getBytes(StandardCharsets.UTF_8); Assert.assertEquals(7, utf8.length); int count; count = StringUtils.getUtf8TruncationIndex(utf8, 1); Assert.assertEquals(1, count); count = StringUtils.getUtf8TruncationIndex(utf8, 2); Assert.assertEquals(1, count); // É is 2 bytes-wide count = StringUtils.getUtf8TruncationIndex(utf8, 3); Assert.assertEquals(3, count); count = StringUtils.getUtf8TruncationIndex(utf8, 4); Assert.assertEquals(4, count); count = StringUtils.getUtf8TruncationIndex(utf8, 5); Assert.assertEquals(4, count); // Ô is 2 bytes-wide count = StringUtils.getUtf8TruncationIndex(utf8, 6); Assert.assertEquals(6, count); count = StringUtils.getUtf8TruncationIndex(utf8, 7); Assert.assertEquals(7, count); count = StringUtils.getUtf8TruncationIndex(utf8, 8); Assert.assertEquals(7, count); // no more chars } } scrcpy-1.21/settings.gradle000066400000000000000000000000221415124136000157470ustar00rootroot00000000000000include ':server'