pax_global_header00006660000000000000000000000064143510402110014501gustar00rootroot0000000000000052 comment=fe21158c2023017df39ee7ecc6462579a1f3fe45 scrcpy-1.25/000077500000000000000000000000001435104021100127335ustar00rootroot00000000000000scrcpy-1.25/.github/000077500000000000000000000000001435104021100142735ustar00rootroot00000000000000scrcpy-1.25/.github/ISSUE_TEMPLATE/000077500000000000000000000000001435104021100164565ustar00rootroot00000000000000scrcpy-1.25/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000014421435104021100211510ustar00rootroot00000000000000--- 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.25/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000014051435104021100222030ustar00rootroot00000000000000--- 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.25/.gitignore000066400000000000000000000001231435104021100147170ustar00rootroot00000000000000build/ /dist/ /build-*/ /build_*/ /release-*/ .idea/ .gradle/ /x/ local.properties scrcpy-1.25/BUILD.md000066400000000000000000000163731435104021100141260ustar00rootroot00000000000000# 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 \ mingw-w64-x86_64-libusb # 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 \ mingw-w64-i686-libusb # 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 libusb # 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 setup 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.24`][direct-scrcpy-server] SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056` [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 Download the prebuilt server somewhere, and specify its path during the Meson configuration: ```bash meson setup 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 several files: - `/usr/local/bin/scrcpy` (main app) - `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device) - `/usr/local/share/man/man1/scrcpy.1` (manpage) - `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon) - `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion) - `/usr/local/share/bash-completion/completions/scrcpy` (bash completion) You can then [run](README.md#run) `scrcpy`. ### Uninstall ```bash sudo ninja -Cx uninstall # without sudo on Windows ``` scrcpy-1.25/DEVELOP.md000066400000000000000000000311361435104021100143570ustar00rootroot00000000000000# 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 setup x -Dserver_debugger=true # or, if x is already configured meson configure x -Dserver_debugger=true ``` If your device runs Android 8 or below, set the `server_debugger_method` to `old` in addition: ```bash meson setup x -Dserver_debugger=true -Dserver_debugger_method=old # or, if x is already configured meson configure x -Dserver_debugger=true -Dserver_debugger_method=old ``` Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. Redirect that port to the computer: ```bash adb forward tcp:5005 tcp:5005 ``` In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on `+`, _Remote_, and fill the form: - Host: `localhost` - Port: `5005` Then click on _Debug_. scrcpy-1.25/FAQ.md000066400000000000000000000223731435104021100136730ustar00rootroot00000000000000# Frequently Asked Questions [Read in another language](#translations) Here are the common reported problems and their status. If you encounter any error, the first step is to upgrade to the latest version. ## `adb` issues `scrcpy` execute `adb` commands to initialize the connection with the device. If `adb` fails, then scrcpy will not work. This is typically not a bug in _scrcpy_, but a problem in your environment. ### `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 not detected > ERROR: Could not find any ADB device Check that you correctly enabled [adb debugging][enable-adb]. Your device must be detected by `adb`: ``` adb devices ``` If your device is not detected, you may need some [drivers] (on Windows). There is a separate [USB driver for Google devices][google-usb-driver]. [enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling [drivers]: https://developer.android.com/studio/run/oem-usb.html [google-usb-driver]: https://developer.android.com/studio/run/win-usb ### Device unauthorized > ERROR: Device is unauthorized: > ERROR: --> (usb) 0123456789abcdef unauthorized > ERROR: A popup should open on the device to request authorization. When connecting, a popup should open on the device. You must authorize USB debugging. If it does not open, check [stackoverflow][device-unauthorized]. [device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized ### Several devices connected If several devices are connected, you will encounter this error: > ERROR: Multiple (2) ADB devices: > ERROR: --> (usb) 0123456789abcdef device Nexus_5 > ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913 > ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip) In that case, you can either provide the identifier of the device you want to mirror: ```bash scrcpy -s 0123456789abcdef ``` Or request the single USB (or TCP/IP) device: ```bash scrcpy -d # USB device scrcpy -e # TCP/IP device ``` Note that if your device is connected over TCP/IP, you might get this message: > adb: error: more than one device/emulator > ERROR: "adb reverse" returned with value 1 > 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 # in bash export ADB=/path/to/your/adb scrcpy ``` ```cmd :: in cmd set ADB=C:\path\to\your\adb.exe scrcpy ``` ```powershell # in PowerShell $env:ADB = 'C:\path\to\your\adb.exe' scrcpy ``` ### 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 This problem should be fixed in scrcpy v1.22: **update to the latest version**. On older versions, you must 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 Also, 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 to enable mipmapping: ``` scrcpy --render-driver=opengl ``` ### 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 ``` Since scrcpy v1.22, scrcpy automatically tries again with a lower definition before failing. This behavior can be disabled with `--no-downsize-on-error`. 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 Since v1.22, a "shortcut" has been added to directly open a terminal in the scrcpy directory. Double-click on `open_a_terminal_here.bat`, then type your command. For example: ``` scrcpy --record file.mkv ``` You could also open a terminal and go to the scrcpy folder manually: 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 Translations of this FAQ in other languages are available in the [wiki]. [wiki]: https://github.com/Genymobile/scrcpy/wiki Only this README file is guaranteed to be up-to-date. scrcpy-1.25/LICENSE000066400000000000000000000261731435104021100137510ustar00rootroot00000000000000 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-2022 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.25/README.md000066400000000000000000000772741435104021100142330ustar00rootroot00000000000000# scrcpy (v1.24) scrcpy _pronounced "**scr**een **c**o**py**"_ [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 Android device - **user benefits**: no account, no ads, no internet required - **freedom**: free and open source software [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 Its features include: - [recording](#recording) - mirroring with [Android device screen off](#turn-screen-off) - [copy-paste](#copy-paste) in both directions - [configurable quality](#capture-configuration) - Android device [as a webcam (V4L2)](#v4l2loopback) (Linux-only) - [physical keyboard simulation (HID)](#physical-keyboard-simulation-hid) - [physical mouse simulation (HID)](#physical-mouse-simulation-hid) - [OTG mode](#otg) - and more… ## Requirements The Android device requires at least API 21 (Android 5.0). Make sure you [enable 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 a 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 ``` On Arch Linux: ``` pacman -S 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 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 can also [build the app manually][BUILD] ([simplified process][BUILD_simple]). ### Windows For Windows, a prebuilt archive with all the dependencies (including `adb`) is available: - [`scrcpy-win64-v1.24.zip`][direct-win64] SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367` [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.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 into your computer, 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 resolution 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 so that the Android 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. The actual capture framerate may be printed to the console: ``` scrcpy --print-fps ``` It may also be enabled or disabled at any time with MOD+i. #### 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 can 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` with the device ID, check with `ls /dev/video*`) Once enabled, you can open your video stream with a v4l2-capable tool: ```bash ffplay -i /dev/videoN vlc v4l2:///dev/videoN # VLC might add some buffering delay ``` For example, you could capture the video within [OBS]. [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 and adb port, enable TCP/IP mode if necessary, then connect to the device before starting. ##### Manual Alternatively, it is possible to enable the TCP/IP connection manually using `adb`: 1. Plug the device into a USB port on your computer. 2. Connect the device to the same Wi-Fi network as your computer. 3. Get your device IP address, in Settings → About phone → Status, or by executing this command: ```bash adb shell ip route | awk '{print $9}' ``` 4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`. 5. Unplug your device. 6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP` with the device IP address you found)_. 7. Run `scrcpy` as usual. Since Android 11, a [Wireless debugging option][adb-wireless] allows to bypass having to physically connect your device directly to your computer. [adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+ If the connection randomly drops, run your `scrcpy` command to reconnect. If it says there are no devices/emulators found, try running `adb connect DEVICE_IP:5555` again, and then `scrcpy` as usual. If it still says there are none found, try running `adb disconnect`, and then run those two commands again. It may be useful to decrease the bit-rate and the resolution: ```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 can specify the _serial_: ```bash scrcpy --serial=0123456789abcdef scrcpy -s 0123456789abcdef # short version ``` The serial may also be provided via the environment variable `ANDROID_SERIAL` (also used by `adb`). 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 ``` If only one device is connected via either USB or TCP/IP, it is possible to select it automatically: ```bash # Select the only device connected via USB scrcpy -d # like adb -d scrcpy --select-usb # long version # Select the only device connected via TCP/IP scrcpy -e # like adb -e scrcpy --select-tcpip # long 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 the _adb server_ are unencrypted.** Suppose that this server is accessible at 192.168.1.2. Then, from another terminal, run `scrcpy`: ```bash # in bash export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 scrcpy --tunnel-host=192.168.1.2 ``` ```cmd :: in cmd set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037 scrcpy --tunnel-host=192.168.1.2 ``` ```powershell # in PowerShell $env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037' scrcpy --tunnel-host=192.168.1.2 ``` By default, `scrcpy` uses the local port used for `adb forward` tunnel establishment (typically `27183`, see `--port`). It is also possible to force a different tunnel port (it may be useful in more complex situations, when more redirections are involved): ``` scrcpy --tunnel-port=1234 ``` ##### SSH tunnel To communicate with a remote _adb server_ securely, it is preferable to use an SSH tunnel. First, make sure the _adb server_ is running on the remote computer: ```bash adb start-server ``` Then, establish an SSH tunnel: ```bash # local 5038 --> remote 5037 # local 27183 <-- remote 27183 ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer # keep this open ``` From another terminal, run `scrcpy`: ```bash # in bash export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` ```cmd :: in cmd set ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy ``` ```powershell # in PowerShell $env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' scrcpy ``` To avoid enabling remote port forwarding, you could force a forward connection instead (notice the `-L` instead of `-R`): ```bash # local 5038 --> remote 5037 # local 27183 --> remote 27183 ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer # keep this open ``` From another terminal, run `scrcpy`: ```bash # in bash export ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` ```cmd :: in cmd set ADB_SERVER_SOCKET=tcp:localhost:5038 scrcpy --force-adb-forward ``` ```powershell # in PowerShell $env:ADB_SERVER_SOCKET = 'tcp:localhost:5038' scrcpy --force-adb-forward ``` 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 ``` Possible values: - `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 as read-only). #### Stay awake To prevent the device from sleeping after a delay when the device is plugged in: ```bash scrcpy --stay-awake scrcpy -w ``` The initial state is restored when _scrcpy_ is closed. #### 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 ``` #### Power on on start By default, on start, the device is powered on. To prevent this behavior: ```bash scrcpy --no-power-on ``` #### 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 (by a finger on the device). #### Disable screensaver By default, _scrcpy_ does not prevent the screensaver from running 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 injects the computer clipboard text as a sequence of key events. This is useful when the component does not accept text pasting (for example in _Termux_), but it can break non-ASCII content. **WARNING:** Pasting the computer clipboard to the device (either via Ctrl+v or MOD+v) copies the content into the Android clipboard. As a consequence, any Android application could read its content. You should avoid pasting sensitive content (like passwords) that way. Some Android devices do not behave as expected when setting the device clipboard programmatically. An option `--legacy-paste` is provided to change the behavior of Ctrl+v and MOD+v so that they also inject the computer clipboard text as a sequence of key events (the same way as MOD+Shift+v). To disable automatic clipboard synchronization, use `--no-clipboard-autosync`. #### Pinch-to-zoom To simulate "pinch-to-zoom": Ctrl+_click-and-move_. More precisely, hold down Ctrl while pressing the left-click button. Until the left-click button is released, all mouse movements scale and rotate the content (if supported by the app) relative to the center of the screen. Technically, _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. Alternatively, `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 via USB. Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it is not possible to open a USB device if it is already open by another process like the _adb daemon_). 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 using 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 #### Physical mouse simulation (HID) Similarly to the physical keyboard simulation, it is possible to simulate a physical mouse. Likewise, it only works if the device is connected by USB. By default, _scrcpy_ uses Android mouse events injection with absolute coordinates. By simulating a physical mouse, a mouse pointer appears on the Android device, and relative mouse motion, clicks and scrolls are injected. To enable this mode: ```bash scrcpy --hid-mouse scrcpy -M # short version ``` You can also add `--forward-all-clicks` to [forward all mouse buttons][forward_all_clicks]. [forward_all_clicks]: #right-click-and-middle-click When this mode is enabled, the computer mouse is "captured" (the mouse pointer disappears from the computer and appears on the Android device instead). Special capture keys, either Alt or Super, toggle (disable or enable) the mouse capture. Use one of them to give the control of the mouse back to the computer. #### OTG It is possible to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled. To enable OTG mode: ```bash scrcpy --otg # Pass the serial if several USB devices are available scrcpy --otg -s 0123456789abcdef ``` It is possible to enable only HID keyboard or HID mouse: ```bash scrcpy --otg --hid-keyboard # keyboard only scrcpy --otg --hid-mouse # mouse only scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse # for convenience, enable both by default scrcpy --otg # keyboard and mouse ``` Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is connected by USB. #### Text injection preference Two kinds of [events][textevents] are generated when typing text: - _key events_, signaling that a key is pressed or released; - _text events_, signaling that a text has been entered. By default, 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._ _⁴For react-native apps in development, `MENU` triggers development menu._ _⁵Only on Android >= 7._ Shortcuts with repeated keys are executed by releasing and pressing the key a second time. For example, to execute "Expand settings panel": 1. Press and keep pressing MOD. 2. Then double-press n. 3. Finally, release MOD. All Ctrl+_key_ shortcuts are forwarded to the device, so they are handled by the active application. ## 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 the name _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]: FAQ.md ## Developers Read the [developers page]. [developers page]: DEVELOP.md ## Licence Copyright (C) 2018 Genymobile Copyright (C) 2018-2022 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/ ## Contact If you encounter a bug, please read the [FAQ] first, then open an [issue]. [issue]: https://github.com/Genymobile/scrcpy/issues For general questions or discussions, you can also use: - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) ## Translations Translations of this README in other languages are available in the [wiki]. [wiki]: https://github.com/Genymobile/scrcpy/wiki Only this README file is guaranteed to be up-to-date. scrcpy-1.25/app/000077500000000000000000000000001435104021100135135ustar00rootroot00000000000000scrcpy-1.25/app/data/000077500000000000000000000000001435104021100144245ustar00rootroot00000000000000scrcpy-1.25/app/data/bash-completion/000077500000000000000000000000001435104021100175105ustar00rootroot00000000000000scrcpy-1.25/app/data/bash-completion/scrcpy000066400000000000000000000063411435104021100207420ustar00rootroot00000000000000_scrcpy() { local cur prev words cword local opts=" --always-on-top -b --bit-rate= --codec-options= --crop= -d --select-usb --disable-screensaver --display= --display-buffer= -e --select-tcpip --encoder= --force-adb-forward --forward-all-clicks -f --fullscreen -K --hid-keyboard -h --help --legacy-paste --lock-video-orientation --lock-video-orientation= --max-fps= -M --hid-mouse -m --max-size= --no-cleanup --no-clipboard-on-error --no-downsize-on-error -n --no-control -N --no-display --no-key-repeat --no-mipmaps --no-power-on --otg -p --port= --power-off-on-close --prefer-text --print-fps --push-target= --raw-key-events -r --record= --record-format= --render-driver= --rotation= -s --serial= --shortcut-mod= -S --turn-screen-off -t --show-touches --tcpip --tcpip= --tunnel-host= --tunnel-port= --v4l2-buffer= --v4l2-sink= -V --verbosity= -v --version -w --stay-awake --window-borderless --window-title= --window-x= --window-y= --window-width= --window-height=" _init_completion -s || return case "$prev" in --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return ;; -r|--record) COMPREPLY=($(compgen -f -- "$cur")) return ;; --record-format) COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur")) return ;; --render-driver) COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) return ;; --rotation) COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur")) return ;; --shortcut-mod) # Only auto-complete a single key COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) return ;; -V|--verbosity) COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur")) return ;; -s|--serial) # Use 'adb devices' to list serial numbers COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) return ;; -b|--bitrate \ |--codec-options \ |--crop \ |--display \ |--display-buffer \ |--encoder \ |--max-fps \ |-m|--max-size \ |-p|--port \ |--push-target \ |--tunnel-host \ |--tunnel-port \ |--v4l2-buffer \ |--v4l2-sink \ |--tcpip \ |--window-*) # Option accepting an argument, but nothing to auto-complete return ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) [[ $COMPREPLY == *= ]] && compopt -o nospace } complete -F _scrcpy scrcpy scrcpy-1.25/app/data/icon.ico000066400000000000000000000212101435104021100160440ustar00rootroot00000000000000 r"PNG  IHDR\rf"9IDATxy$W̺+{{43@h4: b#Wf6`F8XvZbX6k de,#9@ `f$k4wGG^Tw'B1Wٕ|!B!B!B!B!B!B!B!B!B!B!B!B!Bi&PxgB@]72.*PL=L_}ɗ?!8@?,!/ .ٷ~TVc'=mj}0B3~I~wV}O}"IfO';4~~ l~q'V9x7 >p$GXU .,3 g{ ?) 2 : *1DA80}wϊѳop\BN~BBl|a@SPmVϒ(Ug5ͪE+)\ L.I x'$9ha1"T]G2_jc@&8kT4 M~Mi`5[}n8K B"],[`oI(U7ORuEh@.-="xU%j-Ì q+)K@7 BLd9eIP^Be9eSB#Pax!F@Qax!F@Qax!F@Qax!F@Qax!F@Qax!F@Qax!F@QaG]K_D> 9vH k CUt&sȧUowg{#w9n`|#\Fr*ow1I5.o .'r8} .3kw1-"dU^t4/7_.2y[7V07-s8BStl׶0񡫞^ ob۰Kuq7CXÿu*[:U캭E'wD붞u+-7w]t˸2D}~pi(`t_ih;$;lh߿tn a1(*88$&"|!BooG.1։XChF)=(dv\_38euƵc LZ |Sq=Y~= :BjEcw[˯$gZ iwvseJ +zoR#c̿Hd$},g"ze'qPq@1"y&WǴD `t+4oF.Tגo+u&bR̕AM&TV4Hdzcص%VP]10ʜ2 -c?i6  8~vq/`|( iT~> p 2 iCv$r;E8VP?{H巒N·KA!ןu _N_i@Qߎ\95sK|DA"_jTΡtCiPy }b:mo)גo)]10Œ>%\ w7TQ@( 3`=}ު!u-aĚÐeBA UV(U* LTɩi"۷2*MUHd *zDvًמ{זhE]kk~ hz(I!5Gr"4/QH YV@?s.H֖Qm;2pG;PecZ]5a,O',/w C[_=ze4uɈo$vM2Rf M,/Gk6 7_2c􂎱?1,:m #bb` /%o\n}s=A#DADn8t&0zrIA 73P>F)=K h9Wx& y*@nBP/?{֔EشC U~CC9xs7v",`,ο9\'RE(P1*V\ި/TD:!߽w­@϶&ʪODnoFS[tWzP 4C?59RXh aLo, @d DHL LXa\Ėز3?1oOWa}0/\,>\FI=q{܄)( Dۊm*g7T"Vk!H*i~ 14hl\>33Ƒ^TͤV_Xիlq-D">칳G@]gonGwocqZ Y^D(MXa{y4n@ X?_CP!~?>/o[ [0y1Ko=HRj<@'T1-n_oAז&\9U}s_ [oDsh@( m36k?&9CA fRd!Y@b9 hCKoL]GŪ8ʳ𸧻[QDBϡ^,-6^L_LV z|+\+zy^;l*d"FBY Bke{[13[/_ũ! {+N;2 =>³:|FmV`BK!:u }sw~ӈ4tů۰1sn\\E^S14̇FmBckb;'֑ >"KGmWr$YwNV]AHnF$zo:p7=M7T$Rzy;1- -Bf#bz>${z@_]+W\_tǯyg".8N#Rםp"2YݪUj U&1g1 2t&^9p@bտKyQ&1%ӎSq-UkkeY3(銁 (9K8LcTKT<\L-3g569ƞo\*{;YVI- oѯ&k!"#ԮIjdHU(Vx%HO*,[!k(_) $5nV0A!. S8؋0TDަAb  ejZN蘤VpGRs:1( V,@at}LrT\duLb輯 +{v+l?W^T8v݃.J͡ǀKԆ=Cp\Rd>4I2n! '9jwj ؆=;iA @z|KŪz`AQ|..oh5й[6G WSal6(7t4mqe':{;e C`ųg@n7/C{4n:,pQxd#>7/[TS.3ϭOjJH>7l3uk" A`cD{\#B`iN(Ϳ8>λ{Ծ8M \SB@[r{z7UgTthks\Rg,EB+bmoAVU hC{[K, ϳ:lݷ C&0 $1-f!*FDAyc$'`n4,3gн$yQ8L,v*\5-chbόQĒ+1B}8ي=h AY\RgY<.]ݾÃh#UOEɝı9ӖUdTݎ3s>ow:I{jSMe__i\**Ű? pO#ikieRIB;ǀM۞uL@OĖ[5JYɥr&wi͟5ԫF9gmoqU-50p H3L"`᱐҆yN5_3mOܳ+@S7rÍtOS߈M(ɍodI1Vx"ش<^\fTxam=muΞ5u{xEq!_F(Zh82|aVn7+L^ZvٿnX::X9^9m0ߕ aΗ͹8:q*ǨzbϽkʶ4C>Y7zoa(y8@8>V/.7);?2҉8Os/Bv*Kj{1HhPzTc%&_1Μж?4^= s7 8>swOoh;Ϟ-ocweكc{`G{w yC|~?/ Ǿ*hΩ⿜yiAo.F1x𷧞[6AZ:{_8P L`>/p9Cc@g/.2k5 B4`N_=Ќ6,$cy3QW#ȁ{''1UXۣ= xWƜR* X:73Ѭ{G :KS]K4#csH aҾCcoߟ{ bNѶeE<{8~x답& cppxim| ؇=`+' 0K1P>x{scΜͱ54yCD91쇞[GP  " Aōo r% ,Ceh0(HU0 cO>䊏AH9 b B<*5sYrNYY{,b8cv- |>?i [:, cBɓGخ%;IEh6{NmKjSSSS~`!FFFԔ%z  _xJQQ !W,/, :3GBP1FswNa~:dw9*dtbSFF g.֭[?ZGy_ر  RCax ;}~]k0PjHWC]\xW `e!rb߾};cE5{n^݅U8|{rspΟ$aAL Q(VR b#Gh(V4vn(}B %>,rοnw958B( -vXf(~B y-rk1Z" y tAY?cw!jpIvTt:СCbON~/e9e U5ιkW_}{.Ǐ?hw!2{0<16 @l `7 ]QУPa@9hwd2.G6=jw8v.DZc~nwF^zht!Փfߺ뮻TUub'?0 ӗ^&0ÇBUU_iDѣ6m:.fF_o|oe|/Wlٲ5](b|>?+'?].;y +a;sTWWw#cowI8J2͛o/r Gi).Ϣ([x).(Y\.rwgffV^W (YV p R bwAHUL`QePȭ8苾6qq Rk}NF:M9RC_@ nw!B!B!B!B!B!B!B!B!B!B!W;=IENDB`scrcpy-1.25/app/data/icon.png000066400000000000000000000146021435104021100160650ustar00rootroot00000000000000PNG  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.25/app/data/icon.svg000066400000000000000000000110241435104021100160730ustar00rootroot00000000000000 scrcpy-1.25/app/data/open_a_terminal_here.bat000066400000000000000000000000051435104021100212460ustar00rootroot00000000000000@cmd scrcpy-1.25/app/data/scrcpy-console.bat000066400000000000000000000001321435104021100200530ustar00rootroot00000000000000@echo off scrcpy.exe %* :: if the exit code is >= 1, then pause if errorlevel 1 pause scrcpy-1.25/app/data/scrcpy-console.desktop000066400000000000000000000007751435104021100207730ustar00rootroot00000000000000[Desktop Entry] Name=scrcpy (console) GenericName=Android Remote Control Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."' Icon=scrcpy Terminal=true Type=Application Categories=Utility;RemoteAccess; StartupNotify=false scrcpy-1.25/app/data/scrcpy-noconsole.vbs000066400000000000000000000003241435104021100204370ustar00rootroot00000000000000strCommand = "cmd /c scrcpy.exe" For Each Arg In WScript.Arguments strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """" Next CreateObject("Wscript.Shell").Run strCommand, 0, false scrcpy-1.25/app/data/scrcpy.desktop000066400000000000000000000006661435104021100173320ustar00rootroot00000000000000[Desktop Entry] Name=scrcpy GenericName=Android Remote Control Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. Exec=/bin/sh -c '"$SHELL" -i -c scrcpy' Icon=scrcpy Terminal=false Type=Application Categories=Utility;RemoteAccess; StartupNotify=false scrcpy-1.25/app/data/zsh-completion/000077500000000000000000000000001435104021100173775ustar00rootroot00000000000000scrcpy-1.25/app/data/zsh-completion/_scrcpy000066400000000000000000000106631435104021100207720ustar00rootroot00000000000000#compdef -N scrcpy -N scrcpy.exe # # name: scrcpy # auth: hltdev [hltdev8642@gmail.com] # desc: completion file for scrcpy (all OSes) # local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' {-b,--bit-rate=}'[Encode the video at the given bit-rate]' '--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' {-e,--select-tcpip}'[Use TCP/IP device]' '--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--forward-all-clicks[Forward clicks to device]' {-f,--fullscreen}'[Start in fullscreen]' {-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]' {-h,--help}'[Print the help]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)' '--max-fps=[Limit the frame rate of screen capture]' {-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' {-m,--max-size=}'[Limit both the width and height of the video to value]' '--no-cleanup[Disable device cleanup actions on exit]' '--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-power-on[Do not power on the device on start]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--power-off-on-close[Turn the device screen off when closing scrcpy]' '--prefer-text[Inject alpha characters and space as text events instead of key events]' '--print-fps[Start FPS counter, to print frame logs to the console]' '--push-target=[Set the target directory for pushing files to the device by drag and drop]' '--raw-key-events[Inject key events for all input keys, and ignore text events]' {-r,--record=}'[Record screen to file]:record file:_files' '--record-format=[Force recording format]:format:(mp4 mkv)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' {-S,--turn-screen-off}'[Turn the device screen off immediately]' {-t,--show-touches}'[Show physical touches]' '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' '--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]' '--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]' '--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]' '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-v,--version}'[Print the version of scrcpy]' {-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' '--window-borderless[Disable window decorations \(display borderless window\)]' '--window-title=[Set a custom window title]' '--window-x=[Set the initial window horizontal position]' '--window-y=[Set the initial window vertical position]' '--window-width=[Set the initial window width]' '--window-height=[Set the initial window height]' ) _arguments -s $arguments scrcpy-1.25/app/meson.build000066400000000000000000000221171435104021100156600ustar00rootroot00000000000000src = [ 'src/main.c', 'src/adb/adb.c', 'src/adb/adb_device.c', 'src/adb/adb_parser.c', 'src/adb/adb_tunnel.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', 'src/control_msg.c', 'src/controller.c', 'src/decoder.c', 'src/demuxer.c', 'src/device_msg.c', 'src/icon.c', 'src/file_pusher.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/version.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' windows = import('windows') src += [ 'src/sys/win/file.c', 'src/sys/win/process.c', windows.compile_resources('scrcpy-windows.rc'), ] conf.set('_WIN32_WINNT', '0x0600') conf.set('WINVER', '0x0600') else src += [ 'src/sys/unix/file.c', 'src/sys/unix/process.c', ] if host_machine.system() == 'darwin' conf.set('_DARWIN_C_SOURCE', true) endif endif v4l2_support = get_option('v4l2') and host_machine.system() == 'linux' if v4l2_support src += [ 'src/v4l2_sink.c' ] endif usb_support = get_option('usb') if usb_support src += [ 'src/usb/aoa_hid.c', 'src/usb/hid_keyboard.c', 'src/usb/hid_mouse.c', 'src/usb/scrcpy_otg.c', 'src/usb/screen_otg.c', 'src/usb/usb.c', ] endif cc = meson.get_compiler('c') crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows' if not crossbuild_windows # native build dependencies = [ dependency('libavformat', version: '>= 57.33'), dependency('libavcodec', version: '>= 57.37'), dependency('libavutil'), dependency('sdl2', version: '>= 2.0.5'), ] if v4l2_support dependencies += dependency('libavdevice') endif if usb_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/data/' + prebuilt_sdl2 + '/bin' sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib' sdl2_include_dir = 'prebuilt-deps/data/' + 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 = meson.get_cross_property('prebuilt_ffmpeg') ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' # ffmpeg versions are different for win32 and win64 builds ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec') ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat') ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil') ffmpeg = declare_dependency( dependencies: [ cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir), cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir), cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir), ], include_directories: include_directories(ffmpeg_include_dir) ) prebuilt_libusb = meson.get_cross_property('prebuilt_libusb') prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root') libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include' libusb = declare_dependency( dependencies: [ cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir), ], include_directories: include_directories(libusb_include_dir) ) dependencies = [ ffmpeg, sdl2, libusb, 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_USB', usb_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: []) # datadir = get_option('datadir') # by default 'share' install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) install_data('data/zsh-completion/_scrcpy', install_dir: join_paths(datadir, 'zsh/site-functions')) install_data('data/bash-completion/scrcpy', install_dir: join_paths(datadir, 'bash-completion/completions')) # Desktop entry file for application launchers if host_machine.system() == 'linux' # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) install_data('data/scrcpy.desktop', install_dir: join_paths(datadir, 'applications')) install_data('data/scrcpy-console.desktop', install_dir: join_paths(datadir, 'applications')) endif ### TESTS # 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/adb_device.c', 'src/adb/adb_parser.c', 'src/util/str.c', 'src/util/strbuf.c', ]], ['test_binary', [ 'tests/test_binary.c', ]], ['test_cbuf', [ 'tests/test_cbuf.c', ]], ['test_cli', [ 'tests/test_cli.c', 'src/cli.c', 'src/options.c', 'src/util/log.c', 'src/util/net.c', 'src/util/str.c', 'src/util/strbuf.c', 'src/util/term.c', ]], ['test_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', ]], ['test_vector', [ 'tests/test_vector.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.25/app/prebuilt-deps/000077500000000000000000000000001435104021100162725ustar00rootroot00000000000000scrcpy-1.25/app/prebuilt-deps/.gitignore000066400000000000000000000000061435104021100202560ustar00rootroot00000000000000/data scrcpy-1.25/app/prebuilt-deps/common000077500000000000000000000006361435104021100175150ustar00rootroot00000000000000PREBUILT_DATA_DIR=data 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" } scrcpy-1.25/app/prebuilt-deps/prepare-adb.sh000077500000000000000000000012331435104021100210120ustar00rootroot00000000000000#!/usr/bin/env bash set -e DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DIR" . common mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" DEP_DIR=platform-tools-33.0.3 FILENAME=platform-tools_r33.0.3-windows.zip SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2 if [[ -d "$DEP_DIR" ]] then echo "$DEP_DIR" found exit 0 fi get_file "https://dl.google.com/android/repository/$FILENAME" \ "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" ZIP_PREFIX=platform-tools unzip "../$FILENAME" \ "$ZIP_PREFIX"/AdbWinApi.dll \ "$ZIP_PREFIX"/AdbWinUsbApi.dll \ "$ZIP_PREFIX"/adb.exe mv "$ZIP_PREFIX"/* . rmdir "$ZIP_PREFIX" scrcpy-1.25/app/prebuilt-deps/prepare-ffmpeg-win32.sh000077500000000000000000000023701435104021100224730ustar00rootroot00000000000000#!/usr/bin/env bash set -e DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DIR" . common mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" DEP_DIR=ffmpeg-win32-4.3.1 FILENAME_SHARED=ffmpeg-4.3.1-win32-shared.zip SHA256SUM_SHARED=357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2 FILENAME_DEV=ffmpeg-4.3.1-win32-dev.zip SHA256SUM_DEV=230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b if [[ -d "$DEP_DIR" ]] then echo "$DEP_DIR" found exit 0 fi get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_SHARED" \ "$FILENAME_SHARED" "$SHA256SUM_SHARED" get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_DEV" \ "$FILENAME_DEV" "$SHA256SUM_DEV" mkdir "$DEP_DIR" cd "$DEP_DIR" ZIP_PREFIX_SHARED=ffmpeg-4.3.1-win32-shared unzip "../$FILENAME_SHARED" \ "$ZIP_PREFIX_SHARED"/bin/avutil-56.dll \ "$ZIP_PREFIX_SHARED"/bin/avcodec-58.dll \ "$ZIP_PREFIX_SHARED"/bin/avformat-58.dll \ "$ZIP_PREFIX_SHARED"/bin/swresample-3.dll \ "$ZIP_PREFIX_SHARED"/bin/swscale-5.dll ZIP_PREFIX_DEV=ffmpeg-4.3.1-win32-dev unzip "../$FILENAME_DEV" \ "$ZIP_PREFIX_DEV/include/*" mv "$ZIP_PREFIX_SHARED"/* . mv "$ZIP_PREFIX_DEV"/* . rmdir "$ZIP_PREFIX_SHARED" "$ZIP_PREFIX_DEV" scrcpy-1.25/app/prebuilt-deps/prepare-ffmpeg-win64.sh000077500000000000000000000015211435104021100224750ustar00rootroot00000000000000#!/usr/bin/env bash set -e DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DIR" . common mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" VERSION=5.1.2 DEP_DIR=ffmpeg-win64-$VERSION FILENAME=ffmpeg-$VERSION-full_build-shared.7z SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9 if [[ -d "$DEP_DIR" ]] then echo "$DEP_DIR" found exit 0 fi get_file "https://github.com/GyanD/codexffmpeg/releases/download/$VERSION/$FILENAME" \ "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" ZIP_PREFIX=ffmpeg-$VERSION-full_build-shared 7z x "../$FILENAME" \ "$ZIP_PREFIX"/bin/avutil-57.dll \ "$ZIP_PREFIX"/bin/avcodec-59.dll \ "$ZIP_PREFIX"/bin/avformat-59.dll \ "$ZIP_PREFIX"/bin/swresample-4.dll \ "$ZIP_PREFIX"/bin/swscale-6.dll \ "$ZIP_PREFIX"/include mv "$ZIP_PREFIX"/* . rmdir "$ZIP_PREFIX" scrcpy-1.25/app/prebuilt-deps/prepare-libusb.sh000077500000000000000000000016551435104021100215540ustar00rootroot00000000000000#!/usr/bin/env bash set -e DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DIR" . common mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" DEP_DIR=libusb-1.0.26 FILENAME=libusb-1.0.26-binaries.7z SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 if [[ -d "$DEP_DIR" ]] then echo "$DEP_DIR" found exit 0 fi get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" # include/ is the same in all folders of the archive 7z x "../$FILENAME" \ libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \ libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \ libusb-1.0.26-binaries/libusb-MinGW-x64/include/ mv libusb-1.0.26-binaries/libusb-MinGW-Win32/bin MinGW-Win32 mv libusb-1.0.26-binaries/libusb-MinGW-x64/bin MinGW-x64 mv libusb-1.0.26-binaries/libusb-MinGW-x64/include . rm -rf libusb-1.0.26-binaries scrcpy-1.25/app/prebuilt-deps/prepare-sdl.sh000077500000000000000000000015151435104021100210510ustar00rootroot00000000000000#!/usr/bin/env bash set -e DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DIR" . common mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" DEP_DIR=SDL2-2.26.1 FILENAME=SDL2-devel-2.26.1-mingw.tar.gz SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c if [[ -d "$DEP_DIR" ]] then echo "$DEP_DIR" found exit 0 fi get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" TAR_PREFIX="$DEP_DIR" # root directory inside the tar has the same name tar xf "../$FILENAME" --strip-components=1 \ "$TAR_PREFIX"/i686-w64-mingw32/bin/SDL2.dll \ "$TAR_PREFIX"/i686-w64-mingw32/include/ \ "$TAR_PREFIX"/i686-w64-mingw32/lib/ \ "$TAR_PREFIX"/x86_64-w64-mingw32/bin/SDL2.dll \ "$TAR_PREFIX"/x86_64-w64-mingw32/include/ \ "$TAR_PREFIX"/x86_64-w64-mingw32/lib/ \ scrcpy-1.25/app/scrcpy-windows.manifest000066400000000000000000000007571435104021100202470ustar00rootroot00000000000000 true PerMonitorV2 scrcpy-1.25/app/scrcpy-windows.rc000066400000000000000000000010421435104021100170310ustar00rootroot00000000000000#include 0 ICON "data/icon.ico" 1 RT_MANIFEST "scrcpy-windows.manifest" 2 VERSIONINFO BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904E4" BEGIN VALUE "FileDescription", "Display and control your Android device" VALUE "InternalName", "scrcpy" VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" VALUE "ProductVersion", "1.25" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x409, 1252 END END scrcpy-1.25/app/scrcpy.1000066400000000000000000000315401435104021100151030ustar00rootroot00000000000000.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 .B \-d, \-\-select\-usb Use USB device (if there is exactly one, like adb -d). Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). .TP .BI "\-\-disable-screensaver" Disable screensaver while scrcpy is running. .TP .BI "\-\-display " id Specify the display id to mirror. 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 .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). Also see \fB\-d\fR (\fB\-\-select\-usb\fR). .TP .BI "\-\-encoder " name Use a specific MediaCodec encoder (must be a H.264 encoder). .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. 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). Also see \fB\-\-hid\-mouse\fR. .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 \-M, \-\-hid\-mouse Simulate a physical mouse by using HID over AOAv2. In this mode, the computer mouse is captured to control the device directly (relative mouse mode). LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer. It may only work over USB. Also see \fB\-\-hid\-keyboard\fR. .TP .B \-\-no\-cleanup By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. This option disables this cleanup. .TP .B \-\-no\-clipboard\-autosync By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes. This option disables this automatic synchronization. .TP .B \-\-no\-downsize\-on\-error By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. This option disables this behavior. .TP .B \-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 .B \-\-no\-power\-on Do not power on the device on start. .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. In this mode, adb (USB debugging) is not necessary, and mirroring is disabled. LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer. If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both. It may only work over USB. See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR. .TP .BI "\-p, \-\-port " port[: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 .B "\-\-print\-fps Start FPS counter, to print framerate logs to the console. It can be started or stopped at any time with MOD+i. .TP .BI "\-\-push\-target " path Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". 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 and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. .TP .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 EXIT STATUS .B scrcpy will exit with code 0 on normal program termination. If an initial connection cannot be established, the exit code 1 will be returned. If the device disconnects while a session is active, exit code 2 will be returned. .SH SHORTCUTS In the following list, MOD is the shortcut modifier. By default, it's (left) 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 Path to adb. .TP .B ANDROID_SERIAL Device serial to use if no selector (-s, -d, -e or --tcpip=) is specified. .TP .B SCRCPY_ICON_PATH Path to the program icon. .TP .B SCRCPY_SERVER_PATH Path to the 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\-2022 .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.25/app/src/000077500000000000000000000000001435104021100143025ustar00rootroot00000000000000scrcpy-1.25/app/src/adb/000077500000000000000000000000001435104021100150305ustar00rootroot00000000000000scrcpy-1.25/app/src/adb/adb.c000066400000000000000000000522611435104021100157300ustar00rootroot00000000000000#include "adb.h" #include #include #include #include #include "adb_device.h" #include "adb_parser.h" #include "util/file.h" #include "util/log.h" #include "util/process_intr.h" #include "util/str.h" /* Convenience macro to expand: * * const char *const argv[] = * SC_ADB_COMMAND("shell", "echo", "hello"); * * to: * * const char *const argv[] = * { sc_adb_get_executable(), "shell", "echo", "hello", NULL }; */ #define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL } static const char *adb_executable; const char * sc_adb_get_executable(void) { if (!adb_executable) { adb_executable = getenv("ADB"); if (!adb_executable) adb_executable = "adb"; } return adb_executable; } // serialize argv to string "[arg1], [arg2], [arg3]" static size_t argv_to_string(const char *const *argv, char *buf, size_t bufsize) { size_t idx = 0; bool first = true; while (*argv) { const char *arg = *argv; size_t len = strlen(arg); // count space for "[], ...\0" if (idx + len + 8 >= bufsize) { // not enough space, truncate assert(idx < bufsize - 4); memcpy(&buf[idx], "...", 3); idx += 3; break; } if (first) { first = false; } else { buf[idx++] = ','; buf[idx++] = ' '; } buf[idx++] = '['; memcpy(&buf[idx], arg, len); idx += len; buf[idx++] = ']'; argv++; } assert(idx < bufsize); buf[idx] = '\0'; return idx; } static void show_adb_installation_msg() { #ifndef __WINDOWS__ static const struct { const char *binary; const char *command; } pkg_managers[] = { {"apt", "apt install adb"}, {"apt-get", "apt-get install adb"}, {"brew", "brew cask install android-platform-tools"}, {"dnf", "dnf install android-tools"}, {"emerge", "emerge dev-util/android-tools"}, {"pacman", "pacman -S android-tools"}, }; for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { if (sc_file_executable_exists(pkg_managers[i].binary)) { LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); return; } } #endif } static void show_adb_err_msg(enum sc_process_result err, const char *const argv[]) { #define MAX_COMMAND_STRING_LEN 1024 char *buf = malloc(MAX_COMMAND_STRING_LEN); if (!buf) { LOG_OOM(); LOGE("Failed to execute"); return; } switch (err) { case SC_PROCESS_ERROR_GENERIC: argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); LOGE("Failed to execute: %s", buf); break; case SC_PROCESS_ERROR_MISSING_BINARY: argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); LOGE("Command not found: %s", buf); LOGE("(make 'adb' accessible from your PATH or define its full" "path in the ADB environment variable)"); show_adb_installation_msg(); break; case SC_PROCESS_SUCCESS: // do nothing break; } free(buf); } static bool process_check_success_internal(sc_pid pid, const char *name, bool close, unsigned flags) { bool log_errors = !(flags & SC_ADB_NO_LOGERR); if (pid == SC_PROCESS_NONE) { if (log_errors) { LOGE("Could not execute \"%s\"", name); } return false; } sc_exit_code exit_code = sc_process_wait(pid, close); if (exit_code) { if (log_errors) { if (exit_code != SC_EXIT_CODE_NONE) { LOGE("\"%s\" returned with value %" SC_PRIexitcode, name, exit_code); } else { LOGE("\"%s\" exited unexpectedly", name); } } return false; } return true; } static bool process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name, unsigned flags) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted return false; } // Always pass close=false, interrupting would be racy otherwise bool ret = process_check_success_internal(pid, name, false, flags); if (intr) { sc_intr_set_process(intr, SC_PROCESS_NONE); } // Close separately sc_process_close(pid); return ret; } static sc_pid sc_adb_execute_p(const char *const argv[], unsigned flags, sc_pipe *pout) { unsigned process_flags = 0; if (flags & SC_ADB_NO_STDOUT) { process_flags |= SC_PROCESS_NO_STDOUT; } if (flags & SC_ADB_NO_STDERR) { process_flags |= SC_PROCESS_NO_STDERR; } sc_pid pid; enum sc_process_result r = sc_process_execute_p(argv, &pid, process_flags, NULL, pout, NULL); if (r != SC_PROCESS_SUCCESS) { // If the execution itself failed (not the command exit code), log the // error in all cases show_adb_err_msg(r, argv); pid = SC_PROCESS_NONE; } return pid; } sc_pid sc_adb_execute(const char *const argv[], unsigned flags) { return sc_adb_execute_p(argv, flags, NULL); } bool sc_adb_start_server(struct sc_intr *intr, unsigned flags) { const char *const argv[] = SC_ADB_COMMAND("start-server"); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb start-server", flags); } bool sc_adb_kill_server(struct sc_intr *intr, unsigned flags) { const char *const argv[] = SC_ADB_COMMAND("kill-server"); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb kill-server", flags); } bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "forward", local, remote); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb forward", flags); } bool sc_adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT sprintf(local, "tcp:%" PRIu16, local_port); assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "forward", "--remove", local); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb forward --remove", flags); } bool sc_adb_reverse(struct sc_intr *intr, const char *serial, const char *device_socket_name, uint16_t local_port, unsigned flags) { char local[4 + 5 + 1]; // tcp:PORT char remote[108 + 14 + 1]; // localabstract:NAME sprintf(local, "tcp:%" PRIu16, local_port); snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "reverse", remote, local); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb reverse", flags); } bool sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name, unsigned flags) { char remote[108 + 14 + 1]; // localabstract:NAME snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "reverse", "--remove", remote); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb reverse --remove", flags); } bool sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote, unsigned flags) { #ifdef __WINDOWS__ // Windows will parse the string, so the paths must be quoted // (see sys/win/command.c) local = sc_str_quote(local); if (!local) { return SC_PROCESS_NONE; } remote = sc_str_quote(remote); if (!remote) { free((void *) local); return SC_PROCESS_NONE; } #endif assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "push", local, remote); sc_pid pid = sc_adb_execute(argv, flags); #ifdef __WINDOWS__ free((void *) remote); free((void *) local); #endif return process_check_success_intr(intr, pid, "adb push", flags); } bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags) { #ifdef __WINDOWS__ // Windows will parse the string, so the local name must be quoted // (see sys/win/command.c) local = sc_str_quote(local); if (!local) { return SC_PROCESS_NONE; } #endif assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "install", "-r", local); sc_pid pid = sc_adb_execute(argv, flags); #ifdef __WINDOWS__ free((void *) local); #endif return process_check_success_intr(intr, pid, "adb install", flags); } bool sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags) { char port_string[5 + 1]; sprintf(port_string, "%" PRIu16, port); assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "tcpip", port_string); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb tcpip", flags); } bool sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { const char *const argv[] = SC_ADB_COMMAND("connect", ip_port); sc_pipe pout; sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb connect\""); return false; } // "adb connect" always returns successfully (with exit code 0), even in // case of failure. As a workaround, check if its output starts with // "connected". char buf[128]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb connect", flags); if (!ok) { return false; } if (r == -1) { return false; } assert((size_t) r < sizeof(buf)); buf[r] = '\0'; ok = !strncmp("connected", buf, sizeof("connected") - 1); if (!ok && !(flags & SC_ADB_NO_STDERR)) { // "adb connect" also prints errors to stdout. Since we capture it, // re-print the error to stderr. size_t len = strcspn(buf, "\r\n"); buf[len] = '\0'; fprintf(stderr, "%s\n", buf); } return ok; } bool sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) { assert(ip_port); const char *const argv[] = SC_ADB_COMMAND("disconnect", ip_port); sc_pid pid = sc_adb_execute(argv, flags); return process_check_success_intr(intr, pid, "adb disconnect", flags); } static bool sc_adb_list_devices(struct sc_intr *intr, unsigned flags, struct sc_vec_adb_devices *out_vec) { const char *const argv[] = SC_ADB_COMMAND("devices", "-l"); #define BUFSIZE 65536 char *buf = malloc(BUFSIZE); if (!buf) { LOG_OOM(); return false; } sc_pipe pout; sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb devices -l\""); free(buf); return false; } ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, BUFSIZE - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags); if (!ok) { free(buf); return false; } if (r == -1) { free(buf); return false; } assert((size_t) r < BUFSIZE); if (r == BUFSIZE - 1) { // The implementation assumes that the output of "adb devices -l" fits // in the buffer in a single pass LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " "Please report an issue."); return false; } // It is parsed as a NUL-terminated string buf[r] = '\0'; // List all devices to the output list directly ok = sc_adb_parse_devices(buf, out_vec); free(buf); return ok; } static bool sc_adb_accept_device(const struct sc_adb_device *device, const struct sc_adb_device_selector *selector) { switch (selector->type) { case SC_ADB_DEVICE_SELECT_ALL: return true; case SC_ADB_DEVICE_SELECT_SERIAL: assert(selector->serial); char *device_serial_colon = strchr(device->serial, ':'); if (device_serial_colon) { // The device serial is an IP:port... char *serial_colon = strchr(selector->serial, ':'); if (!serial_colon) { // But the requested serial has no ':', so only consider // the IP part of the device serial. This allows to use // "192.168.1.1" to match any "192.168.1.1:port". size_t serial_len = strlen(selector->serial); size_t device_ip_len = device_serial_colon - device->serial; if (serial_len != device_ip_len) { // They are not equal, they don't even have the same // length return false; } return !strncmp(selector->serial, device->serial, device_ip_len); } } return !strcmp(selector->serial, device->serial); case SC_ADB_DEVICE_SELECT_USB: return sc_adb_device_get_type(device->serial) == SC_ADB_DEVICE_TYPE_USB; case SC_ADB_DEVICE_SELECT_TCPIP: // Both emulators and TCP/IP devices are selected via -e return sc_adb_device_get_type(device->serial) != SC_ADB_DEVICE_TYPE_USB; default: assert(!"Missing SC_ADB_DEVICE_SELECT_* handling"); break; } return false; } static size_t sc_adb_devices_select(struct sc_adb_device *devices, size_t len, const struct sc_adb_device_selector *selector, size_t *idx_out) { size_t count = 0; for (size_t i = 0; i < len; ++i) { struct sc_adb_device *device = &devices[i]; device->selected = sc_adb_accept_device(device, selector); if (device->selected) { if (idx_out && !count) { *idx_out = i; } ++count; } } return count; } static void sc_adb_devices_log(enum sc_log_level level, struct sc_adb_device *devices, size_t count) { for (size_t i = 0; i < count; ++i) { struct sc_adb_device *d = &devices[i]; const char *selection = d->selected ? "-->" : " "; bool is_usb = sc_adb_device_get_type(d->serial) == SC_ADB_DEVICE_TYPE_USB; const char *type = is_usb ? " (usb)" : "(tcpip)"; LOG(level, " %s %s %-20s %16s %s", selection, type, d->serial, d->state, d->model ? d->model : ""); } } static bool sc_adb_device_check_state(struct sc_adb_device *device, struct sc_adb_device *devices, size_t count) { const char *state = device->state; if (!strcmp("device", state)) { return true; } if (!strcmp("unauthorized", state)) { LOGE("Device is unauthorized:"); sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count); LOGE("A popup should open on the device to request authorization."); LOGE("Check the FAQ: " ""); } else { LOGE("Device could not be connected (state=%s)", state); } return false; } bool sc_adb_select_device(struct sc_intr *intr, const struct sc_adb_device_selector *selector, unsigned flags, struct sc_adb_device *out_device) { struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_list_devices(intr, flags, &vec); if (!ok) { LOGE("Could not list ADB devices"); return false; } if (vec.size == 0) { LOGE("Could not find any ADB device"); return false; } size_t sel_idx; // index of the single matching device if sel_count == 1 size_t sel_count = sc_adb_devices_select(vec.data, vec.size, selector, &sel_idx); if (sel_count == 0) { // if count > 0 && sel_count == 0, then necessarily a selection is // requested assert(selector->type != SC_ADB_DEVICE_SELECT_ALL); switch (selector->type) { case SC_ADB_DEVICE_SELECT_SERIAL: assert(selector->serial); LOGE("Could not find ADB device %s:", selector->serial); break; case SC_ADB_DEVICE_SELECT_USB: LOGE("Could not find any ADB device over USB:"); break; case SC_ADB_DEVICE_SELECT_TCPIP: LOGE("Could not find any ADB device over TCP/IP:"); break; default: assert(!"Unexpected selector type"); break; } sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); sc_adb_devices_destroy(&vec); return false; } if (sel_count > 1) { switch (selector->type) { case SC_ADB_DEVICE_SELECT_ALL: LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count); break; case SC_ADB_DEVICE_SELECT_SERIAL: assert(selector->serial); LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:", sel_count, selector->serial); break; case SC_ADB_DEVICE_SELECT_USB: LOGE("Multiple (%" SC_PRIsizet ") ADB devices over USB:", sel_count); break; case SC_ADB_DEVICE_SELECT_TCPIP: LOGE("Multiple (%" SC_PRIsizet ") ADB devices over TCP/IP:", sel_count); break; default: assert(!"Unexpected selector type"); break; } sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); LOGE("Select a device via -s (--serial), -d (--select-usb) or -e " "(--select-tcpip)"); sc_adb_devices_destroy(&vec); return false; } assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 struct sc_adb_device *device = &vec.data[sel_idx]; ok = sc_adb_device_check_state(device, vec.data, vec.size); if (!ok) { sc_adb_devices_destroy(&vec); return false; } LOGD("ADB device found:"); sc_adb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); // Move devics into out_device (do not destroy device) sc_adb_device_move(out_device, device); sc_adb_devices_destroy(&vec); return true; } char * sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags) { assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "shell", "getprop", prop); sc_pipe pout; sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGE("Could not execute \"adb getprop\""); return NULL; } char buf[128]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "adb getprop", flags); if (!ok) { return NULL; } if (r == -1) { return NULL; } assert((size_t) r < sizeof(buf)); buf[r] = '\0'; size_t len = strcspn(buf, " \r\n"); buf[len] = '\0'; return strdup(buf); } char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "shell", "ip", "route"); sc_pipe pout; sc_pid pid = sc_adb_execute_p(argv, flags, &pout); if (pid == SC_PROCESS_NONE) { LOGD("Could not execute \"ip route\""); return NULL; } // "adb shell ip route" output should contain only a few lines char buf[1024]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); bool ok = process_check_success_intr(intr, pid, "ip route", flags); if (!ok) { return NULL; } if (r == -1) { return NULL; } assert((size_t) r < sizeof(buf)); if (r == sizeof(buf) - 1) { // The implementation assumes that the output of "ip route" fits in the // buffer in a single pass LOGW("Result of \"ip route\" does not fit in 1Kb. " "Please report an issue."); return NULL; } // It is parsed as a NUL-terminated string buf[r] = '\0'; return sc_adb_parse_device_ip(buf); } scrcpy-1.25/app/src/adb/adb.h000066400000000000000000000056051435104021100157350ustar00rootroot00000000000000#ifndef SC_ADB_H #define SC_ADB_H #include "common.h" #include #include #include "adb_device.h" #include "util/intr.h" #define SC_ADB_NO_STDOUT (1 << 0) #define SC_ADB_NO_STDERR (1 << 1) #define SC_ADB_NO_LOGERR (1 << 2) #define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) const char * sc_adb_get_executable(void); enum sc_adb_device_selector_type { SC_ADB_DEVICE_SELECT_ALL, SC_ADB_DEVICE_SELECT_SERIAL, SC_ADB_DEVICE_SELECT_USB, SC_ADB_DEVICE_SELECT_TCPIP, }; struct sc_adb_device_selector { enum sc_adb_device_selector_type type; const char *serial; }; sc_pid sc_adb_execute(const char *const argv[], unsigned flags); bool sc_adb_start_server(struct sc_intr *intr, unsigned flags); bool sc_adb_kill_server(struct sc_intr *intr, unsigned flags); bool sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port, const char *device_socket_name, unsigned flags); bool sc_adb_forward_remove(struct sc_intr *intr, const char *serial, uint16_t local_port, unsigned flags); bool sc_adb_reverse(struct sc_intr *intr, const char *serial, const char *device_socket_name, uint16_t local_port, unsigned flags); bool sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, const char *device_socket_name, unsigned flags); bool sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote, unsigned flags); bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags); /** * Execute `adb tcpip ` */ bool sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, unsigned flags); /** * Execute `adb connect ` * * `ip_port` may not be NULL. */ bool sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags); /** * Execute `adb disconnect []` * * If `ip_port` is NULL, execute `adb disconnect`. * Otherwise, execute `adb disconnect `. */ bool sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags); /** * Execute `adb devices` and parse the result to select a device * * Return true if a single matching device is found, and write it to out_device. */ bool sc_adb_select_device(struct sc_intr *intr, const struct sc_adb_device_selector *selector, unsigned flags, struct sc_adb_device *out_device); /** * Execute `adb getprop ` */ char * sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, unsigned flags); /** * Attempt to retrieve the device IP * * Return the IP as a string of the form "xxx.xxx.xxx.xxx", to be freed by the * caller, or NULL on error. */ char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); #endif scrcpy-1.25/app/src/adb/adb_device.c000066400000000000000000000020341435104021100172400ustar00rootroot00000000000000#include "adb_device.h" #include #include void sc_adb_device_destroy(struct sc_adb_device *device) { free(device->serial); free(device->state); free(device->model); } void sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) { *dst = *src; src->serial = NULL; src->state = NULL; src->model = NULL; } void sc_adb_devices_destroy(struct sc_vec_adb_devices *devices) { for (size_t i = 0; i < devices->size; ++i) { sc_adb_device_destroy(&devices->data[i]); } sc_vector_destroy(devices); } enum sc_adb_device_type sc_adb_device_get_type(const char *serial) { // Starts with "emulator-" if (!strncmp(serial, "emulator-", sizeof("emulator-") - 1)) { return SC_ADB_DEVICE_TYPE_EMULATOR; } // If the serial contains a ':', then it is a TCP/IP device (it is // sufficient to distinguish an ip:port from a real USB serial) if (strchr(serial, ':')) { return SC_ADB_DEVICE_TYPE_TCPIP; } return SC_ADB_DEVICE_TYPE_USB; } scrcpy-1.25/app/src/adb/adb_device.h000066400000000000000000000017451435104021100172550ustar00rootroot00000000000000#ifndef SC_ADB_DEVICE_H #define SC_ADB_DEVICE_H #include "common.h" #include #include #include "util/vector.h" struct sc_adb_device { char *serial; char *state; char *model; bool selected; }; enum sc_adb_device_type { SC_ADB_DEVICE_TYPE_USB, SC_ADB_DEVICE_TYPE_TCPIP, SC_ADB_DEVICE_TYPE_EMULATOR, }; struct sc_vec_adb_devices SC_VECTOR(struct sc_adb_device); void sc_adb_device_destroy(struct sc_adb_device *device); /** * Move src to dst * * After this call, the content of src is undefined, except that * sc_adb_device_destroy() can be called. * * This is useful to take a device from a list that will be destroyed, without * making unnecessary copies. */ void sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); void sc_adb_devices_destroy(struct sc_vec_adb_devices *devices); /** * Deduce the device type from the serial */ enum sc_adb_device_type sc_adb_device_get_type(const char *serial); #endif scrcpy-1.25/app/src/adb/adb_parser.c000066400000000000000000000136101435104021100172770ustar00rootroot00000000000000#include "adb_parser.h" #include #include #include #include "util/log.h" #include "util/str.h" bool sc_adb_parse_device(char *line, struct sc_adb_device *device) { // One device line looks like: // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " // "device:MyDevice transport_id:1" if (line[0] == '*') { // Garbage lines printed by adb daemon while starting start with a '*' return false; } if (!strncmp("adb server", line, sizeof("adb server") - 1)) { // Ignore lines starting with "adb server": // adb server version (41) doesn't match this client (39); killing... return false; } char *s = line; // cursor in the line // After the serial: // - "adb devices" writes a single '\t' // - "adb devices -l" writes multiple spaces // For flexibility, accept both. size_t serial_len = strcspn(s, " \t"); if (!serial_len) { // empty serial return false; } bool eol = s[serial_len] == '\0'; if (eol) { // serial alone is unexpected return false; } s[serial_len] = '\0'; char *serial = s; s += serial_len + 1; // After the serial, there might be several spaces s += strspn(s, " \t"); // consume all separators size_t state_len = strcspn(s, " "); if (!state_len) { // empty state return false; } eol = s[state_len] == '\0'; s[state_len] = '\0'; char *state = s; char *model = NULL; if (!eol) { s += state_len + 1; // Iterate over all properties "key:value key:value ..." for (;;) { size_t token_len = strcspn(s, " "); if (!token_len) { break; } eol = s[token_len] == '\0'; s[token_len] = '\0'; char *token = s; if (!strncmp("model:", token, sizeof("model:") - 1)) { model = &token[sizeof("model:") - 1]; // We only need the model break; } if (eol) { break; } else { s+= token_len + 1; } } } device->serial = strdup(serial); if (!device->serial) { return false; } device->state = strdup(state); if (!device->state) { free(device->serial); return false; } if (model) { device->model = strdup(model); if (!device->model) { LOG_OOM(); // model is optional, do not fail } } else { device->model = NULL; } device->selected = false; return true; } bool sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec) { #define HEADER "List of devices attached" #define HEADER_LEN (sizeof(HEADER) - 1) bool header_found = false; size_t idx_line = 0; while (str[idx_line] != '\0') { char *line = &str[idx_line]; size_t len = strcspn(line, "\n"); // The next line starts after the '\n' (replaced by `\0`) idx_line += len; if (str[idx_line] != '\0') { // The next line starts after the '\n' ++idx_line; } if (!header_found) { if (!strncmp(line, HEADER, HEADER_LEN)) { header_found = true; } // Skip everything until the header, there might be garbage lines // related to daemon starting before continue; } // The line, but without any trailing '\r' size_t line_len = sc_str_remove_trailing_cr(line, len); line[line_len] = '\0'; struct sc_adb_device device; bool ok = sc_adb_parse_device(line, &device); if (!ok) { continue; } ok = sc_vector_push(out_vec, device); if (!ok) { LOG_OOM(); LOGE("Could not push adb_device to vector"); sc_adb_device_destroy(&device); // continue anyway continue; } } assert(header_found || out_vec->size == 0); return header_found; } static char * sc_adb_parse_device_ip_from_line(char *line) { // One line from "ip route" looks like: // "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x" // Get the location of the device name (index of "wlan0" in the example) ssize_t idx_dev_name = sc_str_index_of_column(line, 2, " "); if (idx_dev_name == -1) { return NULL; } // Get the location of the ip address (column 8, but column 6 if we start // from column 2). Must be computed before truncating individual columns. ssize_t idx_ip = sc_str_index_of_column(&line[idx_dev_name], 6, " "); if (idx_ip == -1) { return NULL; } // idx_ip is searched from &line[idx_dev_name] idx_ip += idx_dev_name; char *dev_name = &line[idx_dev_name]; size_t dev_name_len = strcspn(dev_name, " \t"); dev_name[dev_name_len] = '\0'; char *ip = &line[idx_ip]; size_t ip_len = strcspn(ip, " \t"); ip[ip_len] = '\0'; // Only consider lines where the device name starts with "wlan" if (strncmp(dev_name, "wlan", sizeof("wlan") - 1)) { LOGD("Device ip lookup: ignoring %s (%s)", ip, dev_name); return NULL; } return strdup(ip); } char * sc_adb_parse_device_ip(char *str) { size_t idx_line = 0; while (str[idx_line] != '\0') { char *line = &str[idx_line]; size_t len = strcspn(line, "\n"); // The same, but without any trailing '\r' size_t line_len = sc_str_remove_trailing_cr(line, len); line[line_len] = '\0'; char *ip = sc_adb_parse_device_ip_from_line(line); if (ip) { // Found return ip; } idx_line += len; if (str[idx_line] != '\0') { // The next line starts after the '\n' ++idx_line; } } return NULL; } scrcpy-1.25/app/src/adb/adb_parser.h000066400000000000000000000011731435104021100173050ustar00rootroot00000000000000#ifndef SC_ADB_PARSER_H #define SC_ADB_PARSER_H #include "common.h" #include #include "adb_device.h" /** * Parse the available devices from the output of `adb devices` * * The parameter must be a NUL-terminated string. * * Warning: this function modifies the buffer for optimization purposes. */ bool sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec); /** * Parse the ip from the output of `adb shell ip route` * * The parameter must be a NUL-terminated string. * * Warning: this function modifies the buffer for optimization purposes. */ char * sc_adb_parse_device_ip(char *str); #endif scrcpy-1.25/app/src/adb/adb_tunnel.c000066400000000000000000000125571435104021100173210ustar00rootroot00000000000000#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 (!sc_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 (!sc_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 (sc_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 = sc_adb_forward_remove(intr, serial, tunnel->local_port, SC_ADB_NO_STDOUT); } else { ret = sc_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.25/app/src/adb/adb_tunnel.h000066400000000000000000000021351435104021100173150ustar00rootroot00000000000000#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.25/app/src/android/000077500000000000000000000000001435104021100157225ustar00rootroot00000000000000scrcpy-1.25/app/src/android/input.h000066400000000000000000000765711435104021100172520ustar00rootroot00000000000000// 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.25/app/src/android/keycodes.h000066400000000000000000000672601435104021100177140ustar00rootroot00000000000000// 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.25/app/src/cli.c000066400000000000000000001541431435104021100152250ustar00rootroot00000000000000#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 #define OPT_NO_DOWNSIZE_ON_ERROR 1035 #define OPT_OTG 1036 #define OPT_NO_CLEANUP 1037 #define OPT_PRINT_FPS 1038 #define OPT_NO_POWER_ON 1039 struct sc_option { char shortopt; int longopt_id; // either shortopt or longopt_id is non-zero const char *longopt; // no argument: argdesc == NULL && !optional_arg // optional argument: argdesc != NULL && optional_arg // required argument: argdesc != NULL && !optional_arg const char *argdesc; bool optional_arg; const char *text; // if NULL, the option does not appear in the help }; #define MAX_EQUIVALENT_SHORTCUTS 3 struct sc_shortcut { const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1]; const char *text; }; struct sc_envvar { const char *name; const char *text; }; struct sc_exit_status { unsigned value; const char *text; }; struct sc_getopt_adapter { char *optstring; struct option *longopts; }; static const struct sc_option options[] = { { .longopt_id = OPT_ALWAYS_ON_TOP, .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, { .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 computed on the cropped size.", }, { .shortopt = 'd', .longopt = "select-usb", .text = "Use USB device (if there is exactly one, like adb -d).\n" "Also see -e (--select-tcpip).", }, { .longopt_id = OPT_DISABLE_SCREENSAVER, .longopt = "disable-screensaver", .text = "Disable screensaver while scrcpy is running.", }, { .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).", }, { .shortopt = 'e', .longopt = "select-tcpip", .text = "Use TCP/IP device (if there is exactly one, like adb -e).\n" "Also see -d (--select-usb).", }, { .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.\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).\n" "Also see --hid-mouse.", }, { .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 = "hid-mouse", .text = "Simulate a physical mouse by using HID over AOAv2.\n" "In this mode, the computer mouse is captured to control the " "device directly (relative mouse mode).\n" "LAlt, LSuper or RSuper toggle the capture mode, to give " "control of the mouse back to the computer.\n" "It may only work over USB.\n" "Also see --hid-keyboard.", }, { .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_CLEANUP, .longopt = "no-cleanup", .text = "By default, scrcpy removes the server binary from the device " "and restores the device state (show touches, stay awake and " "power mode) on exit.\n" "This option disables this cleanup." }, { .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, .longopt = "no-clipboard-autosync", .text = "By default, scrcpy automatically synchronizes the computer " "clipboard to the device clipboard before injecting Ctrl+v, " "and the device clipboard to the computer clipboard whenever " "it changes.\n" "This option disables this automatic synchronization." }, { .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, .longopt = "no-downsize-on-error", .text = "By default, on MediaCodec error, scrcpy automatically tries " "again with a lower definition.\n" "This option disables this behavior.", }, { .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 or V4L2 " "sink 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.", }, { .longopt_id = OPT_NO_POWER_ON, .longopt = "no-power-on", .text = "Do not power on the device on start.", }, { .longopt_id = OPT_OTG, .longopt = "otg", .text = "Run in OTG mode: simulate physical keyboard and mouse, " "as if the computer keyboard and mouse were plugged directly " "to the device via an OTG cable.\n" "In this mode, adb (USB debugging) is not necessary, and " "mirroring is disabled.\n" "LAlt, LSuper or RSuper toggle the mouse capture mode, to give " "control of the mouse back to the computer.\n" "If any of --hid-keyboard or --hid-mouse is set, only enable " "keyboard or mouse respectively, otherwise enable both.\n" "It may only work over USB.\n" "See --hid-keyboard and --hid-mouse.", }, { .shortopt = 'p', .longopt = "port", .argdesc = "port[:port]", .text = "Set the TCP port (range) used by the client to listen.\n" "Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":" STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".", }, { .longopt_id = OPT_POWER_OFF_ON_CLOSE, .longopt = "power-off-on-close", .text = "Turn the device screen off when closing scrcpy.", }, { .longopt_id = OPT_PREFER_TEXT, .longopt = "prefer-text", .text = "Inject alpha characters and space as text events instead of " "key events.\n" "This avoids issues when combining multiple keys to enter a " "special character, but breaks the expected behavior of alpha " "keys in games (typically WASD).", }, { .longopt_id = OPT_PRINT_FPS, .longopt = "print-fps", .text = "Start FPS counter, to print framerate logs to the console. " "It can be started or stopped at any time with MOD+i.", }, { .longopt_id = OPT_PUSH_TARGET, .longopt = "push-target", .argdesc = "path", .text = "Set the target directory for pushing files to the device by " "drag & drop. It is passed as is to \"adb push\".\n" "Default is \"/sdcard/Download/\".", }, { .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_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_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.", }, { .longopt_id = OPT_V4L2_SINK, .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" "It requires to lock the video orientation (see " "--lock-video-orientation).\n" "This feature is only available on Linux.", }, { .longopt_id = OPT_V4L2_BUFFER, .longopt = "v4l2-buffer", .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before pushing " "frames. This increases latency to compensate for jitter.\n" "This option is similar to --display-buffer, but specific to " "V4L2 sink.\n" "Default is 0 (no buffering).\n" "This option is only available on Linux.", }, { .shortopt = 'V', .longopt = "verbosity", .argdesc = "value", .text = "Set the log level (verbose, debug, info, warn or error).\n" #ifndef NDEBUG "Default is debug.", #else "Default is info.", #endif }, { .shortopt = 'v', .longopt = "version", .text = "Print the version of scrcpy.", }, { .shortopt = 'w', .longopt = "stay-awake", .text = "Keep the device on while scrcpy is running, when the device " "is plugged in.", }, { .longopt_id = OPT_WINDOW_BORDERLESS, .longopt = "window-borderless", .text = "Disable window decorations (display borderless window)." }, { .longopt_id = OPT_WINDOW_TITLE, .longopt = "window-title", .argdesc = "text", .text = "Set a custom window title.", }, { .longopt_id = OPT_WINDOW_X, .longopt = "window-x", .argdesc = "value", .text = "Set the initial window horizontal position.\n" "Default is \"auto\".", }, { .longopt_id = OPT_WINDOW_Y, .longopt = "window-y", .argdesc = "value", .text = "Set the initial window vertical position.\n" "Default is \"auto\".", }, { .longopt_id = OPT_WINDOW_WIDTH, .longopt = "window-width", .argdesc = "value", .text = "Set the initial window width.\n" "Default is 0 (automatic).", }, { .longopt_id = OPT_WINDOW_HEIGHT, .longopt = "window-height", .argdesc = "value", .text = "Set the initial window height.\n" "Default is 0 (automatic).", }, }; static const struct sc_shortcut shortcuts[] = { { .shortcuts = { "MOD+f" }, .text = "Switch fullscreen mode", }, { .shortcuts = { "MOD+Left" }, .text = "Rotate display left", }, { .shortcuts = { "MOD+Right" }, .text = "Rotate display right", }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", }, { .shortcuts = { "MOD+w", "Double-click on black borders" }, .text = "Resize window to remove black borders", }, { .shortcuts = { "MOD+h", "Middle-click" }, .text = "Click on HOME", }, { .shortcuts = { "MOD+b", "MOD+Backspace", "Right-click (when screen is on)", }, .text = "Click on BACK", }, { .shortcuts = { "MOD+s", "4th-click" }, .text = "Click on APP_SWITCH", }, { .shortcuts = { "MOD+m" }, .text = "Click on MENU", }, { .shortcuts = { "MOD+Up" }, .text = "Click on VOLUME_UP", }, { .shortcuts = { "MOD+Down" }, .text = "Click on VOLUME_DOWN", }, { .shortcuts = { "MOD+p" }, .text = "Click on POWER (turn screen on/off)", }, { .shortcuts = { "Right-click (when screen is off)" }, .text = "Power on", }, { .shortcuts = { "MOD+o" }, .text = "Turn device screen off (keep mirroring)", }, { .shortcuts = { "MOD+Shift+o" }, .text = "Turn device screen on", }, { .shortcuts = { "MOD+r" }, .text = "Rotate device screen", }, { .shortcuts = { "MOD+n", "5th-click" }, .text = "Expand notification panel", }, { .shortcuts = { "MOD+Shift+n" }, .text = "Collapse notification panel", }, { .shortcuts = { "MOD+c" }, .text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)", }, { .shortcuts = { "MOD+x" }, .text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)", }, { .shortcuts = { "MOD+v" }, .text = "Copy computer clipboard to device, then paste (inject PASTE " "keycode, Android >= 7 only)", }, { .shortcuts = { "MOD+Shift+v" }, .text = "Inject computer clipboard text as a sequence of key events", }, { .shortcuts = { "MOD+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 const struct sc_envvar envvars[] = { { .name = "ADB", .text = "Path to adb executable", }, { .name = "ANDROID_SERIAL", .text = "Device serial to use if no selector (-s, -d, -e or " "--tcpip=) is specified", }, { .name = "SCRCPY_ICON_PATH", .text = "Path to the program icon", }, { .name = "SCRCPY_SERVER_PATH", .text = "Path to the server binary", }, }; static const struct sc_exit_status exit_statuses[] = { { .value = 0, .text = "Normal program termination", }, { .value = 1, .text = "Start failure", }, { .value = 2, .text = "Device disconnected while running", }, }; static char * sc_getopt_adapter_create_optstring(void) { struct sc_strbuf buf; if (!sc_strbuf_init(&buf, 64)) { return false; } for (size_t i = 0; i < ARRAY_LEN(options); ++i) { const struct sc_option *opt = &options[i]; if (opt->shortopt) { if (!sc_strbuf_append_char(&buf, opt->shortopt)) { goto error; } // If there is an argument, add ':' if (opt->argdesc) { if (!sc_strbuf_append_char(&buf, ':')) { goto error; } // If the argument is optional, add another ':' if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) { goto error; } } } } return buf.s; error: free(buf.s); return NULL; } static struct option * sc_getopt_adapter_create_longopts(void) { struct option *longopts = malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts)); if (!longopts) { LOG_OOM(); return NULL; } size_t out_idx = 0; for (size_t i = 0; i < ARRAY_LEN(options); ++i) { const struct sc_option *in = &options[i]; // If longopt_id is set, then longopt must be set assert(!in->longopt_id || in->longopt); if (!in->longopt) { // The longopts array must only contain long options continue; } struct option *out = &longopts[out_idx++]; out->name = in->longopt; if (!in->argdesc) { assert(!in->optional_arg); out->has_arg = no_argument; } else if (in->optional_arg) { out->has_arg = optional_argument; } else { out->has_arg = required_argument; } out->flag = NULL; // Either shortopt or longopt_id is set, but not both assert(!!in->shortopt ^ !!in->longopt_id); out->val = in->shortopt ? in->shortopt : in->longopt_id; } // The array must be terminated by a NULL item longopts[out_idx] = (struct option) {0}; return longopts; } static bool sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) { adapter->optstring = sc_getopt_adapter_create_optstring(); if (!adapter->optstring) { return false; } adapter->longopts = sc_getopt_adapter_create_longopts(); if (!adapter->longopts) { free(adapter->optstring); return false; } return true; } static void sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) { free(adapter->optstring); free(adapter->longopts); } static void print_option_usage_header(const struct sc_option *opt) { struct sc_strbuf buf; if (!sc_strbuf_init(&buf, 64)) { goto error; } bool ok = true; (void) ok; // only used for assertions if (opt->shortopt) { ok = sc_strbuf_append_char(&buf, '-'); assert(ok); ok = sc_strbuf_append_char(&buf, opt->shortopt); assert(ok); if (opt->longopt) { ok = sc_strbuf_append_staticstr(&buf, ", "); assert(ok); } } if (opt->longopt) { ok = sc_strbuf_append_staticstr(&buf, "--"); assert(ok); if (!sc_strbuf_append_str(&buf, opt->longopt)) { goto error; } } if (opt->argdesc) { if (opt->optional_arg && !sc_strbuf_append_char(&buf, '[')) { goto error; } if (!sc_strbuf_append_char(&buf, '=')) { goto error; } if (!sc_strbuf_append_str(&buf, opt->argdesc)) { goto error; } if (opt->optional_arg && !sc_strbuf_append_char(&buf, ']')) { goto error; } } printf("\n %s\n", buf.s); free(buf.s); return; error: printf("\n"); } static void print_option_usage(const struct sc_option *opt, unsigned cols) { assert(cols > 8); // sc_str_wrap_lines() requires indent < columns if (!opt->text) { // Option not documented in help (for example because it is deprecated) return; } print_option_usage_header(opt); char *text = sc_str_wrap_lines(opt->text, cols, 8); if (!text) { printf("\n"); return; } printf("%s\n", text); free(text); } static void print_shortcuts_intro(unsigned cols) { char *intro = sc_str_wrap_lines( "In the following list, MOD is the shortcut modifier. By default, it's " "(left) Alt or (left) Super, but it can be configured by " "--shortcut-mod (see above).", cols, 4); if (!intro) { printf("\n"); return; } printf("\n%s\n", intro); free(intro); } static void print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) { assert(cols > 8); // sc_str_wrap_lines() requires indent < columns assert(shortcut->shortcuts[0]); // At least one shortcut assert(shortcut->text); printf("\n"); unsigned i = 0; while (shortcut->shortcuts[i]) { printf(" %s\n", shortcut->shortcuts[i]); ++i; }; char *text = sc_str_wrap_lines(shortcut->text, cols, 8); if (!text) { printf("\n"); return; } printf("%s\n", text); free(text); } static void print_envvar(const struct sc_envvar *envvar, unsigned cols) { assert(cols > 8); // sc_str_wrap_lines() requires indent < columns assert(envvar->name); assert(envvar->text); printf("\n %s\n", envvar->name); char *text = sc_str_wrap_lines(envvar->text, cols, 8); if (!text) { printf("\n"); return; } printf("%s\n", text); free(text); } static void print_exit_status(const struct sc_exit_status *status, unsigned cols) { assert(cols > 8); // sc_str_wrap_lines() requires indent < columns assert(status->text); // The text starts at 9: 4 ident spaces, 3 chars for numeric value, 2 spaces char *text = sc_str_wrap_lines(status->text, cols, 9); if (!text) { printf("\n"); return; } assert(strlen(text) >= 9); // Contains at least the initial identation // text + 9 to remove the initial indentation printf(" %3d %s\n", status->value, text + 9); free(text); } void scrcpy_print_usage(const char *arg0) { #define SC_TERM_COLS_DEFAULT 80 unsigned cols; if (!isatty(STDERR_FILENO)) { // Not a tty cols = SC_TERM_COLS_DEFAULT; } else { bool ok = sc_term_get_size(NULL, &cols); if (!ok) { // Could not get the terminal size cols = SC_TERM_COLS_DEFAULT; } if (cols < 20) { // Do not accept a too small value cols = 20; } } printf("Usage: %s [options]\n\n" "Options:\n", arg0); for (size_t i = 0; i < ARRAY_LEN(options); ++i) { print_option_usage(&options[i], cols); } // Print shortcuts section printf("\nShortcuts:\n"); print_shortcuts_intro(cols); for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) { print_shortcut(&shortcuts[i], cols); } // Print environment variables section printf("\nEnvironment variables:\n"); for (size_t i = 0; i < ARRAY_LEN(envvars); ++i) { print_envvar(&envvars[i], cols); } printf("\nExit status:\n\n"); for (size_t i = 0; i < ARRAY_LEN(exit_statuses); ++i) { print_exit_status(&exit_statuses[i], cols); } } static bool 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_SHORTCUT_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_SHORTCUT_MOD_LCTRL; } else if (STREQ("rctrl", item, key_len)) { mod |= SC_SHORTCUT_MOD_RCTRL; } else if (STREQ("lalt", item, key_len)) { mod |= SC_SHORTCUT_MOD_LALT; } else if (STREQ("ralt", item, key_len)) { mod |= SC_SHORTCUT_MOD_RALT; } else if (STREQ("lsuper", item, key_len)) { mod |= SC_SHORTCUT_MOD_LSUPER; } else if (STREQ("rsuper", item, key_len)) { mod |= SC_SHORTCUT_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 'd': opts->select_usb = true; break; case 'e': opts->select_tcpip = true; 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': #ifdef HAVE_USB opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID; break; #else LOGE("HID over AOA (-K/--hid-keyboard) is disabled."); return false; #endif 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 'M': #ifdef HAVE_USB opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID; break; #else LOGE("HID over AOA (-M/--hid-mouse) is disabled."); return false; #endif 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; case OPT_NO_DOWNSIZE_ON_ERROR: opts->downsize_on_error = false; break; case OPT_NO_CLEANUP: opts->cleanup = false; break; case OPT_NO_POWER_ON: opts->power_on = false; break; case OPT_PRINT_FPS: opts->start_fps_counter = true; break; case OPT_OTG: #ifdef HAVE_USB opts->otg = true; break; #else LOGE("OTG mode (--otg) is disabled."); return false; #endif case OPT_V4L2_SINK: #ifdef HAVE_V4L2 opts->v4l2_device = optarg; break; #else LOGE("V4L2 (--v4l2-sink) is disabled (or unsupported on this " "platform)."); return false; #endif case OPT_V4L2_BUFFER: #ifdef HAVE_V4L2 if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { return false; } break; #else LOGE("V4L2 (--v4l2-buffer) is only available on Linux."); return false; #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); unsigned selectors = !!opts->serial + !!opts->tcpip_dst + opts->select_tcpip + opts->select_usb; if (selectors > 1) { LOGE("At most one device selector option may be passed, among:\n" " --serial (-s)\n" " --select-usb (-d)\n" " --select-tcpip (-e)\n" " --tcpip= (with an argument)"); return false; } #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) { if (opts->lock_video_orientation == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " "See --lock-video-orientation."); opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; } // V4L2 could not handle size change. // Do not log because downsizing on error is the default behavior, // not an explicit request from the user. opts->downsize_on_error = false; } if (opts->v4l2_buffer && !opts->v4l2_device) { LOGE("V4L2 buffer value without V4L2 sink\n"); return false; } #else if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); return false; } #endif 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) { if (opts->turn_screen_off) { LOGE("Could not request to turn screen off if control is disabled"); return false; } if (opts->stay_awake) { LOGE("Could not request to stay awake if control is disabled"); return false; } if (opts->show_touches) { LOGE("Could not request to show touches if control is disabled"); return false; } if (opts->power_off_on_close) { LOGE("Could not request power off on close if control is disabled"); return false; } } #ifdef HAVE_USB # ifdef _WIN32 if (!opts->otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) { LOGE("On Windows, it is not possible to open a USB device already open " "by another process (like adb)."); LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in " "OTG mode (--otg)."); return false; } # endif if (opts->otg) { // OTG mode is compatible with only very few options. // Only report obvious errors. if (opts->record_filename) { LOGE("OTG mode: could not record"); return false; } if (opts->turn_screen_off) { LOGE("OTG mode: could not turn screen off"); return false; } if (opts->stay_awake) { LOGE("OTG mode: could not stay awake"); return false; } if (opts->show_touches) { LOGE("OTG mode: could not request to show touches"); return false; } if (opts->power_off_on_close) { LOGE("OTG mode: could not request power off on close"); return false; } if (opts->display_id) { LOGE("OTG mode: could not select display"); return false; } # ifdef HAVE_V4L2 if (opts->v4l2_device) { LOGE("OTG mode: could not sink to V4L2 device"); return false; } # endif } #endif 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.25/app/src/cli.h000066400000000000000000000006561435104021100152310ustar00rootroot00000000000000#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.25/app/src/clock.c000066400000000000000000000070301435104021100155410ustar00rootroot00000000000000#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: %f * 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.25/app/src/clock.h000066400000000000000000000035631435104021100155550ustar00rootroot00000000000000#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.25/app/src/common.h000066400000000000000000000005541435104021100157470ustar00rootroot00000000000000#ifndef SC_COMMON_H #define SC_COMMON_H #include "config.h" #include "compat.h" #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y) #define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) ) #define container_of(ptr, type, member) \ ((type *) (((char *) (ptr)) - offsetof(type, member))) #endif scrcpy-1.25/app/src/compat.c000066400000000000000000000016651435104021100157410ustar00rootroot00000000000000#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.25/app/src/compat.h000066400000000000000000000033161435104021100157410ustar00rootroot00000000000000#ifndef SC_COMPAT_H #define SC_COMPAT_H #include "config.h" #include #include #ifndef __WIN32 # define PRIu64_ PRIu64 # define SC_PRIsizet "zu" #else # define PRIu64_ "I64u" // Windows... # define SC_PRIsizet "Iu" #endif // In ffmpeg/doc/APIchanges: // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h // Deprecate use of av_register_input_format(), av_register_output_format(), // 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, 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.25/app/src/control_msg.c000066400000000000000000000225051435104021100170000ustar00rootroot00000000000000#include "control_msg.h" #include #include #include #include #include "util/binary.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", "pointer-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 inline const char * get_well_known_pointer_id_name(uint64_t pointer_id) { switch (pointer_id) { case POINTER_ID_MOUSE: return "mouse"; case POINTER_ID_GENERIC_FINGER: return "finger"; case POINTER_ID_VIRTUAL_MOUSE: return "vmouse"; case POINTER_ID_VIRTUAL_FINGER: return "vfinger"; default: return NULL; } } static void write_position(uint8_t *buf, const struct sc_position *position) { sc_write32be(&buf[0], position->point.x); sc_write32be(&buf[4], position->point.y); sc_write16be(&buf[8], position->screen_size.width); sc_write16be(&buf[10], position->screen_size.height); } // write length (4 bytes) + string (non null-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); sc_write32be(buf, len); memcpy(&buf[4], utf8, len); return 4 + len; } size_t sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { buf[0] = msg->type; switch (msg->type) { case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: buf[1] = msg->inject_keycode.action; sc_write32be(&buf[2], msg->inject_keycode.keycode); sc_write32be(&buf[6], msg->inject_keycode.repeat); sc_write32be(&buf[10], msg->inject_keycode.metastate); return 14; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: { size_t len = write_string(msg->inject_text.text, SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]); return 1 + len; } case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: buf[1] = msg->inject_touch_event.action; sc_write64be(&buf[2], msg->inject_touch_event.pointer_id); write_position(&buf[10], &msg->inject_touch_event.position); uint16_t pressure = sc_float_to_u16fp(msg->inject_touch_event.pressure); sc_write16be(&buf[22], pressure); sc_write32be(&buf[24], msg->inject_touch_event.buttons); return 28; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); int16_t hscroll = sc_float_to_i16fp(msg->inject_scroll_event.hscroll); int16_t vscroll = sc_float_to_i16fp(msg->inject_scroll_event.vscroll); sc_write16be(&buf[13], (uint16_t) hscroll); sc_write16be(&buf[15], (uint16_t) vscroll); sc_write32be(&buf[17], msg->inject_scroll_event.buttons); return 21; case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: buf[1] = msg->inject_keycode.action; return 2; case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD: buf[1] = msg->get_clipboard.copy_key; return 2; case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: sc_write64be(&buf[1], msg->set_clipboard.sequence); buf[9] = !!msg->set_clipboard.paste; size_t len = write_string(msg->set_clipboard.text, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH, &buf[10]); return 10 + len; case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: buf[1] = msg->set_screen_power_mode.mode; return 2; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: // no additional data return 1; default: LOGW("Unknown message type: %u", (unsigned) msg->type); return 0; } } void sc_control_msg_log(const struct sc_control_msg *msg) { #define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__) switch (msg->type) { case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action), (int) msg->inject_keycode.keycode, msg->inject_keycode.repeat, (long) msg->inject_keycode.metastate); break; case SC_CONTROL_MSG_TYPE_INJECT_TEXT: LOG_CMSG("text \"%s\"", msg->inject_text.text); break; case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: { int action = msg->inject_touch_event.action & AMOTION_EVENT_ACTION_MASK; uint64_t id = msg->inject_touch_event.pointer_id; const char *pointer_name = get_well_known_pointer_id_name(id); if (pointer_name) { // string pointer id LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%f buttons=%06lx", pointer_name, MOTIONEVENT_ACTION_LABEL(action), msg->inject_touch_event.position.point.x, msg->inject_touch_event.position.point.y, msg->inject_touch_event.pressure, (long) msg->inject_touch_event.buttons); } else { // numeric pointer id LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%" PRIi32 " pressure=%f 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 SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%f" " vscroll=%f buttons=%06lx", msg->inject_scroll_event.position.point.x, msg->inject_scroll_event.position.point.y, msg->inject_scroll_event.hscroll, msg->inject_scroll_event.vscroll, (long) msg->inject_scroll_event.buttons); break; case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON: LOG_CMSG("back-or-screen-on %s", KEYEVENT_ACTION_LABEL(msg->inject_keycode.action)); break; case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD: LOG_CMSG("get clipboard copy_key=%s", copy_key_labels[msg->get_clipboard.copy_key]); break; case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"", msg->set_clipboard.sequence, msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.text); break; case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: LOG_CMSG("power mode %s", SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); break; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); break; case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: LOG_CMSG("expand settings panel"); break; case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: LOG_CMSG("collapse panels"); break; case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: LOG_CMSG("rotate device"); break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; } } void sc_control_msg_destroy(struct sc_control_msg *msg) { switch (msg->type) { case SC_CONTROL_MSG_TYPE_INJECT_TEXT: free(msg->inject_text.text); break; case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; default: // do nothing break; } } scrcpy-1.25/app/src/control_msg.h000066400000000000000000000062431435104021100170060ustar00rootroot00000000000000#ifndef SC_CONTROLMSG_H #define SC_CONTROLMSG_H #include "common.h" #include #include #include #include "android/input.h" #include "android/keycodes.h" #include "coords.h" #define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300 // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes #define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14) #define POINTER_ID_MOUSE UINT64_C(-1) #define POINTER_ID_GENERIC_FINGER UINT64_C(-2) // Used for injecting an additional virtual pointer for pinch-to-zoom #define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3) #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4) enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, SC_CONTROL_MSG_TYPE_INJECT_TEXT, SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; enum sc_screen_power_mode { // see SC_SCREEN_POWER_MODE_OFF = 0, SC_SCREEN_POWER_MODE_NORMAL = 2, }; enum sc_copy_key { SC_COPY_KEY_NONE, SC_COPY_KEY_COPY, SC_COPY_KEY_CUT, }; struct sc_control_msg { enum sc_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; float hscroll; float vscroll; enum android_motionevent_buttons buttons; } inject_scroll_event; struct { enum android_keyevent_action action; // action for the BACK key // screen may only be turned on on ACTION_DOWN } back_or_screen_on; struct { enum sc_copy_key copy_key; } get_clipboard; struct { uint64_t sequence; char *text; // owned, to be freed by free() bool paste; } set_clipboard; struct { enum sc_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 sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf); void sc_control_msg_log(const struct sc_control_msg *msg); void sc_control_msg_destroy(struct sc_control_msg *msg); #endif scrcpy-1.25/app/src/controller.c000066400000000000000000000073001435104021100166310ustar00rootroot00000000000000#include "controller.h" #include #include "util/log.h" bool sc_controller_init(struct sc_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 sc_controller_destroy(struct sc_controller *controller) { sc_cond_destroy(&controller->msg_cond); sc_mutex_destroy(&controller->mutex); struct sc_control_msg msg; while (cbuf_take(&controller->queue, &msg)) { sc_control_msg_destroy(&msg); } receiver_destroy(&controller->receiver); } bool sc_controller_push_msg(struct sc_controller *controller, const struct sc_control_msg *msg) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { sc_control_msg_log(msg); } sc_mutex_lock(&controller->mutex); bool was_empty = cbuf_is_empty(&controller->queue); bool res = cbuf_push(&controller->queue, *msg); if (was_empty) { sc_cond_signal(&controller->msg_cond); } sc_mutex_unlock(&controller->mutex); return res; } static bool process_msg(struct sc_controller *controller, const struct sc_control_msg *msg) { static unsigned char serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; size_t length = sc_control_msg_serialize(msg, serialized_msg); if (!length) { return false; } ssize_t w = net_send_all(controller->control_socket, serialized_msg, length); return (size_t) w == length; } static int run_controller(void *data) { struct sc_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 sc_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); sc_control_msg_destroy(&msg); if (!ok) { LOGD("Could not write msg to socket"); break; } } return 0; } bool sc_controller_start(struct sc_controller *controller) { LOGD("Starting controller thread"); bool ok = sc_thread_create(&controller->thread, run_controller, "scrcpy-ctl", controller); if (!ok) { LOGE("Could not start controller thread"); return false; } if (!receiver_start(&controller->receiver)) { sc_controller_stop(controller); sc_thread_join(&controller->thread, NULL); return false; } return true; } void sc_controller_stop(struct sc_controller *controller) { sc_mutex_lock(&controller->mutex); controller->stopped = true; sc_cond_signal(&controller->msg_cond); sc_mutex_unlock(&controller->mutex); } void sc_controller_join(struct sc_controller *controller) { sc_thread_join(&controller->thread, NULL); receiver_join(&controller->receiver); } scrcpy-1.25/app/src/controller.h000066400000000000000000000017631435104021100166450ustar00rootroot00000000000000#ifndef SC_CONTROLLER_H #define SC_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 sc_control_msg_queue CBUF(struct sc_control_msg, 64); struct sc_controller { sc_socket control_socket; sc_thread thread; sc_mutex mutex; sc_cond msg_cond; bool stopped; struct sc_control_msg_queue queue; struct receiver receiver; }; bool sc_controller_init(struct sc_controller *controller, sc_socket control_socket, struct sc_acksync *acksync); void sc_controller_destroy(struct sc_controller *controller); bool sc_controller_start(struct sc_controller *controller); void sc_controller_stop(struct sc_controller *controller); void sc_controller_join(struct sc_controller *controller); bool sc_controller_push_msg(struct sc_controller *controller, const struct sc_control_msg *msg); #endif scrcpy-1.25/app/src/coords.h000066400000000000000000000006621435104021100157500ustar00rootroot00000000000000#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.25/app/src/decoder.c000066400000000000000000000107231435104021100160560ustar00rootroot00000000000000#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 sc_decoder, packet_sink) static void sc_decoder_close_first_sinks(struct sc_decoder *decoder, unsigned count) { while (count) { struct sc_frame_sink *sink = decoder->sinks[--count]; sink->ops->close(sink); } } static inline void sc_decoder_close_sinks(struct sc_decoder *decoder) { sc_decoder_close_first_sinks(decoder, decoder->sink_count); } static bool sc_decoder_open_sinks(struct sc_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); sc_decoder_close_first_sinks(decoder, i); return false; } } return true; } static bool sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) { decoder->codec_ctx = avcodec_alloc_context3(codec); if (!decoder->codec_ctx) { LOG_OOM(); return false; } decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY; 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 (!sc_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 sc_decoder_close(struct sc_decoder *decoder) { sc_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 sc_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 sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) { bool is_config = packet->pts == AV_NOPTS_VALUE; if (is_config) { // nothing to do return true; } int ret = avcodec_send_packet(decoder->codec_ctx, packet); 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 sc_decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { struct sc_decoder *decoder = DOWNCAST(sink); return sc_decoder_open(decoder, codec); } static void sc_decoder_packet_sink_close(struct sc_packet_sink *sink) { struct sc_decoder *decoder = DOWNCAST(sink); sc_decoder_close(decoder); } static bool sc_decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { struct sc_decoder *decoder = DOWNCAST(sink); return sc_decoder_push(decoder, packet); } void sc_decoder_init(struct sc_decoder *decoder) { decoder->sink_count = 0; static const struct sc_packet_sink_ops ops = { .open = sc_decoder_packet_sink_open, .close = sc_decoder_packet_sink_close, .push = sc_decoder_packet_sink_push, }; decoder->packet_sink.ops = &ops; } void sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink) { assert(decoder->sink_count < SC_DECODER_MAX_SINKS); assert(sink); assert(sink->ops); decoder->sinks[decoder->sink_count++] = sink; } scrcpy-1.25/app/src/decoder.h000066400000000000000000000010761435104021100160640ustar00rootroot00000000000000#ifndef SC_DECODER_H #define SC_DECODER_H #include "common.h" #include "trait/packet_sink.h" #include #include #include #define SC_DECODER_MAX_SINKS 2 struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS]; unsigned sink_count; AVCodecContext *codec_ctx; AVFrame *frame; }; void sc_decoder_init(struct sc_decoder *decoder); void sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink); #endif scrcpy-1.25/app/src/demuxer.c000066400000000000000000000172341435104021100161260ustar00rootroot00000000000000#include "demuxer.h" #include #include #include #include "decoder.h" #include "events.h" #include "recorder.h" #include "util/binary.h" #include "util/log.h" #define SC_PACKET_HEADER_SIZE 12 #define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63) #define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62) #define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1) static bool sc_demuxer_recv_packet(struct sc_demuxer *demuxer, 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. // // The most significant bits of the PTS are used for packet flags: // // byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0 // CK...... ........ ........ ........ ........ ........ ........ ........ // ^^<-------------------------------------------------------------------> // || PTS // | `- key frame // `-- config packet uint8_t header[SC_PACKET_HEADER_SIZE]; ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE); if (r < SC_PACKET_HEADER_SIZE) { return false; } uint64_t pts_flags = sc_read64be(header); uint32_t len = sc_read32be(&header[8]); assert(len); if (av_new_packet(packet, len)) { LOG_OOM(); return false; } r = net_recv_all(demuxer->socket, packet->data, len); if (r < 0 || ((uint32_t) r) < len) { av_packet_unref(packet); return false; } if (pts_flags & SC_PACKET_FLAG_CONFIG) { packet->pts = AV_NOPTS_VALUE; } else { packet->pts = pts_flags & SC_PACKET_PTS_MASK; } if (pts_flags & SC_PACKET_FLAG_KEY_FRAME) { packet->flags |= AV_PKT_FLAG_KEY; } packet->dts = packet->pts; return true; } static bool push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) { for (unsigned i = 0; i < demuxer->sink_count; ++i) { struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->push(sink, packet)) { LOGE("Could not send config packet to sink %d", i); return false; } } return true; } static bool sc_demuxer_push_packet(struct sc_demuxer *demuxer, 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 (demuxer->pending || is_config) { size_t offset; if (demuxer->pending) { offset = demuxer->pending->size; if (av_grow_packet(demuxer->pending, packet->size)) { LOG_OOM(); return false; } } else { offset = 0; demuxer->pending = av_packet_alloc(); if (!demuxer->pending) { LOG_OOM(); return false; } if (av_new_packet(demuxer->pending, packet->size)) { LOG_OOM(); av_packet_free(&demuxer->pending); return false; } } memcpy(demuxer->pending->data + offset, packet->data, packet->size); if (!is_config) { // prepare the concat packet to send to the decoder demuxer->pending->pts = packet->pts; demuxer->pending->dts = packet->dts; demuxer->pending->flags = packet->flags; packet = demuxer->pending; } } bool ok = push_packet_to_sinks(demuxer, packet); if (!is_config && demuxer->pending) { // the pending packet must be discarded (consumed or error) av_packet_free(&demuxer->pending); } if (!ok) { LOGE("Could not process packet"); return false; } return true; } static void sc_demuxer_close_first_sinks(struct sc_demuxer *demuxer, unsigned count) { while (count) { struct sc_packet_sink *sink = demuxer->sinks[--count]; sink->ops->close(sink); } } static inline void sc_demuxer_close_sinks(struct sc_demuxer *demuxer) { sc_demuxer_close_first_sinks(demuxer, demuxer->sink_count); } static bool sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) { for (unsigned i = 0; i < demuxer->sink_count; ++i) { struct sc_packet_sink *sink = demuxer->sinks[i]; if (!sink->ops->open(sink, codec)) { LOGE("Could not open packet sink %d", i); sc_demuxer_close_first_sinks(demuxer, i); return false; } } return true; } static int run_demuxer(void *data) { struct sc_demuxer *demuxer = data; const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); if (!codec) { LOGE("H.264 decoder not found"); goto end; } demuxer->codec_ctx = avcodec_alloc_context3(codec); if (!demuxer->codec_ctx) { LOG_OOM(); goto end; } if (!sc_demuxer_open_sinks(demuxer, codec)) { LOGE("Could not open demuxer sinks"); goto finally_free_codec_ctx; } demuxer->parser = av_parser_init(AV_CODEC_ID_H264); if (!demuxer->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! demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; AVPacket *packet = av_packet_alloc(); if (!packet) { LOG_OOM(); goto finally_close_parser; } for (;;) { bool ok = sc_demuxer_recv_packet(demuxer, packet); if (!ok) { // end of stream break; } ok = sc_demuxer_push_packet(demuxer, packet); av_packet_unref(packet); if (!ok) { // cannot process packet (error already logged) break; } } LOGD("End of frames"); if (demuxer->pending) { av_packet_free(&demuxer->pending); } av_packet_free(&packet); finally_close_parser: av_parser_close(demuxer->parser); finally_close_sinks: sc_demuxer_close_sinks(demuxer); finally_free_codec_ctx: avcodec_free_context(&demuxer->codec_ctx); end: demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata); return 0; } void sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) { demuxer->socket = socket; demuxer->pending = NULL; demuxer->sink_count = 0; assert(cbs && cbs->on_eos); demuxer->cbs = cbs; demuxer->cbs_userdata = cbs_userdata; } void sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) { assert(demuxer->sink_count < SC_DEMUXER_MAX_SINKS); assert(sink); assert(sink->ops); demuxer->sinks[demuxer->sink_count++] = sink; } bool sc_demuxer_start(struct sc_demuxer *demuxer) { LOGD("Starting demuxer thread"); bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer", demuxer); if (!ok) { LOGE("Could not start demuxer thread"); return false; } return true; } void sc_demuxer_join(struct sc_demuxer *demuxer) { sc_thread_join(&demuxer->thread, NULL); } scrcpy-1.25/app/src/demuxer.h000066400000000000000000000021511435104021100161230ustar00rootroot00000000000000#ifndef SC_DEMUXER_H #define SC_DEMUXER_H #include "common.h" #include #include #include #include #include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" #define SC_DEMUXER_MAX_SINKS 2 struct sc_demuxer { sc_socket socket; sc_thread thread; struct sc_packet_sink *sinks[SC_DEMUXER_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 sc_demuxer_callbacks *cbs; void *cbs_userdata; }; struct sc_demuxer_callbacks { void (*on_eos)(struct sc_demuxer *demuxer, void *userdata); }; void sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket, const struct sc_demuxer_callbacks *cbs, void *cbs_userdata); void sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink); bool sc_demuxer_start(struct sc_demuxer *demuxer); void sc_demuxer_join(struct sc_demuxer *demuxer); #endif scrcpy-1.25/app/src/device_msg.c000066400000000000000000000026611435104021100165600ustar00rootroot00000000000000#include "device_msg.h" #include #include #include #include "util/binary.h" #include "util/log.h" ssize_t device_msg_deserialize(const unsigned char *buf, size_t len, struct device_msg *msg) { if (len < 5) { // at least type + empty string length return 0; // not available } msg->type = buf[0]; switch (msg->type) { case DEVICE_MSG_TYPE_CLIPBOARD: { size_t clipboard_len = sc_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 = sc_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.25/app/src/device_msg.h000066400000000000000000000015311435104021100165600ustar00rootroot00000000000000#ifndef SC_DEVICEMSG_H #define SC_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.25/app/src/events.h000066400000000000000000000004421435104021100157570ustar00rootroot00000000000000#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) #define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4) scrcpy-1.25/app/src/file_pusher.c000066400000000000000000000106561435104021100167630ustar00rootroot00000000000000#include "file_pusher.h" #include #include #include "adb/adb.h" #include "util/log.h" #include "util/process_intr.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" static void sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) { free(req->file); } bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, const char *push_target) { assert(serial); cbuf_init(&fp->queue); bool ok = sc_mutex_init(&fp->mutex); if (!ok) { return false; } ok = sc_cond_init(&fp->event_cond); if (!ok) { sc_mutex_destroy(&fp->mutex); return false; } ok = sc_intr_init(&fp->intr); if (!ok) { sc_cond_destroy(&fp->event_cond); sc_mutex_destroy(&fp->mutex); return false; } fp->serial = strdup(serial); if (!fp->serial) { LOG_OOM(); sc_intr_destroy(&fp->intr); sc_cond_destroy(&fp->event_cond); sc_mutex_destroy(&fp->mutex); return false; } // lazy initialization fp->initialized = false; fp->stopped = false; fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; return true; } void sc_file_pusher_destroy(struct sc_file_pusher *fp) { sc_cond_destroy(&fp->event_cond); sc_mutex_destroy(&fp->mutex); sc_intr_destroy(&fp->intr); free(fp->serial); struct sc_file_pusher_request req; while (cbuf_take(&fp->queue, &req)) { sc_file_pusher_request_destroy(&req); } } bool sc_file_pusher_request(struct sc_file_pusher *fp, enum sc_file_pusher_action action, char *file) { // start file_pusher if it's used for the first time if (!fp->initialized) { if (!sc_file_pusher_start(fp)) { return false; } fp->initialized = true; } LOGI("Request to %s %s", action == SC_FILE_PUSHER_ACTION_INSTALL_APK ? "install" : "push", file); struct sc_file_pusher_request req = { .action = action, .file = file, }; sc_mutex_lock(&fp->mutex); bool was_empty = cbuf_is_empty(&fp->queue); bool res = cbuf_push(&fp->queue, req); if (was_empty) { sc_cond_signal(&fp->event_cond); } sc_mutex_unlock(&fp->mutex); return res; } static int run_file_pusher(void *data) { struct sc_file_pusher *fp = data; struct sc_intr *intr = &fp->intr; const char *serial = fp->serial; assert(serial); const char *push_target = fp->push_target; assert(push_target); for (;;) { sc_mutex_lock(&fp->mutex); while (!fp->stopped && cbuf_is_empty(&fp->queue)) { sc_cond_wait(&fp->event_cond, &fp->mutex); } if (fp->stopped) { // stop immediately, do not process further events sc_mutex_unlock(&fp->mutex); break; } struct sc_file_pusher_request req; bool non_empty = cbuf_take(&fp->queue, &req); assert(non_empty); (void) non_empty; sc_mutex_unlock(&fp->mutex); if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) { LOGI("Installing %s...", req.file); bool ok = sc_adb_install(intr, serial, req.file, 0); if (ok) { LOGI("%s successfully installed", req.file); } else { LOGE("Failed to install %s", req.file); } } else { LOGI("Pushing %s...", req.file); bool ok = sc_adb_push(intr, serial, req.file, push_target, 0); if (ok) { LOGI("%s successfully pushed to %s", req.file, push_target); } else { LOGE("Failed to push %s to %s", req.file, push_target); } } sc_file_pusher_request_destroy(&req); } return 0; } bool sc_file_pusher_start(struct sc_file_pusher *fp) { LOGD("Starting file_pusher thread"); bool ok = sc_thread_create(&fp->thread, run_file_pusher, "scrcpy-file", fp); if (!ok) { LOGE("Could not start file_pusher thread"); return false; } return true; } void sc_file_pusher_stop(struct sc_file_pusher *fp) { sc_mutex_lock(&fp->mutex); fp->stopped = true; sc_cond_signal(&fp->event_cond); sc_intr_interrupt(&fp->intr); sc_mutex_unlock(&fp->mutex); } void sc_file_pusher_join(struct sc_file_pusher *fp) { sc_thread_join(&fp->thread, NULL); } scrcpy-1.25/app/src/file_pusher.h000066400000000000000000000023011435104021100167540ustar00rootroot00000000000000#ifndef SC_FILE_PUSHER_H #define SC_FILE_PUSHER_H #include "common.h" #include #include "util/cbuf.h" #include "util/thread.h" #include "util/intr.h" enum sc_file_pusher_action { SC_FILE_PUSHER_ACTION_INSTALL_APK, SC_FILE_PUSHER_ACTION_PUSH_FILE, }; struct sc_file_pusher_request { enum sc_file_pusher_action action; char *file; }; struct sc_file_pusher_request_queue CBUF(struct sc_file_pusher_request, 16); struct sc_file_pusher { char *serial; const char *push_target; sc_thread thread; sc_mutex mutex; sc_cond event_cond; bool stopped; bool initialized; struct sc_file_pusher_request_queue queue; struct sc_intr intr; }; bool sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial, const char *push_target); void sc_file_pusher_destroy(struct sc_file_pusher *fp); bool sc_file_pusher_start(struct sc_file_pusher *fp); void sc_file_pusher_stop(struct sc_file_pusher *fp); void sc_file_pusher_join(struct sc_file_pusher *fp); // take ownership of file, and will free() it bool sc_file_pusher_request(struct sc_file_pusher *fp, enum sc_file_pusher_action action, char *file); #endif scrcpy-1.25/app/src/fps_counter.c000066400000000000000000000115661435104021100170060ustar00rootroot00000000000000#include "fps_counter.h" #include #include "util/log.h" #define SC_FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool sc_fps_counter_init(struct sc_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 sc_fps_counter_destroy(struct sc_fps_counter *counter) { sc_cond_destroy(&counter->state_cond); sc_mutex_destroy(&counter->mutex); } static inline bool is_started(struct sc_fps_counter *counter) { return atomic_load_explicit(&counter->started, memory_order_acquire); } static inline void set_started(struct sc_fps_counter *counter, bool started) { atomic_store_explicit(&counter->started, started, memory_order_release); } // must be called with mutex locked static void display_fps(struct sc_fps_counter *counter) { unsigned rendered_per_second = counter->nr_rendered * SC_TICK_FREQ / SC_FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); } else { LOGI("%u fps", rendered_per_second); } } // must be called with mutex locked static void check_interval_expired(struct sc_fps_counter *counter, sc_tick 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) / SC_FPS_COUNTER_INTERVAL + 1; counter->next_timestamp += SC_FPS_COUNTER_INTERVAL * elapsed_slices; } static int run_fps_counter(void *data) { struct sc_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 sc_fps_counter_start(struct sc_fps_counter *counter) { sc_mutex_lock(&counter->mutex); counter->next_timestamp = sc_tick_now() + SC_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, "scrcpy-fps", counter); if (!ok) { LOGE("Could not start FPS counter thread"); return false; } counter->thread_started = true; } LOGI("FPS counter started"); return true; } void sc_fps_counter_stop(struct sc_fps_counter *counter) { set_started(counter, false); sc_cond_signal(&counter->state_cond); LOGI("FPS counter stopped"); } bool sc_fps_counter_is_started(struct sc_fps_counter *counter) { return is_started(counter); } void sc_fps_counter_interrupt(struct sc_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 sc_fps_counter_join(struct sc_fps_counter *counter) { if (counter->thread_started) { // interrupted must be set by the thread calling join(), so no need to // lock for the assertion assert(counter->interrupted); sc_thread_join(&counter->thread, NULL); } } void sc_fps_counter_add_rendered_frame(struct sc_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 sc_fps_counter_add_skipped_frame(struct sc_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.25/app/src/fps_counter.h000066400000000000000000000023631435104021100170060ustar00rootroot00000000000000#ifndef SC_FPSCOUNTER_H #define SC_FPSCOUNTER_H #include "common.h" #include #include #include #include "util/thread.h" struct sc_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 sc_fps_counter_init(struct sc_fps_counter *counter); void sc_fps_counter_destroy(struct sc_fps_counter *counter); bool sc_fps_counter_start(struct sc_fps_counter *counter); void sc_fps_counter_stop(struct sc_fps_counter *counter); bool sc_fps_counter_is_started(struct sc_fps_counter *counter); // request to stop the thread (on quit) // must be called before sc_fps_counter_join() void sc_fps_counter_interrupt(struct sc_fps_counter *counter); void sc_fps_counter_join(struct sc_fps_counter *counter); void sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter); void sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter); #endif scrcpy-1.25/app/src/frame_buffer.c000066400000000000000000000043161435104021100170750ustar00rootroot00000000000000#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) { // 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; } sc_mutex_lock(&fb->mutex); // Now that av_frame_ref() succeeded, we can replace the previous // pending_frame swap_frames(&fb->pending_frame, &fb->tmp_frame); 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.25/app/src/frame_buffer.h000066400000000000000000000017331435104021100171020ustar00rootroot00000000000000#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.25/app/src/icon.c000066400000000000000000000175701435104021100154100ustar00rootroot00000000000000#include "icon.h" #include #include #include #include #include #include #include "config.h" #include "compat.h" #include "util/file.h" #include "util/log.h" #include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png" static char * get_icon_path(void) { #ifdef __WINDOWS__ const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); #else const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); #endif if (icon_path_env) { // if the envvar is set, use it #ifdef __WINDOWS__ char *icon_path = sc_str_from_wchars(icon_path_env); #else char *icon_path = strdup(icon_path_env); #endif if (!icon_path) { LOG_OOM(); return NULL; } LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); return icon_path; } #ifndef PORTABLE LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); if (!icon_path) { LOG_OOM(); return NULL; } #else char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); if (!icon_path) { LOGE("Could not get icon path"); return NULL; } LOGD("Using icon (portable): %s", icon_path); #endif return icon_path; } static AVFrame * decode_image(const char *path) { AVFrame *result = NULL; AVFormatContext *ctx = avformat_alloc_context(); if (!ctx) { LOG_OOM(); return NULL; } if (avformat_open_input(&ctx, path, NULL, NULL) < 0) { LOGE("Could not open 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; const AVCodec *codec = avcodec_find_decoder(params->codec_id); if (!codec) { LOGE("Could not find image decoder"); goto close_input; } AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); if (!codec_ctx) { LOG_OOM(); goto close_input; } if (avcodec_parameters_to_context(codec_ctx, params) < 0) { LOGE("Could not fill codec context"); goto free_codec_ctx; } if (avcodec_open2(codec_ctx, codec, NULL) < 0) { LOGE("Could not open image codec"); goto free_codec_ctx; } AVFrame *frame = av_frame_alloc(); if (!frame) { LOG_OOM(); goto close_codec; } AVPacket *packet = av_packet_alloc(); if (!packet) { LOG_OOM(); av_frame_free(&frame); goto close_codec; } if (av_read_frame(ctx, packet) < 0) { LOGE("Could not read frame"); av_packet_free(&packet); av_frame_free(&frame); goto close_codec; } int ret; if ((ret = avcodec_send_packet(codec_ctx, packet)) < 0) { LOGE("Could not send icon packet: %d", ret); av_packet_free(&packet); av_frame_free(&frame); goto close_codec; } if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) { LOGE("Could not receive icon frame: %d", ret); av_packet_free(&packet); av_frame_free(&frame); goto close_codec; } av_packet_free(&packet); result = frame; close_codec: avcodec_close(codec_ctx); free_codec_ctx: avcodec_free_context(&codec_ctx); close_input: avformat_close_input(&ctx); free_ctx: avformat_free_context(ctx); return result; } #if !SDL_VERSION_ATLEAST(2, 0, 10) // SDL_PixelFormatEnum has been introduced in SDL 2.0.10. Use int for older SDL // versions. typedef int SDL_PixelFormatEnum; #endif static SDL_PixelFormatEnum to_sdl_pixel_format(enum AVPixelFormat fmt) { switch (fmt) { case AV_PIX_FMT_RGB24: return SDL_PIXELFORMAT_RGB24; case AV_PIX_FMT_BGR24: return SDL_PIXELFORMAT_BGR24; case AV_PIX_FMT_ARGB: return SDL_PIXELFORMAT_ARGB32; case AV_PIX_FMT_RGBA: return SDL_PIXELFORMAT_RGBA32; case AV_PIX_FMT_ABGR: return SDL_PIXELFORMAT_ABGR32; case AV_PIX_FMT_BGRA: return SDL_PIXELFORMAT_BGRA32; case AV_PIX_FMT_RGB565BE: return SDL_PIXELFORMAT_RGB565; case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_RGB555; case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565; case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555; case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444; #if SDL_VERSION_ATLEAST(2, 0, 12) case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444; #endif case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8; default: return SDL_PIXELFORMAT_UNKNOWN; } } static SDL_Surface * load_from_path(const char *path) { AVFrame *frame = decode_image(path); if (!frame) { return NULL; } const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(frame->format); if (!desc) { LOGE("Could not get icon format descriptor"); goto error; } bool is_packed = !(desc->flags & AV_PIX_FMT_FLAG_PLANAR); if (!is_packed) { LOGE("Could not load non-packed icon"); goto error; } SDL_PixelFormatEnum format = to_sdl_pixel_format(frame->format); if (format == SDL_PIXELFORMAT_UNKNOWN) { LOGE("Unsupported icon pixel format: %s (%d)", desc->name, frame->format); goto error; } int bits_per_pixel = av_get_bits_per_pixel(desc); SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormatFrom(frame->data[0], frame->width, frame->height, bits_per_pixel, frame->linesize[0], format); if (!surface) { LOGE("Could not create icon surface"); goto error; } if (frame->format == AV_PIX_FMT_PAL8) { // Initialize the SDL palette uint8_t *data = frame->data[1]; SDL_Color colors[256]; for (int i = 0; i < 256; ++i) { SDL_Color *color = &colors[i]; // The palette is transported in AVFrame.data[1], is 1024 bytes // long (256 4-byte entries) and is formatted the same as in // AV_PIX_FMT_RGB32 described above (i.e., it is also // endian-specific). // #if SDL_BYTEORDER == SDL_BIG_ENDIAN color->a = data[i * 4]; color->r = data[i * 4 + 1]; color->g = data[i * 4 + 2]; color->b = data[i * 4 + 3]; #else color->a = data[i * 4 + 3]; color->r = data[i * 4 + 2]; color->g = data[i * 4 + 1]; color->b = data[i * 4]; #endif } SDL_Palette *palette = surface->format->palette; assert(palette); int ret = SDL_SetPaletteColors(palette, colors, 0, 256); if (ret) { LOGE("Could not set palette colors"); SDL_FreeSurface(surface); goto error; } } surface->userdata = frame; // frame owns the data return surface; error: av_frame_free(&frame); return NULL; } SDL_Surface * scrcpy_icon_load() { 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.25/app/src/icon.h000066400000000000000000000003441435104021100154040ustar00rootroot00000000000000#ifndef SC_ICON_H #define SC_ICON_H #include "common.h" #include #include #include SDL_Surface * scrcpy_icon_load(void); void scrcpy_icon_destroy(SDL_Surface *icon); #endif scrcpy-1.25/app/src/input_events.h000066400000000000000000000345671435104021100172150ustar00rootroot00000000000000#ifndef SC_INPUT_EVENTS_H #define SC_INPUT_EVENTS_H #include "common.h" #include #include #include #include #include "coords.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. * * This scrcpy input events API is designed to be consumed by input event * processors (sc_key_processor and sc_mouse_processor, see app/src/trait/). * * One major semantic difference between SDL input events and scrcpy input * events is their frame of reference (for mouse and touch events): SDL events * coordinates are expressed in SDL window coordinates (the visible UI), while * scrcpy events are expressed in device frame coordinates. * * In particular, the window may be visually scaled or rotated (with --rotation * or MOD+Left/Right), but this does not impact scrcpy input events (contrary * to SDL input events). This allows to abstract these display details from the * input event processors (and to make them independent from the "screen"). * * For many enums below, the values are purposely the same as the SDL * constants (though not all SDL values are represented), so that the * implementation to convert from the SDL version to the scrcpy version is * straightforward. * * In practice, there are 3 levels of input events: * 1. SDL input events (as received from SDL) * 2. scrcpy input events (this API) * 3. the key/mouse processors input events (Android API or HID events) * * An input event is first received (1), then (if accepted) converted to an * scrcpy input event (2), then submitted to the relevant key/mouse processor, * which (if accepted) is converted to an Android event (to be sent to the * server) or to an HID event (to be sent over USB/AOA directly). */ enum sc_mod { SC_MOD_LSHIFT = KMOD_LSHIFT, SC_MOD_RSHIFT = KMOD_RSHIFT, SC_MOD_LCTRL = KMOD_LCTRL, SC_MOD_RCTRL = KMOD_RCTRL, SC_MOD_LALT = KMOD_LALT, SC_MOD_RALT = KMOD_RALT, SC_MOD_LGUI = KMOD_LGUI, SC_MOD_RGUI = KMOD_RGUI, SC_MOD_NUM = KMOD_NUM, SC_MOD_CAPS = KMOD_CAPS, }; enum sc_action { SC_ACTION_DOWN, // key or button pressed SC_ACTION_UP, // key or button released }; enum sc_keycode { SC_KEYCODE_UNKNOWN = SDLK_UNKNOWN, SC_KEYCODE_RETURN = SDLK_RETURN, SC_KEYCODE_ESCAPE = SDLK_ESCAPE, SC_KEYCODE_BACKSPACE = SDLK_BACKSPACE, SC_KEYCODE_TAB = SDLK_TAB, SC_KEYCODE_SPACE = SDLK_SPACE, SC_KEYCODE_EXCLAIM = SDLK_EXCLAIM, SC_KEYCODE_QUOTEDBL = SDLK_QUOTEDBL, SC_KEYCODE_HASH = SDLK_HASH, SC_KEYCODE_PERCENT = SDLK_PERCENT, SC_KEYCODE_DOLLAR = SDLK_DOLLAR, SC_KEYCODE_AMPERSAND = SDLK_AMPERSAND, SC_KEYCODE_QUOTE = SDLK_QUOTE, SC_KEYCODE_LEFTPAREN = SDLK_LEFTPAREN, SC_KEYCODE_RIGHTPAREN = SDLK_RIGHTPAREN, SC_KEYCODE_ASTERISK = SDLK_ASTERISK, SC_KEYCODE_PLUS = SDLK_PLUS, SC_KEYCODE_COMMA = SDLK_COMMA, SC_KEYCODE_MINUS = SDLK_MINUS, SC_KEYCODE_PERIOD = SDLK_PERIOD, SC_KEYCODE_SLASH = SDLK_SLASH, SC_KEYCODE_0 = SDLK_0, SC_KEYCODE_1 = SDLK_1, SC_KEYCODE_2 = SDLK_2, SC_KEYCODE_3 = SDLK_3, SC_KEYCODE_4 = SDLK_4, SC_KEYCODE_5 = SDLK_5, SC_KEYCODE_6 = SDLK_6, SC_KEYCODE_7 = SDLK_7, SC_KEYCODE_8 = SDLK_8, SC_KEYCODE_9 = SDLK_9, SC_KEYCODE_COLON = SDLK_COLON, SC_KEYCODE_SEMICOLON = SDLK_SEMICOLON, SC_KEYCODE_LESS = SDLK_LESS, SC_KEYCODE_EQUALS = SDLK_EQUALS, SC_KEYCODE_GREATER = SDLK_GREATER, SC_KEYCODE_QUESTION = SDLK_QUESTION, SC_KEYCODE_AT = SDLK_AT, SC_KEYCODE_LEFTBRACKET = SDLK_LEFTBRACKET, SC_KEYCODE_BACKSLASH = SDLK_BACKSLASH, SC_KEYCODE_RIGHTBRACKET = SDLK_RIGHTBRACKET, SC_KEYCODE_CARET = SDLK_CARET, SC_KEYCODE_UNDERSCORE = SDLK_UNDERSCORE, SC_KEYCODE_BACKQUOTE = SDLK_BACKQUOTE, SC_KEYCODE_a = SDLK_a, SC_KEYCODE_b = SDLK_b, SC_KEYCODE_c = SDLK_c, SC_KEYCODE_d = SDLK_d, SC_KEYCODE_e = SDLK_e, SC_KEYCODE_f = SDLK_f, SC_KEYCODE_g = SDLK_g, SC_KEYCODE_h = SDLK_h, SC_KEYCODE_i = SDLK_i, SC_KEYCODE_j = SDLK_j, SC_KEYCODE_k = SDLK_k, SC_KEYCODE_l = SDLK_l, SC_KEYCODE_m = SDLK_m, SC_KEYCODE_n = SDLK_n, SC_KEYCODE_o = SDLK_o, SC_KEYCODE_p = SDLK_p, SC_KEYCODE_q = SDLK_q, SC_KEYCODE_r = SDLK_r, SC_KEYCODE_s = SDLK_s, SC_KEYCODE_t = SDLK_t, SC_KEYCODE_u = SDLK_u, SC_KEYCODE_v = SDLK_v, SC_KEYCODE_w = SDLK_w, SC_KEYCODE_x = SDLK_x, SC_KEYCODE_y = SDLK_y, SC_KEYCODE_z = SDLK_z, SC_KEYCODE_CAPSLOCK = SDLK_CAPSLOCK, SC_KEYCODE_F1 = SDLK_F1, SC_KEYCODE_F2 = SDLK_F2, SC_KEYCODE_F3 = SDLK_F3, SC_KEYCODE_F4 = SDLK_F4, SC_KEYCODE_F5 = SDLK_F5, SC_KEYCODE_F6 = SDLK_F6, SC_KEYCODE_F7 = SDLK_F7, SC_KEYCODE_F8 = SDLK_F8, SC_KEYCODE_F9 = SDLK_F9, SC_KEYCODE_F10 = SDLK_F10, SC_KEYCODE_F11 = SDLK_F11, SC_KEYCODE_F12 = SDLK_F12, SC_KEYCODE_PRINTSCREEN = SDLK_PRINTSCREEN, SC_KEYCODE_SCROLLLOCK = SDLK_SCROLLLOCK, SC_KEYCODE_PAUSE = SDLK_PAUSE, SC_KEYCODE_INSERT = SDLK_INSERT, SC_KEYCODE_HOME = SDLK_HOME, SC_KEYCODE_PAGEUP = SDLK_PAGEUP, SC_KEYCODE_DELETE = SDLK_DELETE, SC_KEYCODE_END = SDLK_END, SC_KEYCODE_PAGEDOWN = SDLK_PAGEDOWN, SC_KEYCODE_RIGHT = SDLK_RIGHT, SC_KEYCODE_LEFT = SDLK_LEFT, SC_KEYCODE_DOWN = SDLK_DOWN, SC_KEYCODE_UP = SDLK_UP, SC_KEYCODE_KP_DIVIDE = SDLK_KP_DIVIDE, SC_KEYCODE_KP_MULTIPLY = SDLK_KP_MULTIPLY, SC_KEYCODE_KP_MINUS = SDLK_KP_MINUS, SC_KEYCODE_KP_PLUS = SDLK_KP_PLUS, SC_KEYCODE_KP_ENTER = SDLK_KP_ENTER, SC_KEYCODE_KP_1 = SDLK_KP_1, SC_KEYCODE_KP_2 = SDLK_KP_2, SC_KEYCODE_KP_3 = SDLK_KP_3, SC_KEYCODE_KP_4 = SDLK_KP_4, SC_KEYCODE_KP_5 = SDLK_KP_5, SC_KEYCODE_KP_6 = SDLK_KP_6, SC_KEYCODE_KP_7 = SDLK_KP_7, SC_KEYCODE_KP_8 = SDLK_KP_8, SC_KEYCODE_KP_9 = SDLK_KP_9, SC_KEYCODE_KP_0 = SDLK_KP_0, SC_KEYCODE_KP_PERIOD = SDLK_KP_PERIOD, SC_KEYCODE_KP_EQUALS = SDLK_KP_EQUALS, SC_KEYCODE_KP_LEFTPAREN = SDLK_KP_LEFTPAREN, SC_KEYCODE_KP_RIGHTPAREN = SDLK_KP_RIGHTPAREN, SC_KEYCODE_LCTRL = SDLK_LCTRL, SC_KEYCODE_LSHIFT = SDLK_LSHIFT, SC_KEYCODE_LALT = SDLK_LALT, SC_KEYCODE_LGUI = SDLK_LGUI, SC_KEYCODE_RCTRL = SDLK_RCTRL, SC_KEYCODE_RSHIFT = SDLK_RSHIFT, SC_KEYCODE_RALT = SDLK_RALT, SC_KEYCODE_RGUI = SDLK_RGUI, }; enum sc_scancode { SC_SCANCODE_UNKNOWN = SDL_SCANCODE_UNKNOWN, SC_SCANCODE_A = SDL_SCANCODE_A, SC_SCANCODE_B = SDL_SCANCODE_B, SC_SCANCODE_C = SDL_SCANCODE_C, SC_SCANCODE_D = SDL_SCANCODE_D, SC_SCANCODE_E = SDL_SCANCODE_E, SC_SCANCODE_F = SDL_SCANCODE_F, SC_SCANCODE_G = SDL_SCANCODE_G, SC_SCANCODE_H = SDL_SCANCODE_H, SC_SCANCODE_I = SDL_SCANCODE_I, SC_SCANCODE_J = SDL_SCANCODE_J, SC_SCANCODE_K = SDL_SCANCODE_K, SC_SCANCODE_L = SDL_SCANCODE_L, SC_SCANCODE_M = SDL_SCANCODE_M, SC_SCANCODE_N = SDL_SCANCODE_N, SC_SCANCODE_O = SDL_SCANCODE_O, SC_SCANCODE_P = SDL_SCANCODE_P, SC_SCANCODE_Q = SDL_SCANCODE_Q, SC_SCANCODE_R = SDL_SCANCODE_R, SC_SCANCODE_S = SDL_SCANCODE_S, SC_SCANCODE_T = SDL_SCANCODE_T, SC_SCANCODE_U = SDL_SCANCODE_U, SC_SCANCODE_V = SDL_SCANCODE_V, SC_SCANCODE_W = SDL_SCANCODE_W, SC_SCANCODE_X = SDL_SCANCODE_X, SC_SCANCODE_Y = SDL_SCANCODE_Y, SC_SCANCODE_Z = SDL_SCANCODE_Z, SC_SCANCODE_1 = SDL_SCANCODE_1, SC_SCANCODE_2 = SDL_SCANCODE_2, SC_SCANCODE_3 = SDL_SCANCODE_3, SC_SCANCODE_4 = SDL_SCANCODE_4, SC_SCANCODE_5 = SDL_SCANCODE_5, SC_SCANCODE_6 = SDL_SCANCODE_6, SC_SCANCODE_7 = SDL_SCANCODE_7, SC_SCANCODE_8 = SDL_SCANCODE_8, SC_SCANCODE_9 = SDL_SCANCODE_9, SC_SCANCODE_0 = SDL_SCANCODE_0, SC_SCANCODE_RETURN = SDL_SCANCODE_RETURN, SC_SCANCODE_ESCAPE = SDL_SCANCODE_ESCAPE, SC_SCANCODE_BACKSPACE = SDL_SCANCODE_BACKSPACE, SC_SCANCODE_TAB = SDL_SCANCODE_TAB, SC_SCANCODE_SPACE = SDL_SCANCODE_SPACE, SC_SCANCODE_MINUS = SDL_SCANCODE_MINUS, SC_SCANCODE_EQUALS = SDL_SCANCODE_EQUALS, SC_SCANCODE_LEFTBRACKET = SDL_SCANCODE_LEFTBRACKET, SC_SCANCODE_RIGHTBRACKET = SDL_SCANCODE_RIGHTBRACKET, SC_SCANCODE_BACKSLASH = SDL_SCANCODE_BACKSLASH, SC_SCANCODE_NONUSHASH = SDL_SCANCODE_NONUSHASH, SC_SCANCODE_SEMICOLON = SDL_SCANCODE_SEMICOLON, SC_SCANCODE_APOSTROPHE = SDL_SCANCODE_APOSTROPHE, SC_SCANCODE_GRAVE = SDL_SCANCODE_GRAVE, SC_SCANCODE_COMMA = SDL_SCANCODE_COMMA, SC_SCANCODE_PERIOD = SDL_SCANCODE_PERIOD, SC_SCANCODE_SLASH = SDL_SCANCODE_SLASH, SC_SCANCODE_CAPSLOCK = SDL_SCANCODE_CAPSLOCK, SC_SCANCODE_F1 = SDL_SCANCODE_F1, SC_SCANCODE_F2 = SDL_SCANCODE_F2, SC_SCANCODE_F3 = SDL_SCANCODE_F3, SC_SCANCODE_F4 = SDL_SCANCODE_F4, SC_SCANCODE_F5 = SDL_SCANCODE_F5, SC_SCANCODE_F6 = SDL_SCANCODE_F6, SC_SCANCODE_F7 = SDL_SCANCODE_F7, SC_SCANCODE_F8 = SDL_SCANCODE_F8, SC_SCANCODE_F9 = SDL_SCANCODE_F9, SC_SCANCODE_F10 = SDL_SCANCODE_F10, SC_SCANCODE_F11 = SDL_SCANCODE_F11, SC_SCANCODE_F12 = SDL_SCANCODE_F12, SC_SCANCODE_PRINTSCREEN = SDL_SCANCODE_PRINTSCREEN, SC_SCANCODE_SCROLLLOCK = SDL_SCANCODE_SCROLLLOCK, SC_SCANCODE_PAUSE = SDL_SCANCODE_PAUSE, SC_SCANCODE_INSERT = SDL_SCANCODE_INSERT, SC_SCANCODE_HOME = SDL_SCANCODE_HOME, SC_SCANCODE_PAGEUP = SDL_SCANCODE_PAGEUP, SC_SCANCODE_DELETE = SDL_SCANCODE_DELETE, SC_SCANCODE_END = SDL_SCANCODE_END, SC_SCANCODE_PAGEDOWN = SDL_SCANCODE_PAGEDOWN, SC_SCANCODE_RIGHT = SDL_SCANCODE_RIGHT, SC_SCANCODE_LEFT = SDL_SCANCODE_LEFT, SC_SCANCODE_DOWN = SDL_SCANCODE_DOWN, SC_SCANCODE_UP = SDL_SCANCODE_UP, SC_SCANCODE_NUMLOCK = SDL_SCANCODE_NUMLOCKCLEAR, SC_SCANCODE_KP_DIVIDE = SDL_SCANCODE_KP_DIVIDE, SC_SCANCODE_KP_MULTIPLY = SDL_SCANCODE_KP_MULTIPLY, SC_SCANCODE_KP_MINUS = SDL_SCANCODE_KP_MINUS, SC_SCANCODE_KP_PLUS = SDL_SCANCODE_KP_PLUS, SC_SCANCODE_KP_ENTER = SDL_SCANCODE_KP_ENTER, SC_SCANCODE_KP_1 = SDL_SCANCODE_KP_1, SC_SCANCODE_KP_2 = SDL_SCANCODE_KP_2, SC_SCANCODE_KP_3 = SDL_SCANCODE_KP_3, SC_SCANCODE_KP_4 = SDL_SCANCODE_KP_4, SC_SCANCODE_KP_5 = SDL_SCANCODE_KP_5, SC_SCANCODE_KP_6 = SDL_SCANCODE_KP_6, SC_SCANCODE_KP_7 = SDL_SCANCODE_KP_7, SC_SCANCODE_KP_8 = SDL_SCANCODE_KP_8, SC_SCANCODE_KP_9 = SDL_SCANCODE_KP_9, SC_SCANCODE_KP_0 = SDL_SCANCODE_KP_0, SC_SCANCODE_KP_PERIOD = SDL_SCANCODE_KP_PERIOD, SC_SCANCODE_LCTRL = SDL_SCANCODE_LCTRL, SC_SCANCODE_LSHIFT = SDL_SCANCODE_LSHIFT, SC_SCANCODE_LALT = SDL_SCANCODE_LALT, SC_SCANCODE_LGUI = SDL_SCANCODE_LGUI, SC_SCANCODE_RCTRL = SDL_SCANCODE_RCTRL, SC_SCANCODE_RSHIFT = SDL_SCANCODE_RSHIFT, SC_SCANCODE_RALT = SDL_SCANCODE_RALT, SC_SCANCODE_RGUI = SDL_SCANCODE_RGUI, }; // On purpose, only use the "mask" values (1, 2, 4, 8, 16) for a single button, // to avoid unnecessary conversions (and confusion). enum sc_mouse_button { SC_MOUSE_BUTTON_UNKNOWN = 0, SC_MOUSE_BUTTON_LEFT = SDL_BUTTON(SDL_BUTTON_LEFT), SC_MOUSE_BUTTON_RIGHT = SDL_BUTTON(SDL_BUTTON_RIGHT), SC_MOUSE_BUTTON_MIDDLE = SDL_BUTTON(SDL_BUTTON_MIDDLE), SC_MOUSE_BUTTON_X1 = SDL_BUTTON(SDL_BUTTON_X1), SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2), }; static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod), "SDL_Keymod must be convertible to sc_mod"); static_assert(sizeof(enum sc_keycode) >= sizeof(SDL_Keycode), "SDL_Keycode must be convertible to sc_keycode"); static_assert(sizeof(enum sc_scancode) >= sizeof(SDL_Scancode), "SDL_Scancode must be convertible to sc_scancode"); enum sc_touch_action { SC_TOUCH_ACTION_MOVE, SC_TOUCH_ACTION_DOWN, SC_TOUCH_ACTION_UP, }; struct sc_key_event { enum sc_action action; enum sc_keycode keycode; enum sc_scancode scancode; uint16_t mods_state; // bitwise-OR of sc_mod values bool repeat; }; struct sc_text_event { const char *text; // not owned }; struct sc_mouse_click_event { struct sc_position position; enum sc_action action; enum sc_mouse_button button; uint64_t pointer_id; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; struct sc_mouse_scroll_event { struct sc_position position; float hscroll; float vscroll; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; struct sc_mouse_motion_event { struct sc_position position; uint64_t pointer_id; int32_t xrel; int32_t yrel; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; struct sc_touch_event { struct sc_position position; enum sc_touch_action action; uint64_t pointer_id; float pressure; }; static inline uint16_t sc_mods_state_from_sdl(uint16_t mods_state) { return mods_state; } static inline enum sc_keycode sc_keycode_from_sdl(SDL_Keycode keycode) { return (enum sc_keycode) keycode; } static inline enum sc_scancode sc_scancode_from_sdl(SDL_Scancode scancode) { return (enum sc_scancode) scancode; } static inline enum sc_action sc_action_from_sdl_keyboard_type(uint32_t type) { assert(type == SDL_KEYDOWN || type == SDL_KEYUP); if (type == SDL_KEYDOWN) { return SC_ACTION_DOWN; } return SC_ACTION_UP; } static inline enum sc_action sc_action_from_sdl_mousebutton_type(uint32_t type) { assert(type == SDL_MOUSEBUTTONDOWN || type == SDL_MOUSEBUTTONUP); if (type == SDL_MOUSEBUTTONDOWN) { return SC_ACTION_DOWN; } return SC_ACTION_UP; } static inline enum sc_touch_action sc_touch_action_from_sdl(uint32_t type) { assert(type == SDL_FINGERMOTION || type == SDL_FINGERDOWN || type == SDL_FINGERUP); if (type == SDL_FINGERMOTION) { return SC_TOUCH_ACTION_MOVE; } if (type == SDL_FINGERDOWN) { return SC_TOUCH_ACTION_DOWN; } return SC_TOUCH_ACTION_UP; } static inline enum sc_mouse_button sc_mouse_button_from_sdl(uint8_t button) { if (button >= SDL_BUTTON_LEFT && button <= SDL_BUTTON_X2) { // SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index) return SDL_BUTTON(button); } return SC_MOUSE_BUTTON_UNKNOWN; } static inline uint8_t sc_mouse_buttons_state_from_sdl(uint32_t buttons_state, bool forward_all_clicks) { assert(buttons_state < 0x100); // fits in uint8_t uint8_t mask = SC_MOUSE_BUTTON_LEFT; if (forward_all_clicks) { mask |= SC_MOUSE_BUTTON_RIGHT | SC_MOUSE_BUTTON_MIDDLE | SC_MOUSE_BUTTON_X1 | SC_MOUSE_BUTTON_X2; } return buttons_state & mask; } #endif scrcpy-1.25/app/src/input_manager.c000066400000000000000000000664231435104021100173120ustar00rootroot00000000000000#include "input_manager.h" #include #include #include "input_events.h" #include "screen.h" #include "util/log.h" #define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) static inline uint16_t to_sdl_mod(unsigned shortcut_mod) { uint16_t sdl_mod = 0; if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { sdl_mod |= KMOD_LCTRL; } if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) { sdl_mod |= KMOD_RCTRL; } if (shortcut_mod & SC_SHORTCUT_MOD_LALT) { sdl_mod |= KMOD_LALT; } if (shortcut_mod & SC_SHORTCUT_MOD_RALT) { sdl_mod |= KMOD_RALT; } if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) { sdl_mod |= KMOD_LGUI; } if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) { sdl_mod |= KMOD_RGUI; } return sdl_mod; } static bool is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { // keep only the relevant modifier keys sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; 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 sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { assert(!params->controller || (params->kp && params->kp->ops)); assert(!params->controller || (params->mp && params->mp->ops)); im->controller = params->controller; im->fp = params->fp; im->screen = params->screen; im->kp = params->kp; im->mp = params->mp; im->forward_all_clicks = params->forward_all_clicks; im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods; assert(shortcut_mods->count); assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS); for (unsigned i = 0; i < shortcut_mods->count; ++i) { 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 sc_controller *controller, enum android_keycode keycode, enum sc_action action, const char *name) { // send DOWN event struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.inject_keycode.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; msg.inject_keycode.keycode = keycode; msg.inject_keycode.metastate = 0; msg.inject_keycode.repeat = 0; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'inject %s'", name); } } static inline void action_home(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_HOME, action, "HOME"); } static inline void action_back(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_BACK, action, "BACK"); } static inline void action_app_switch(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); } static inline void action_power(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_POWER, action, "POWER"); } static inline void action_volume_up(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); } static inline void action_volume_down(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); } static inline void action_menu(struct sc_controller *controller, enum sc_action action) { send_keycode(controller, AKEYCODE_MENU, action, "MENU"); } // turn the screen on if it was off, press BACK otherwise // If the screen is off, it is turned on only on ACTION_DOWN static void press_back_or_turn_screen_on(struct sc_controller *controller, enum sc_action action) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.back_or_screen_on.action = action == SC_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'press back or turn screen on'"); } } static void expand_notification_panel(struct sc_controller *controller) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand notification panel'"); } } static void expand_settings_panel(struct sc_controller *controller) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'expand settings panel'"); } } static void collapse_panels(struct sc_controller *controller) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'collapse notification panel'"); } } static bool get_device_clipboard(struct sc_controller *controller, enum sc_copy_key copy_key) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.get_clipboard.copy_key = copy_key; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'get device clipboard'"); return false; } return true; } static bool set_device_clipboard(struct sc_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 sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.set_clipboard.sequence = sequence; msg.set_clipboard.text = text_dup; msg.set_clipboard.paste = paste; if (!sc_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 sc_controller *controller, enum sc_screen_power_mode mode) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = mode; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } static void switch_fps_counter_state(struct sc_fps_counter *fps_counter) { // the started state can only be written from the current thread, so there // is no ToCToU issue if (sc_fps_counter_is_started(fps_counter)) { sc_fps_counter_stop(fps_counter); } else { sc_fps_counter_start(fps_counter); // Any error is already logged } } static void clipboard_paste(struct sc_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 sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = text_dup; if (!sc_controller_push_msg(controller, &msg)) { free(text_dup); LOGW("Could not request 'paste clipboard'"); } } static void rotate_device(struct sc_controller *controller) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; if (!sc_controller_push_msg(controller, &msg)) { LOGW("Could not request device rotation"); } } static void rotate_client_left(struct sc_screen *screen) { unsigned new_rotation = (screen->rotation + 1) % 4; sc_screen_set_rotation(screen, new_rotation); } static void rotate_client_right(struct sc_screen *screen) { unsigned new_rotation = (screen->rotation + 3) % 4; sc_screen_set_rotation(screen, new_rotation); } static void sc_input_manager_process_text_input(struct sc_input_manager *im, const SDL_TextInputEvent *event) { if (!im->kp->ops->process_text) { // The key processor does not support text input return; } if (is_shortcut_mod(im, SDL_GetModState())) { // A shortcut must never generate text events return; } struct sc_text_event evt = { .text = event->text, }; im->kp->ops->process_text(im->kp, &evt); } static bool simulate_virtual_finger(struct sc_input_manager *im, enum android_motionevent_action action, struct sc_point point) { bool up = action == AMOTION_EVENT_ACTION_UP; struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT; msg.inject_touch_event.action = action; msg.inject_touch_event.position.screen_size = im->screen->frame_size; msg.inject_touch_event.position.point = point; msg.inject_touch_event.pointer_id = im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE : POINTER_ID_VIRTUAL_FINGER; msg.inject_touch_event.pressure = up ? 0.0f : 1.0f; msg.inject_touch_event.buttons = 0; if (!sc_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 sc_input_manager_process_key(struct sc_input_manager *im, const SDL_KeyboardEvent *event) { // controller is NULL if --no-control is requested struct sc_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) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; switch (keycode) { case SDLK_h: if (controller && !shift && !repeat) { action_home(controller, action); } return; case SDLK_b: // fall-through case SDLK_BACKSPACE: if (controller && !shift && !repeat) { action_back(controller, action); } return; case SDLK_s: if (controller && !shift && !repeat) { action_app_switch(controller, action); } return; case SDLK_m: if (controller && !shift && !repeat) { action_menu(controller, action); } return; case SDLK_p: if (controller && !shift && !repeat) { action_power(controller, action); } return; case SDLK_o: if (controller && !repeat && down) { enum sc_screen_power_mode mode = shift ? SC_SCREEN_POWER_MODE_NORMAL : SC_SCREEN_POWER_MODE_OFF; set_screen_power_mode(controller, mode); } return; case SDLK_DOWN: if (controller && !shift) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: if (controller && !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 (controller && !shift && !repeat && down) { get_device_clipboard(controller, SC_COPY_KEY_COPY); } return; case SDLK_x: if (controller && !shift && !repeat && down) { get_device_clipboard(controller, SC_COPY_KEY_CUT); } return; case SDLK_v: if (controller && !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) { sc_screen_switch_fullscreen(im->screen); } return; case SDLK_w: if (!shift && !repeat && down) { sc_screen_resize_to_fit(im->screen); } return; case SDLK_g: if (!shift && !repeat && down) { sc_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 (controller && !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 (controller && !shift && !repeat && down) { rotate_device(controller); } return; } return; } if (!controller) { 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; } } struct sc_key_event evt = { .action = sc_action_from_sdl_keyboard_type(event->type), .keycode = sc_keycode_from_sdl(event->keysym.sym), .scancode = sc_scancode_from_sdl(event->keysym.scancode), .repeat = event->repeat, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), }; assert(im->kp->ops->process_key); im->kp->ops->process_key(im->kp, &evt, ack_to_wait); } static void sc_input_manager_process_mouse_motion(struct sc_input_manager *im, const SDL_MouseMotionEvent *event) { if (event->which == SDL_TOUCH_MOUSEID) { // simulated from touch events, so it's a duplicate return; } struct sc_mouse_motion_event evt = { .position = { .screen_size = im->screen->frame_size, .point = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y), }, .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE : POINTER_ID_GENERIC_FINGER, .xrel = event->xrel, .yrel = event->yrel, .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, im->forward_all_clicks), }; assert(im->mp->ops->process_mouse_motion); im->mp->ops->process_mouse_motion(im->mp, &evt); // vfinger must never be used in relative mode assert(!im->mp->relative_mode || !im->vfinger_down); if (im->vfinger_down) { assert(!im->mp->relative_mode); // assert one more time struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); } } static void sc_input_manager_process_touch(struct sc_input_manager *im, const SDL_TouchFingerEvent *event) { if (!im->mp->ops->process_touch) { // The mouse processor does not support touch events return; } int dw; int dh; SDL_GL_GetDrawableSize(im->screen->window, &dw, &dh); // SDL touch event coordinates are normalized in the range [0; 1] int32_t x = event->x * dw; int32_t y = event->y * dh; struct sc_touch_event evt = { .position = { .screen_size = im->screen->frame_size, .point = sc_screen_convert_drawable_to_frame_coords(im->screen, x, y), }, .action = sc_touch_action_from_sdl(event->type), .pointer_id = event->fingerId, .pressure = event->pressure, }; im->mp->ops->process_touch(im->mp, &evt); } static void sc_input_manager_process_mouse_button(struct sc_input_manager *im, const SDL_MouseButtonEvent *event) { struct sc_controller *controller = im->controller; 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) { if (controller) { enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; if (event->button == SDL_BUTTON_X1) { action_app_switch(controller, action); return; } if (event->button == SDL_BUTTON_X2 && down) { if (event->clicks < 2) { expand_notification_panel(controller); } else { expand_settings_panel(controller); } return; } if (event->button == SDL_BUTTON_RIGHT) { press_back_or_turn_screen_on(controller, action); return; } if (event->button == SDL_BUTTON_MIDDLE) { action_home(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; sc_screen_hidpi_scale_coords(im->screen, &x, &y); SDL_Rect *r = &im->screen->rect; bool outside = x < r->x || x >= r->x + r->w || y < r->y || y >= r->y + r->h; if (outside) { if (down) { sc_screen_resize_to_fit(im->screen); } return; } } // otherwise, send the click event to the device } if (!controller) { return; } uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); struct sc_mouse_click_event evt = { .position = { .screen_size = im->screen->frame_size, .point = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y), }, .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE : POINTER_ID_GENERIC_FINGER, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, im->forward_all_clicks), }; assert(im->mp->ops->process_mouse_click); im->mp->ops->process_mouse_click(im->mp, &evt); if (im->mp->relative_mode) { assert(!im->vfinger_down); // vfinger must not be used in relative mode // No pinch-to-zoom simulation return; } // Pinch-to-zoom simulation. // // 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 (event->button == SDL_BUTTON_LEFT && ((down && !im->vfinger_down && CTRL_PRESSED) || (!down && im->vfinger_down))) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); 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 sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, const SDL_MouseWheelEvent *event) { if (!im->mp->ops->process_mouse_scroll) { // The mouse processor does not support scroll events return; } // mouse_x and mouse_y are expressed in pixels relative to the window int mouse_x; int mouse_y; uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); struct sc_mouse_scroll_event evt = { .position = { .screen_size = im->screen->frame_size, .point = sc_screen_convert_window_to_frame_coords(im->screen, mouse_x, mouse_y), }, #if SDL_VERSION_ATLEAST(2, 0, 18) .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), #else .hscroll = CLAMP(event->x, -1, 1), .vscroll = CLAMP(event->y, -1, 1), #endif .buttons_state = sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks), }; im->mp->ops->process_mouse_scroll(im->mp, &evt); } static bool is_apk(const char *file) { const char *ext = strrchr(file, '.'); return ext && !strcmp(ext, ".apk"); } static void sc_input_manager_process_file(struct sc_input_manager *im, const SDL_DropEvent *event) { char *file = strdup(event->file); SDL_free(event->file); if (!file) { LOG_OOM(); return; } enum sc_file_pusher_action action; if (is_apk(file)) { action = SC_FILE_PUSHER_ACTION_INSTALL_APK; } else { action = SC_FILE_PUSHER_ACTION_PUSH_FILE; } bool ok = sc_file_pusher_request(im->fp, action, file); if (!ok) { free(file); } } void sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) { bool control = im->controller; switch (event->type) { case SDL_TEXTINPUT: if (!control) { break; } sc_input_manager_process_text_input(im, &event->text); break; case SDL_KEYDOWN: case SDL_KEYUP: // some key events do not interact with the device, so process the // event even if control is disabled sc_input_manager_process_key(im, &event->key); break; case SDL_MOUSEMOTION: if (!control) { break; } sc_input_manager_process_mouse_motion(im, &event->motion); break; case SDL_MOUSEWHEEL: if (!control) { break; } sc_input_manager_process_mouse_wheel(im, &event->wheel); break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: // some mouse events do not interact with the device, so process // the event even if control is disabled sc_input_manager_process_mouse_button(im, &event->button); break; case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: if (!control) { break; } sc_input_manager_process_touch(im, &event->tfinger); break; case SDL_DROPFILE: { if (!control) { break; } sc_input_manager_process_file(im, &event->drop); } } } scrcpy-1.25/app/src/input_manager.h000066400000000000000000000030641435104021100173070ustar00rootroot00000000000000#ifndef SC_INPUTMANAGER_H #define SC_INPUTMANAGER_H #include "common.h" #include #include #include "controller.h" #include "file_pusher.h" #include "fps_counter.h" #include "options.h" #include "trait/key_processor.h" #include "trait/mouse_processor.h" struct sc_input_manager { struct sc_controller *controller; struct sc_file_pusher *fp; struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; 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 }; struct sc_input_manager_params { struct sc_controller *controller; struct sc_file_pusher *fp; struct sc_screen *screen; struct sc_key_processor *kp; struct sc_mouse_processor *mp; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; const struct sc_shortcut_mods *shortcut_mods; }; void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params); void sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event); #endif scrcpy-1.25/app/src/keyboard_inject.c000066400000000000000000000276511435104021100176150ustar00rootroot00000000000000#include "keyboard_inject.h" #include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" #include "util/intmap.h" #include "util/log.h" /** Downcast key processor to sc_keyboard_inject */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) static enum android_keyevent_action convert_keycode_action(enum sc_action action) { if (action == SC_ACTION_DOWN) { return AKEY_EVENT_ACTION_DOWN; } assert(action == SC_ACTION_UP); return AKEY_EVENT_ACTION_UP; } static bool convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, enum sc_key_inject_mode key_inject_mode) { // Navigation keys and ENTER. // Used in all modes. static const struct sc_intmap_entry special_keys[] = { {SC_KEYCODE_RETURN, AKEYCODE_ENTER}, {SC_KEYCODE_KP_ENTER, AKEYCODE_NUMPAD_ENTER}, {SC_KEYCODE_ESCAPE, AKEYCODE_ESCAPE}, {SC_KEYCODE_BACKSPACE, AKEYCODE_DEL}, {SC_KEYCODE_TAB, AKEYCODE_TAB}, {SC_KEYCODE_PAGEUP, AKEYCODE_PAGE_UP}, {SC_KEYCODE_DELETE, AKEYCODE_FORWARD_DEL}, {SC_KEYCODE_HOME, AKEYCODE_MOVE_HOME}, {SC_KEYCODE_END, AKEYCODE_MOVE_END}, {SC_KEYCODE_PAGEDOWN, AKEYCODE_PAGE_DOWN}, {SC_KEYCODE_RIGHT, AKEYCODE_DPAD_RIGHT}, {SC_KEYCODE_LEFT, AKEYCODE_DPAD_LEFT}, {SC_KEYCODE_DOWN, AKEYCODE_DPAD_DOWN}, {SC_KEYCODE_UP, AKEYCODE_DPAD_UP}, {SC_KEYCODE_LCTRL, AKEYCODE_CTRL_LEFT}, {SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT}, {SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT}, {SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT}, }; // Numpad navigation keys. // Used in all modes, when NumLock and Shift are disabled. static const struct sc_intmap_entry kp_nav_keys[] = { {SC_KEYCODE_KP_0, AKEYCODE_INSERT}, {SC_KEYCODE_KP_1, AKEYCODE_MOVE_END}, {SC_KEYCODE_KP_2, AKEYCODE_DPAD_DOWN}, {SC_KEYCODE_KP_3, AKEYCODE_PAGE_DOWN}, {SC_KEYCODE_KP_4, AKEYCODE_DPAD_LEFT}, {SC_KEYCODE_KP_6, AKEYCODE_DPAD_RIGHT}, {SC_KEYCODE_KP_7, AKEYCODE_MOVE_HOME}, {SC_KEYCODE_KP_8, AKEYCODE_DPAD_UP}, {SC_KEYCODE_KP_9, AKEYCODE_PAGE_UP}, {SC_KEYCODE_KP_PERIOD, AKEYCODE_FORWARD_DEL}, }; // Letters and space. // Used in non-text mode. static const struct sc_intmap_entry alphaspace_keys[] = { {SC_KEYCODE_a, AKEYCODE_A}, {SC_KEYCODE_b, AKEYCODE_B}, {SC_KEYCODE_c, AKEYCODE_C}, {SC_KEYCODE_d, AKEYCODE_D}, {SC_KEYCODE_e, AKEYCODE_E}, {SC_KEYCODE_f, AKEYCODE_F}, {SC_KEYCODE_g, AKEYCODE_G}, {SC_KEYCODE_h, AKEYCODE_H}, {SC_KEYCODE_i, AKEYCODE_I}, {SC_KEYCODE_j, AKEYCODE_J}, {SC_KEYCODE_k, AKEYCODE_K}, {SC_KEYCODE_l, AKEYCODE_L}, {SC_KEYCODE_m, AKEYCODE_M}, {SC_KEYCODE_n, AKEYCODE_N}, {SC_KEYCODE_o, AKEYCODE_O}, {SC_KEYCODE_p, AKEYCODE_P}, {SC_KEYCODE_q, AKEYCODE_Q}, {SC_KEYCODE_r, AKEYCODE_R}, {SC_KEYCODE_s, AKEYCODE_S}, {SC_KEYCODE_t, AKEYCODE_T}, {SC_KEYCODE_u, AKEYCODE_U}, {SC_KEYCODE_v, AKEYCODE_V}, {SC_KEYCODE_w, AKEYCODE_W}, {SC_KEYCODE_x, AKEYCODE_X}, {SC_KEYCODE_y, AKEYCODE_Y}, {SC_KEYCODE_z, AKEYCODE_Z}, {SC_KEYCODE_SPACE, AKEYCODE_SPACE}, }; // Numbers and punctuation keys. // Used in raw mode only. static const struct sc_intmap_entry numbers_punct_keys[] = { {SC_KEYCODE_HASH, AKEYCODE_POUND}, {SC_KEYCODE_PERCENT, AKEYCODE_PERIOD}, {SC_KEYCODE_QUOTE, AKEYCODE_APOSTROPHE}, {SC_KEYCODE_ASTERISK, AKEYCODE_STAR}, {SC_KEYCODE_PLUS, AKEYCODE_PLUS}, {SC_KEYCODE_COMMA, AKEYCODE_COMMA}, {SC_KEYCODE_MINUS, AKEYCODE_MINUS}, {SC_KEYCODE_PERIOD, AKEYCODE_PERIOD}, {SC_KEYCODE_SLASH, AKEYCODE_SLASH}, {SC_KEYCODE_0, AKEYCODE_0}, {SC_KEYCODE_1, AKEYCODE_1}, {SC_KEYCODE_2, AKEYCODE_2}, {SC_KEYCODE_3, AKEYCODE_3}, {SC_KEYCODE_4, AKEYCODE_4}, {SC_KEYCODE_5, AKEYCODE_5}, {SC_KEYCODE_6, AKEYCODE_6}, {SC_KEYCODE_7, AKEYCODE_7}, {SC_KEYCODE_8, AKEYCODE_8}, {SC_KEYCODE_9, AKEYCODE_9}, {SC_KEYCODE_SEMICOLON, AKEYCODE_SEMICOLON}, {SC_KEYCODE_EQUALS, AKEYCODE_EQUALS}, {SC_KEYCODE_AT, AKEYCODE_AT}, {SC_KEYCODE_LEFTBRACKET, AKEYCODE_LEFT_BRACKET}, {SC_KEYCODE_BACKSLASH, AKEYCODE_BACKSLASH}, {SC_KEYCODE_RIGHTBRACKET, AKEYCODE_RIGHT_BRACKET}, {SC_KEYCODE_BACKQUOTE, AKEYCODE_GRAVE}, {SC_KEYCODE_KP_1, AKEYCODE_NUMPAD_1}, {SC_KEYCODE_KP_2, AKEYCODE_NUMPAD_2}, {SC_KEYCODE_KP_3, AKEYCODE_NUMPAD_3}, {SC_KEYCODE_KP_4, AKEYCODE_NUMPAD_4}, {SC_KEYCODE_KP_5, AKEYCODE_NUMPAD_5}, {SC_KEYCODE_KP_6, AKEYCODE_NUMPAD_6}, {SC_KEYCODE_KP_7, AKEYCODE_NUMPAD_7}, {SC_KEYCODE_KP_8, AKEYCODE_NUMPAD_8}, {SC_KEYCODE_KP_9, AKEYCODE_NUMPAD_9}, {SC_KEYCODE_KP_0, AKEYCODE_NUMPAD_0}, {SC_KEYCODE_KP_DIVIDE, AKEYCODE_NUMPAD_DIVIDE}, {SC_KEYCODE_KP_MULTIPLY, AKEYCODE_NUMPAD_MULTIPLY}, {SC_KEYCODE_KP_MINUS, AKEYCODE_NUMPAD_SUBTRACT}, {SC_KEYCODE_KP_PLUS, AKEYCODE_NUMPAD_ADD}, {SC_KEYCODE_KP_PERIOD, AKEYCODE_NUMPAD_DOT}, {SC_KEYCODE_KP_EQUALS, AKEYCODE_NUMPAD_EQUALS}, {SC_KEYCODE_KP_LEFTPAREN, AKEYCODE_NUMPAD_LEFT_PAREN}, {SC_KEYCODE_KP_RIGHTPAREN, AKEYCODE_NUMPAD_RIGHT_PAREN}, }; const struct sc_intmap_entry *entry = SC_INTMAP_FIND_ENTRY(special_keys, from); if (entry) { *to = entry->value; return true; } if (!(mod & (SC_MOD_NUM | SC_MOD_LSHIFT | SC_MOD_RSHIFT))) { // Handle Numpad events when Num Lock is disabled // If SHIFT is pressed, a text event will be sent instead entry = SC_INTMAP_FIND_ENTRY(kp_nav_keys, from); if (entry) { *to = entry->value; return true; } } if (key_inject_mode == SC_KEY_INJECT_MODE_TEXT && !(mod & (SC_MOD_LCTRL | SC_MOD_RCTRL))) { // do not forward alpha and space key events (unless Ctrl is pressed) return false; } if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) { return false; } // if ALT and META are not pressed, also handle letters and space entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); if (entry) { *to = entry->value; return true; } if (key_inject_mode == SC_KEY_INJECT_MODE_RAW) { entry = SC_INTMAP_FIND_ENTRY(numbers_punct_keys, from); if (entry) { *to = entry->value; return true; } } return false; } static enum android_metastate autocomplete_metastate(enum android_metastate metastate) { // fill dependent flags if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) { metastate |= AMETA_SHIFT_ON; } if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) { metastate |= AMETA_CTRL_ON; } if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) { metastate |= AMETA_ALT_ON; } if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) { metastate |= AMETA_META_ON; } return metastate; } static enum android_metastate convert_meta_state(uint16_t mod) { enum android_metastate metastate = 0; if (mod & SC_MOD_LSHIFT) { metastate |= AMETA_SHIFT_LEFT_ON; } if (mod & SC_MOD_RSHIFT) { metastate |= AMETA_SHIFT_RIGHT_ON; } if (mod & SC_MOD_LCTRL) { metastate |= AMETA_CTRL_LEFT_ON; } if (mod & SC_MOD_RCTRL) { metastate |= AMETA_CTRL_RIGHT_ON; } if (mod & SC_MOD_LALT) { metastate |= AMETA_ALT_LEFT_ON; } if (mod & SC_MOD_RALT) { metastate |= AMETA_ALT_RIGHT_ON; } if (mod & SC_MOD_LGUI) { // Windows key metastate |= AMETA_META_LEFT_ON; } if (mod & SC_MOD_RGUI) { // Windows key metastate |= AMETA_META_RIGHT_ON; } if (mod & SC_MOD_NUM) { metastate |= AMETA_NUM_LOCK_ON; } if (mod & SC_MOD_CAPS) { metastate |= AMETA_CAPS_LOCK_ON; } // fill the dependent fields return autocomplete_metastate(metastate); } static bool convert_input_key(const struct sc_key_event *event, struct sc_control_msg *msg, enum sc_key_inject_mode key_inject_mode, uint32_t repeat) { msg->type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; if (!convert_keycode(event->keycode, &msg->inject_keycode.keycode, event->mods_state, key_inject_mode)) { return false; } msg->inject_keycode.action = convert_keycode_action(event->action); msg->inject_keycode.repeat = repeat; msg->inject_keycode.metastate = convert_meta_state(event->mods_state); return true; } static void sc_key_processor_process_key(struct sc_key_processor *kp, const struct sc_key_event *event, uint64_t ack_to_wait) { // The device clipboard synchronization and the key event messages are // serialized, there is nothing special to do to ensure that the clipboard // is set before injecting Ctrl+v. (void) ack_to_wait; struct sc_keyboard_inject *ki = DOWNCAST(kp); if (event->repeat) { if (!ki->forward_key_repeat) { return; } ++ki->repeat; } else { ki->repeat = 0; } struct sc_control_msg msg; if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { if (!sc_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 struct sc_text_event *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 sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.inject_text.text = strdup(event->text); if (!msg.inject_text.text) { LOGW("Could not strdup input text"); return; } if (!sc_controller_push_msg(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 sc_controller *controller, enum sc_key_inject_mode key_inject_mode, bool forward_key_repeat) { ki->controller = controller; ki->key_inject_mode = key_inject_mode; ki->forward_key_repeat = 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.25/app/src/keyboard_inject.h000066400000000000000000000014461435104021100176140ustar00rootroot00000000000000#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 sc_controller *controller; // SDL reports repeated events as a boolean, but Android expects the actual // number of repetitions. This variable keeps track of the count. unsigned repeat; enum sc_key_inject_mode key_inject_mode; bool forward_key_repeat; }; void sc_keyboard_inject_init(struct sc_keyboard_inject *ki, struct sc_controller *controller, enum sc_key_inject_mode key_inject_mode, bool forward_key_repeat); #endif scrcpy-1.25/app/src/main.c000066400000000000000000000055341435104021100154010ustar00rootroot00000000000000#include "common.h" #include #include #include #include #ifdef _WIN32 #include #include "util/str.h" #endif #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 "usb/scrcpy_otg.h" #include "util/log.h" #include "version.h" int main_scrcpy(int argc, char *argv[]) { #ifdef _WIN32 // disable buffering, we want logs immediately // even line buffering (setvbuf() with mode _IOLBF) is not sufficient setbuf(stdout, NULL); setbuf(stderr, NULL); #endif printf("scrcpy " SCRCPY_VERSION " \n"); struct scrcpy_cli_args args = { .opts = scrcpy_options_default, .help = false, .version = false, }; #ifndef NDEBUG args.opts.log_level = SC_LOG_LEVEL_DEBUG; #endif if (!scrcpy_parse_args(&args, argc, argv)) { return SCRCPY_EXIT_FAILURE; } sc_set_log_level(args.opts.log_level); if (args.help) { scrcpy_print_usage(argv[0]); return SCRCPY_EXIT_SUCCESS; } if (args.version) { scrcpy_print_version(); return SCRCPY_EXIT_SUCCESS; } #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 SCRCPY_EXIT_FAILURE; } #ifdef HAVE_USB enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts); #else enum scrcpy_exit_code ret = scrcpy(&args.opts); #endif avformat_network_deinit(); // ignore failure return ret; } int main(int argc, char *argv[]) { #ifndef _WIN32 return main_scrcpy(argc, argv); #else (void) argc; (void) argv; int wargc; wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); if (!wargv) { LOG_OOM(); return SCRCPY_EXIT_FAILURE; } char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8)); if (!argv_utf8) { LOG_OOM(); LocalFree(wargv); return SCRCPY_EXIT_FAILURE; } argv_utf8[wargc] = NULL; for (int i = 0; i < wargc; ++i) { argv_utf8[i] = sc_str_from_wchars(wargv[i]); if (!argv_utf8[i]) { LOG_OOM(); for (int j = 0; j < i; ++j) { free(argv_utf8[j]); } LocalFree(wargv); free(argv_utf8); return SCRCPY_EXIT_FAILURE; } } LocalFree(wargv); int ret = main_scrcpy(wargc, argv_utf8); for (int i = 0; i < wargc; ++i) { free(argv_utf8[i]); } free(argv_utf8); return ret; #endif } scrcpy-1.25/app/src/mouse_inject.c000066400000000000000000000117521435104021100171400ustar00rootroot00000000000000#include "mouse_inject.h" #include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" #include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_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 & SC_MOUSE_BUTTON_LEFT) { buttons |= AMOTION_EVENT_BUTTON_PRIMARY; } if (state & SC_MOUSE_BUTTON_RIGHT) { buttons |= AMOTION_EVENT_BUTTON_SECONDARY; } if (state & SC_MOUSE_BUTTON_MIDDLE) { buttons |= AMOTION_EVENT_BUTTON_TERTIARY; } if (state & SC_MOUSE_BUTTON_X1) { buttons |= AMOTION_EVENT_BUTTON_BACK; } if (state & SC_MOUSE_BUTTON_X2) { buttons |= AMOTION_EVENT_BUTTON_FORWARD; } return buttons; } static enum android_motionevent_action convert_mouse_action(enum sc_action action) { if (action == SC_ACTION_DOWN) { return AMOTION_EVENT_ACTION_DOWN; } assert(action == SC_ACTION_UP); return AMOTION_EVENT_ACTION_UP; } static enum android_motionevent_action convert_touch_action(enum sc_touch_action action) { switch (action) { case SC_TOUCH_ACTION_MOVE: return AMOTION_EVENT_ACTION_MOVE; case SC_TOUCH_ACTION_DOWN: return AMOTION_EVENT_ACTION_DOWN; default: assert(action == SC_TOUCH_ACTION_UP); return AMOTION_EVENT_ACTION_UP; } } static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { if (!event->buttons_state) { // Do not send motion events when no click is pressed return; } struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = AMOTION_EVENT_ACTION_MOVE, .pointer_id = event->pointer_id, .position = event->position, .pressure = 1.f, .buttons = convert_mouse_buttons(event->buttons_state), }, }; if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse motion event'"); } } static void sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_mouse_action(event->action), .pointer_id = event->pointer_id, .position = event->position, .pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f, .buttons = convert_mouse_buttons(event->buttons_state), }, }; if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse click event'"); } } static void sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, .hscroll = event->hscroll, .vscroll = event->vscroll, .buttons = convert_mouse_buttons(event->buttons_state), }, }; if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject mouse scroll event'"); } } static void sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, const struct sc_touch_event *event) { struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .inject_touch_event = { .action = convert_touch_action(event->action), .pointer_id = event->pointer_id, .position = event->position, .pressure = event->pressure, .buttons = 0, }, }; if (!sc_controller_push_msg(mi->controller, &msg)) { LOGW("Could not request 'inject touch event'"); } } void sc_mouse_inject_init(struct sc_mouse_inject *mi, struct sc_controller *controller) { mi->controller = controller; static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, .process_mouse_click = sc_mouse_processor_process_mouse_click, .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, .process_touch = sc_mouse_processor_process_touch, }; mi->mouse_processor.ops = &ops; mi->mouse_processor.relative_mode = false; } scrcpy-1.25/app/src/mouse_inject.h000066400000000000000000000006621435104021100171430ustar00rootroot00000000000000#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 sc_controller *controller; }; void sc_mouse_inject_init(struct sc_mouse_inject *mi, struct sc_controller *controller); #endif scrcpy-1.25/app/src/opengl.c000066400000000000000000000031601435104021100157320ustar00rootroot00000000000000#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(OPENGL_ES_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.25/app/src/opengl.h000066400000000000000000000013161435104021100157400ustar00rootroot00000000000000#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.25/app/src/options.c000066400000000000000000000034751435104021100161520ustar00rootroot00000000000000#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_SHORTCUT_MOD_LALT, SC_SHORTCUT_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, #ifdef HAVE_USB .otg = false, #endif .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, .downsize_on_error = true, .tcpip = false, .tcpip_dst = NULL, .select_tcpip = false, .select_usb = false, .cleanup = true, .start_fps_counter = false, .power_on = true, }; scrcpy-1.25/app/src/options.h000066400000000000000000000067651435104021100161640ustar00rootroot00000000000000#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_mouse_input_mode { SC_MOUSE_INPUT_MODE_INJECT, SC_MOUSE_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_SHORTCUT_MOD_LCTRL = 1 << 0, SC_SHORTCUT_MOD_RCTRL = 1 << 1, SC_SHORTCUT_MOD_LALT = 1 << 2, SC_SHORTCUT_MOD_RALT = 1 << 3, SC_SHORTCUT_MOD_LSUPER = 1 << 4, SC_SHORTCUT_MOD_RSUPER = 1 << 5, }; struct sc_shortcut_mods { unsigned data[SC_MAX_SHORTCUT_MODS]; unsigned count; }; struct sc_port_range { uint16_t first; uint16_t last; }; #define SC_WINDOW_POSITION_UNDEFINED (-0x8000) struct scrcpy_options { const char *serial; const char *crop; const char *record_filename; const char *window_title; const char *push_target; const char *render_driver; const char *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; enum sc_mouse_input_mode mouse_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; #ifdef HAVE_USB bool otg; #endif 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 downsize_on_error; bool tcpip; const char *tcpip_dst; bool select_usb; bool select_tcpip; bool cleanup; bool start_fps_counter; bool power_on; }; extern const struct scrcpy_options scrcpy_options_default; #endif scrcpy-1.25/app/src/receiver.c000066400000000000000000000060541435104021100162570ustar00rootroot00000000000000#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, "scrcpy-receiver", receiver); if (!ok) { LOGE("Could not start receiver thread"); return false; } return true; } void receiver_join(struct receiver *receiver) { sc_thread_join(&receiver->thread, NULL); } scrcpy-1.25/app/src/receiver.h000066400000000000000000000012751435104021100162640ustar00rootroot00000000000000#ifndef SC_RECEIVER_H #define SC_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.25/app/src/recorder.c000066400000000000000000000270061435104021100162600ustar00rootroot00000000000000#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 sc_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 sc_record_packet * sc_record_packet_new(const AVPacket *packet) { struct sc_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 sc_record_packet_delete(struct sc_record_packet *rec) { av_packet_free(&rec->packet); free(rec); } static void sc_recorder_queue_clear(struct sc_recorder_queue *queue) { while (!sc_queue_is_empty(queue)) { struct sc_record_packet *rec; sc_queue_take(queue, next, &rec); sc_record_packet_delete(rec); } } static const char * sc_recorder_get_format_name(enum sc_record_format format) { switch (format) { case SC_RECORD_FORMAT_MP4: return "mp4"; case SC_RECORD_FORMAT_MKV: return "matroska"; default: return NULL; } } static bool sc_recorder_write_header(struct sc_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 sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) { AVStream *ostream = recorder->ctx->streams[0]; av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); } static bool sc_recorder_write(struct sc_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 = sc_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; } sc_recorder_rescale_packet(recorder, packet); return av_write_frame(recorder->ctx, packet) >= 0; } static int run_recorder(void *data) { struct sc_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 sc_record_packet *last = recorder->previous; if (last) { // assign an arbitrary duration to the last packet last->packet->duration = 100000; bool ok = sc_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"); } sc_record_packet_delete(last); } break; } struct sc_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 sc_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 = sc_recorder_write(recorder, previous->packet); sc_record_packet_delete(previous); if (!ok) { LOGE("Could not record packet"); sc_mutex_lock(&recorder->mutex); recorder->failed = true; // discard pending packets sc_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 = sc_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 sc_recorder_open(struct sc_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 = sc_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, "scrcpy-recorder", recorder); if (!ok) { LOGE("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 sc_recorder_close(struct sc_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 sc_recorder_push(struct sc_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 sc_record_packet *rec = sc_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 sc_recorder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) { struct sc_recorder *recorder = DOWNCAST(sink); return sc_recorder_open(recorder, codec); } static void sc_recorder_packet_sink_close(struct sc_packet_sink *sink) { struct sc_recorder *recorder = DOWNCAST(sink); sc_recorder_close(recorder); } static bool sc_recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) { struct sc_recorder *recorder = DOWNCAST(sink); return sc_recorder_push(recorder, packet); } bool sc_recorder_init(struct sc_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 = sc_recorder_packet_sink_open, .close = sc_recorder_packet_sink_close, .push = sc_recorder_packet_sink_push, }; recorder->packet_sink.ops = &ops; return true; } void sc_recorder_destroy(struct sc_recorder *recorder) { free(recorder->filename); } scrcpy-1.25/app/src/recorder.h000066400000000000000000000025001435104021100162550ustar00rootroot00000000000000#ifndef SC_RECORDER_H #define SC_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 sc_record_packet { AVPacket *packet; struct sc_record_packet *next; }; struct sc_recorder_queue SC_QUEUE(struct sc_record_packet); struct sc_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 sc_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 sc_record_packet *previous; }; bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, struct sc_size declared_frame_size); void sc_recorder_destroy(struct sc_recorder *recorder); #endif scrcpy-1.25/app/src/scrcpy.c000066400000000000000000000516361435104021100157640ustar00rootroot00000000000000#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 "demuxer.h" #include "events.h" #include "file_pusher.h" #include "keyboard_inject.h" #include "mouse_inject.h" #include "recorder.h" #include "screen.h" #include "server.h" #ifdef HAVE_USB # include "usb/aoa_hid.h" # include "usb/hid_keyboard.h" # include "usb/hid_mouse.h" # include "usb/usb.h" #endif #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 sc_screen screen; struct sc_demuxer demuxer; struct sc_decoder decoder; struct sc_recorder recorder; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; #endif struct sc_controller controller; struct sc_file_pusher file_pusher; #ifdef HAVE_USB struct sc_usb usb; struct sc_aoa aoa; // sequence/ack helper to synchronize clipboard and Ctrl+v via HID struct sc_acksync acksync; #endif union { struct sc_keyboard_inject keyboard_inject; #ifdef HAVE_USB struct sc_hid_keyboard keyboard_hid; #endif }; union { struct sc_mouse_inject mouse_inject; #ifdef HAVE_USB struct sc_hid_mouse mouse_hid; #endif }; }; 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"); } // Handle a click to gain focus as any other click if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { LOGW("Could not enable mouse focus clickthrough"); } #ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS // Disable synthetic mouse events from touch events // Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is // better not to generate them in the first place. if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) { LOGW("Could not disable synthetic mouse events"); } #endif #ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR // 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) { SDL_DisableScreenSaver(); } else { SDL_EnableScreenSaver(); } } static enum scrcpy_exit_code event_loop(struct scrcpy *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case EVENT_STREAM_STOPPED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; default: sc_screen_handle_event(&s->screen, &event); break; } } return SCRCPY_EXIT_FAILURE; } // Return true on success, false on error static bool await_for_server(bool *connected) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case SDL_QUIT: LOGD("User requested to quit"); *connected = false; return true; case EVENT_SERVER_CONNECTION_FAILED: LOGE("Server connection failed"); return false; case EVENT_SERVER_CONNECTED: LOGD("Server connected"); *connected = true; return true; default: break; } } LOGE("SDL_WaitEvent() error: %s", SDL_GetError()); return false; } static SDL_LogPriority sdl_priority_from_av_level(int level) { switch (level) { case AV_LOG_PANIC: case AV_LOG_FATAL: return SDL_LOG_PRIORITY_CRITICAL; case AV_LOG_ERROR: return SDL_LOG_PRIORITY_ERROR; case AV_LOG_WARNING: return SDL_LOG_PRIORITY_WARN; case AV_LOG_INFO: return SDL_LOG_PRIORITY_INFO; } // do not forward others, which are too verbose return 0; } static void 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 sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) { (void) demuxer; (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 } enum scrcpy_exit_code scrcpy(struct scrcpy_options *options) { static struct scrcpy scrcpy; struct scrcpy *s = &scrcpy; // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); return SCRCPY_EXIT_FAILURE; } atexit(SDL_Quit); enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; bool server_started = false; bool file_pusher_initialized = false; bool recorder_initialized = false; #ifdef HAVE_V4L2 bool v4l2_sink_initialized = false; #endif bool demuxer_started = false; #ifdef HAVE_USB bool aoa_hid_initialized = false; bool hid_keyboard_initialized = false; bool hid_mouse_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 = { .req_serial = options->serial, .select_usb = options->select_usb, .select_tcpip = options->select_tcpip, .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, .downsize_on_error = options->downsize_on_error, .tcpip = options->tcpip, .tcpip_dst = options->tcpip_dst, .cleanup = options->cleanup, .power_on = options->power_on, }; static const struct sc_server_callbacks cbs = { .on_connection_failed = sc_server_on_connection_failed, .on_connected = sc_server_on_connected, .on_disconnected = sc_server_on_disconnected, }; if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) { return SCRCPY_EXIT_FAILURE; } if (!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)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); goto end; } sdl_configure(options->display, options->disable_screensaver); // Await for server without blocking Ctrl+C handling bool connected; if (!await_for_server(&connected)) { goto end; } if (!connected) { // This is not an error, user requested to quit ret = SCRCPY_EXIT_SUCCESS; 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.serial; assert(serial); struct sc_file_pusher *fp = NULL; if (options->display && options->control) { if (!sc_file_pusher_init(&s->file_pusher, serial, options->push_target)) { goto end; } fp = &s->file_pusher; file_pusher_initialized = true; } struct sc_decoder *dec = NULL; bool needs_decoder = options->display; #ifdef HAVE_V4L2 needs_decoder |= !!options->v4l2_device; #endif if (needs_decoder) { sc_decoder_init(&s->decoder); dec = &s->decoder; } struct sc_recorder *rec = NULL; if (options->record_filename) { if (!sc_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 sc_demuxer_callbacks demuxer_cbs = { .on_eos = sc_demuxer_on_eos, }; sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL); if (dec) { sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink); } if (rec) { sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink); } struct sc_controller *controller = NULL; struct sc_key_processor *kp = NULL; struct sc_mouse_processor *mp = NULL; if (options->control) { #ifdef HAVE_USB bool use_hid_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; bool use_hid_mouse = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; if (use_hid_keyboard || use_hid_mouse) { bool ok = sc_acksync_init(&s->acksync); if (!ok) { goto end; } ok = sc_usb_init(&s->usb); if (!ok) { LOGE("Failed to initialize USB"); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } assert(serial); struct sc_usb_device usb_device; ok = sc_usb_select_device(&s->usb, serial, &usb_device); if (!ok) { sc_usb_destroy(&s->usb); goto aoa_hid_end; } LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", usb_device.serial, usb_device.vid, usb_device.pid, usb_device.manufacturer, usb_device.product); ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL); sc_usb_device_destroy(&usb_device); if (!ok) { LOGE("Failed to connect to USB device %s", serial); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); if (!ok) { LOGE("Failed to enable HID over AOA"); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_acksync_destroy(&s->acksync); goto aoa_hid_end; } if (use_hid_keyboard) { if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { hid_keyboard_initialized = true; kp = &s->keyboard_hid.key_processor; } else { LOGE("Could not initialize HID keyboard"); } } if (use_hid_mouse) { if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) { hid_mouse_initialized = true; mp = &s->mouse_hid.mouse_processor; } else { LOGE("Could not initialized HID mouse"); } } bool need_aoa = hid_keyboard_initialized || hid_mouse_initialized; if (!need_aoa || !sc_aoa_start(&s->aoa)) { sc_acksync_destroy(&s->acksync); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); sc_aoa_destroy(&s->aoa); goto aoa_hid_end; } acksync = &s->acksync; aoa_hid_initialized = true; aoa_hid_end: if (!aoa_hid_initialized) { if (hid_keyboard_initialized) { sc_hid_keyboard_destroy(&s->keyboard_hid); hid_keyboard_initialized = false; } if (hid_mouse_initialized) { sc_hid_mouse_destroy(&s->mouse_hid); hid_mouse_initialized = false; } } if (use_hid_keyboard && !hid_keyboard_initialized) { LOGE("Fallback to default keyboard injection method " "(-K/--hid-keyboard ignored)"); options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT; } if (use_hid_mouse && !hid_mouse_initialized) { LOGE("Fallback to default mouse injection method " "(-M/--hid-mouse ignored)"); options->mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT; } } #else assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID); assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_HID); #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->key_inject_mode, options->forward_key_repeat); kp = &s->keyboard_inject.key_processor; } // mouse_input_mode may have been reset if HID mode failed if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_INJECT) { sc_mouse_inject_init(&s->mouse_inject, &s->controller); mp = &s->mouse_inject.mouse_processor; } if (!sc_controller_init(&s->controller, s->server.control_socket, acksync)) { goto end; } controller_initialized = true; if (!sc_controller_start(&s->controller)) { goto end; } controller_started = true; controller = &s->controller; if (options->turn_screen_off) { struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; if (!sc_controller_push_msg(&s->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); } } } // There is a controller if and only if control is enabled assert(options->control == !!controller); if (options->display) { const char *window_title = options->window_title ? options->window_title : info->device_name; struct sc_screen_params screen_params = { .controller = controller, .fp = fp, .kp = kp, .mp = mp, .forward_all_clicks = options->forward_all_clicks, .legacy_paste = options->legacy_paste, .clipboard_autosync = options->clipboard_autosync, .shortcut_mods = &options->shortcut_mods, .window_title = window_title, .frame_size = 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, .start_fps_counter = options->start_fps_counter, .buffering_time = options->display_buffer, }; if (!sc_screen_init(&s->screen, &screen_params)) { goto end; } screen_initialized = true; sc_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; } sc_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 demuxer if (!sc_demuxer_start(&s->demuxer)) { goto end; } demuxer_started = true; ret = event_loop(s); LOGD("quit..."); // Close the window immediately on closing, because screen_destroy() may // only be called once the demuxer thread is joined (it may take time) sc_screen_hide_window(&s->screen); end: // The demuxer is not stopped explicitly, because it will stop by itself on // end-of-stream #ifdef HAVE_USB if (aoa_hid_initialized) { if (hid_keyboard_initialized) { sc_hid_keyboard_destroy(&s->keyboard_hid); } if (hid_mouse_initialized) { sc_hid_mouse_destroy(&s->mouse_hid); } sc_aoa_stop(&s->aoa); sc_usb_stop(&s->usb); } if (acksync) { sc_acksync_destroy(acksync); } #endif if (controller_started) { sc_controller_stop(&s->controller); } if (file_pusher_initialized) { sc_file_pusher_stop(&s->file_pusher); } if (screen_initialized) { sc_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 demuxer and controller are // interrupted, we can join them if (demuxer_started) { sc_demuxer_join(&s->demuxer); } #ifdef HAVE_V4L2 if (v4l2_sink_initialized) { sc_v4l2_sink_destroy(&s->v4l2_sink); } #endif #ifdef HAVE_USB if (aoa_hid_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); sc_usb_join(&s->usb); sc_usb_disconnect(&s->usb); sc_usb_destroy(&s->usb); } #endif // Destroy the screen only after the demuxer is guaranteed to be finished, // because otherwise the screen could receive new frames after destruction if (screen_initialized) { sc_screen_join(&s->screen); sc_screen_destroy(&s->screen); } if (controller_started) { sc_controller_join(&s->controller); } if (controller_initialized) { sc_controller_destroy(&s->controller); } if (recorder_initialized) { sc_recorder_destroy(&s->recorder); } if (file_pusher_initialized) { sc_file_pusher_join(&s->file_pusher); sc_file_pusher_destroy(&s->file_pusher); } sc_server_destroy(&s->server); return ret; } scrcpy-1.25/app/src/scrcpy.h000066400000000000000000000006201435104021100157540ustar00rootroot00000000000000#ifndef SCRCPY_H #define SCRCPY_H #include "common.h" #include #include "options.h" enum scrcpy_exit_code { // Normal program termination SCRCPY_EXIT_SUCCESS, // No connection could be established SCRCPY_EXIT_FAILURE, // Device was disconnected while running SCRCPY_EXIT_DISCONNECTED, }; enum scrcpy_exit_code scrcpy(struct scrcpy_options *options); #endif scrcpy-1.25/app/src/screen.c000066400000000000000000000760431435104021100157370ustar00rootroot00000000000000#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 sc_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 sc_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 sc_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 sc_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; if (SDL_GetDisplayUsableBounds(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, bool within_display_bounds) { 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 (!within_display_bounds || !get_preferred_display_bounds(&display_size)) { // do not constraint the size window_size = current_size; } else { window_size.width = MIN(current_size.width, display_size.width); window_size.height = MIN(current_size.height, display_size.height); } 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, true); } 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 inline bool sc_screen_is_relative_mode(struct sc_screen *screen) { // screen->im.mp may be NULL if --no-control return screen->im.mp && screen->im.mp->relative_mode; } static void sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { #ifdef __APPLE__ // Workaround for SDL bug on macOS: // if (capture) { int mouse_x, mouse_y; SDL_GetGlobalMouseState(&mouse_x, &mouse_y); int x, y, w, h; SDL_GetWindowPosition(screen->window, &x, &y); SDL_GetWindowSize(screen->window, &w, &h); bool outside_window = mouse_x < x || mouse_x >= x + w || mouse_y < y || mouse_y >= y + h; if (outside_window) { SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); } } #else (void) screen; #endif if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); } } static inline bool sc_screen_get_mouse_capture(struct sc_screen *screen) { (void) screen; return SDL_GetRelativeMouseMode(); } static inline void sc_screen_toggle_mouse_capture(struct sc_screen *screen) { (void) screen; bool new_value = !sc_screen_get_mouse_capture(screen); sc_screen_set_mouse_capture(screen, new_value); } static void sc_screen_update_content_rect(struct sc_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 sc_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 sc_screen_render(struct sc_screen *screen, bool update_content_rect) { if (update_content_rect) { sc_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__) # define CONTINUOUS_RESIZING_WORKAROUND #endif #ifdef CONTINUOUS_RESIZING_WORKAROUND // On Windows and MacOS, resizing blocks the event loop, so resizing events are // not triggered. On MacOS, as a workaround, handle them in an event handler // (it does not work for Windows unfortunately). // // // static int event_watcher(void *data, SDL_Event *event) { struct sc_screen *screen = data; if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { // In practice, it seems to always be called from the same thread in // that specific case. Anyway, it's just a workaround. sc_screen_render(screen, true); } return 0; } #endif static bool sc_screen_frame_sink_open(struct sc_frame_sink *sink) { struct sc_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 sc_screen_frame_sink_close(struct sc_frame_sink *sink) { struct sc_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 sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_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 sc_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) { sc_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 sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params) { screen->resize_pending = false; screen->has_frame = false; screen->fullscreen = false; screen->maximized = false; screen->event_failed = false; screen->mouse_capture_key_pressed = 0; screen->req.x = params->window_x; screen->req.y = params->window_y; screen->req.width = params->window_width; screen->req.height = params->window_height; screen->req.fullscreen = params->fullscreen; screen->req.start_fps_counter = params->start_fps_counter; 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 (!sc_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; uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } // The window will be positioned and sized on first video frame screen->window = SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); if (!screen->window) { LOGE("Could not create window: %s", SDL_GetError()); goto error_destroy_fps_counter; } screen->renderer = SDL_CreateRenderer(screen->window, -1, SDL_RENDERER_ACCELERATED); if (!screen->renderer) { LOGE("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) { LOGE("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; } struct sc_input_manager_params im_params = { .controller = params->controller, .fp = params->fp, .screen = screen, .kp = params->kp, .mp = params->mp, .forward_all_clicks = params->forward_all_clicks, .legacy_paste = params->legacy_paste, .clipboard_autosync = params->clipboard_autosync, .shortcut_mods = params->shortcut_mods, }; sc_input_manager_init(&screen->im, &im_params); #ifdef CONTINUOUS_RESIZING_WORKAROUND SDL_AddEventWatch(event_watcher, screen); #endif static const struct sc_frame_sink_ops ops = { .open = sc_screen_frame_sink_open, .close = sc_screen_frame_sink_close, .push = sc_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: sc_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 sc_screen_show_initial_window(struct sc_screen *screen) { int x = screen->req.x != SC_WINDOW_POSITION_UNDEFINED ? screen->req.x : (int) SDL_WINDOWPOS_CENTERED; int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED ? screen->req.y : (int) SDL_WINDOWPOS_CENTERED; struct sc_size window_size = get_initial_optimal_size(screen->content_size, screen->req.width, screen->req.height); set_window_size(screen, window_size); SDL_SetWindowPosition(screen->window, x, y); if (screen->req.fullscreen) { sc_screen_switch_fullscreen(screen); } if (screen->req.start_fps_counter) { sc_fps_counter_start(&screen->fps_counter); } SDL_ShowWindow(screen->window); } void sc_screen_hide_window(struct sc_screen *screen) { SDL_HideWindow(screen->window); } void sc_screen_interrupt(struct sc_screen *screen) { sc_video_buffer_stop(&screen->vb); sc_fps_counter_interrupt(&screen->fps_counter); } void sc_screen_join(struct sc_screen *screen) { sc_video_buffer_join(&screen->vb); sc_fps_counter_join(&screen->fps_counter); } void sc_screen_destroy(struct sc_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); sc_fps_counter_destroy(&screen->fps_counter); sc_video_buffer_destroy(&screen->vb); } static void resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width / old_content_size.width, .height = (uint32_t) window_size.height * new_content_size.height / old_content_size.height, }; target_size = get_optimal_size(target_size, new_content_size, true); set_window_size(screen, target_size); } static void set_content_size(struct sc_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 sc_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 sc_screen_set_rotation(struct sc_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); sc_screen_render(screen, true); } // recreate the texture and resize the window if the frame size has changed static bool prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { if (screen->frame_size.width != new_frame_size.width || screen->frame_size.height != new_frame_size.height) { // 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); sc_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) { LOGE("Could not create texture: %s", SDL_GetError()); return false; } } return true; } // write the frame into the texture static void update_texture(struct sc_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 sc_screen_update_frame(struct sc_screen *screen) { av_frame_unref(screen->frame); sc_video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; sc_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); if (!screen->has_frame) { screen->has_frame = true; // this is the very first frame, show the window sc_screen_show_initial_window(screen); if (sc_screen_is_relative_mode(screen)) { // Capture mouse on start sc_screen_set_mouse_capture(screen, true); } } sc_screen_render(screen, false); return true; } void sc_screen_switch_fullscreen(struct sc_screen *screen) { uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; if (SDL_SetWindowFullscreen(screen->window, new_mode)) { LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); return; } screen->fullscreen = !screen->fullscreen; if (!screen->fullscreen && !screen->maximized) { apply_pending_resize(screen); } LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed"); sc_screen_render(screen, true); } void sc_screen_resize_to_fit(struct sc_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, false); // 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 sc_screen_resize_to_pixel_perfect(struct sc_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); } static inline bool sc_screen_is_mouse_capture_key(SDL_Keycode key) { return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; } void sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { bool relative_mode = sc_screen_is_relative_mode(screen); switch (event->type) { case EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { LOGW("Frame update failed\n"); } return; } case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing return; } switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: sc_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); sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_FOCUS_LOST: if (relative_mode) { sc_screen_set_mouse_capture(screen, false); } break; } return; case SDL_KEYDOWN: if (relative_mode) { SDL_Keycode key = event->key.keysym.sym; if (sc_screen_is_mouse_capture_key(key)) { if (!screen->mouse_capture_key_pressed) { screen->mouse_capture_key_pressed = key; } else { // Another mouse capture key has been pressed, cancel // mouse (un)capture screen->mouse_capture_key_pressed = 0; } // Mouse capture keys are never forwarded to the device return; } } break; case SDL_KEYUP: if (relative_mode) { SDL_Keycode key = event->key.keysym.sym; SDL_Keycode cap = screen->mouse_capture_key_pressed; screen->mouse_capture_key_pressed = 0; if (sc_screen_is_mouse_capture_key(key)) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode sc_screen_toggle_mouse_capture(screen); } // Mouse capture keys are never forwarded to the device return; } } break; case SDL_MOUSEWHEEL: case SDL_MOUSEMOTION: case SDL_MOUSEBUTTONDOWN: if (relative_mode && !sc_screen_get_mouse_capture(screen)) { // Do not forward to input manager, the mouse will be captured // on SDL_MOUSEBUTTONUP return; } break; case SDL_FINGERMOTION: case SDL_FINGERDOWN: case SDL_FINGERUP: if (relative_mode) { // Touch events are not compatible with relative mode // (coordinates are not relative) return; } break; case SDL_MOUSEBUTTONUP: if (relative_mode && !sc_screen_get_mouse_capture(screen)) { sc_screen_set_mouse_capture(screen, true); return; } break; } sc_input_manager_handle_event(&screen->im, event); } struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_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 sc_screen_convert_window_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { sc_screen_hidpi_scale_coords(screen, &x, &y); return sc_screen_convert_drawable_to_frame_coords(screen, x, y); } void sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) { // take the HiDPI scaling (dw/ww and dh/wh) into account int ww, wh, dw, dh; SDL_GetWindowSize(screen->window, &ww, &wh); 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.25/app/src/screen.h000066400000000000000000000107251435104021100157370ustar00rootroot00000000000000#ifndef SCREEN_H #define SCREEN_H #include "common.h" #include #include #include #include "controller.h" #include "coords.h" #include "fps_counter.h" #include "input_manager.h" #include "opengl.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" #include "trait/mouse_processor.h" #include "video_buffer.h" struct sc_screen { struct sc_frame_sink frame_sink; // frame sink trait #ifndef NDEBUG bool open; // track the open/close state to assert correct behavior #endif struct sc_input_manager im; struct sc_video_buffer vb; struct sc_fps_counter fps_counter; // The initial requested window properties struct { int16_t x; int16_t y; uint16_t width; uint16_t height; bool fullscreen; bool start_fps_counter; } req; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; struct sc_opengl gl; struct 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 // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. SDL_Keycode mouse_capture_key_pressed; AVFrame *frame; }; struct sc_screen_params { struct sc_controller *controller; struct sc_file_pusher *fp; struct sc_key_processor *kp; struct sc_mouse_processor *mp; bool forward_all_clicks; bool legacy_paste; bool clipboard_autosync; const struct sc_shortcut_mods *shortcut_mods; const char *window_title; struct sc_size frame_size; bool always_on_top; int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED uint16_t window_width; uint16_t window_height; bool window_borderless; uint8_t rotation; bool mipmaps; bool fullscreen; bool start_fps_counter; sc_tick buffering_time; }; // initialize screen, create window, renderer and texture (window is hidden) bool sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params); // request to interrupt any inner thread // must be called before screen_join() void sc_screen_interrupt(struct sc_screen *screen); // join any inner thread void sc_screen_join(struct sc_screen *screen); // destroy window, renderer and texture (if any) void sc_screen_destroy(struct sc_screen *screen); // hide the window // // It is used to hide the window immediately on closing without waiting for // screen_destroy() void sc_screen_hide_window(struct sc_screen *screen); // switch the fullscreen mode void sc_screen_switch_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void sc_screen_resize_to_fit(struct sc_screen *screen); // resize window to 1:1 (pixel-perfect) void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); // set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) void sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); // react to SDL events void sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event); // convert point from window coordinates to frame coordinates // x and y are expressed in pixels struct sc_point sc_screen_convert_window_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y); // convert point from drawable coordinates to frame coordinates // x and y are expressed in pixels struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y); // Convert coordinates from window to drawable. // Events are expressed in window coordinates, but content is expressed in // drawable coordinates. They are the same if HiDPI scaling is 1, but differ // otherwise. void sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y); #endif scrcpy-1.25/app/src/server.c000066400000000000000000000632611435104021100157640ustar00rootroot00000000000000#include "server.h" #include #include #include #include #include #include #include "adb/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" #define SC_ADB_PORT_DEFAULT 5555 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->req_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(req_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 = sc_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 *serial = server->serial; assert(serial); const char *cmd[128]; unsigned count = 0; cmd[count++] = sc_adb_get_executable(); cmd[count++] = "-s"; cmd[count++] = serial; cmd[count++] = "shell"; cmd[count++] = "CLASSPATH=" SC_DEVICE_SERVER_PATH; cmd[count++] = "app_process"; #ifdef SERVER_DEBUGGER # define SERVER_DEBUGGER_PORT "5005" cmd[count++] = # ifdef SERVER_DEBUGGER_METHOD_NEW /* Android 9 and above */ "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y," "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; \ } 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=true"); } if (params->crop) { ADD_PARAM("crop=%s", params->crop); } if (!params->control) { // By default, control is true ADD_PARAM("control=false"); } if (params->display_id) { ADD_PARAM("display_id=%" PRIu32, params->display_id); } if (params->show_touches) { ADD_PARAM("show_touches=true"); } if (params->stay_awake) { ADD_PARAM("stay_awake=true"); } 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=true"); } if (!params->clipboard_autosync) { // By default, clipboard_autosync is true ADD_PARAM("clipboard_autosync=false"); } if (!params->downsize_on_error) { // By default, downsize_on_error is true ADD_PARAM("downsize_on_error=false"); } if (!params->cleanup) { // By default, cleanup is true ADD_PARAM("cleanup=false"); } if (!params->power_on) { // By default, power_on is true ADD_PARAM("power_on=false"); } #undef ADD_PARAM cmd[count++] = NULL; #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 = sc_adb_execute(cmd, 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->serial = NULL; 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->serial; assert(serial); bool control = server->params.control; 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; } if (control) { control_socket = net_accept_intr(&server->intr, tunnel->server_socket); if (control_socket == SC_SOCKET_NONE) { goto fail; } } } else { uint32_t tunnel_host = server->params.tunnel_host; if (!tunnel_host) { tunnel_host = IPV4_LOCALHOST; } uint16_t tunnel_port = server->params.tunnel_port; if (!tunnel_port) { tunnel_port = tunnel->local_port; } unsigned attempts = 100; sc_tick delay = SC_TICK_FROM_MS(100); video_socket = connect_to_server(server, attempts, delay, tunnel_host, tunnel_port); if (video_socket == SC_SOCKET_NONE) { goto fail; } if (control) { // 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 || 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"); } } if (tunnel->enabled) { // 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 uint16_t get_adb_tcp_port(struct sc_server *server, const char *serial) { struct sc_intr *intr = &server->intr; char *current_port = sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT); if (!current_port) { return 0; } long value; bool ok = sc_str_parse_integer(current_port, &value); free(current_port); if (!ok) { return 0; } if (value < 0 || value > 0xFFFF) { return 0; } return value; } static bool wait_tcpip_mode_enabled(struct sc_server *server, const char *serial, uint16_t expected_port, unsigned attempts, sc_tick delay) { uint16_t adb_port = get_adb_tcp_port(server, serial); if (adb_port == expected_port) { return true; } // Only print this log if TCP/IP is not enabled LOGI("Waiting for TCP/IP mode enabled..."); do { sc_tick deadline = sc_tick_now() + delay; if (!sc_server_sleep(server, deadline)) { LOGI("TCP/IP mode waiting interrupted"); return false; } adb_port = get_adb_tcp_port(server, serial); if (adb_port == expected_port) { return true; } } while (--attempts); return false; } static char * append_port(const char *ip, uint16_t port) { char *ip_port; int ret = asprintf(&ip_port, "%s:%" PRIu16, ip, port); if (ret == -1) { LOG_OOM(); return NULL; } return ip_port; } static char * sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { assert(serial); struct sc_intr *intr = &server->intr; LOGI("Switching device %s to TCP/IP...", serial); char *ip = sc_adb_get_device_ip(intr, serial, 0); if (!ip) { LOGE("Device IP not found"); return NULL; } uint16_t adb_port = get_adb_tcp_port(server, serial); if (adb_port) { LOGI("TCP/IP mode already enabled on port %" PRIu16, adb_port); } else { LOGI("Enabling TCP/IP mode on port " SC_STR(SC_ADB_PORT_DEFAULT) "..."); bool ok = sc_adb_tcpip(intr, serial, SC_ADB_PORT_DEFAULT, SC_ADB_NO_STDOUT); if (!ok) { LOGE("Could not restart adbd in TCP/IP mode"); free(ip); return NULL; } unsigned attempts = 40; sc_tick delay = SC_TICK_FROM_MS(250); ok = wait_tcpip_mode_enabled(server, serial, SC_ADB_PORT_DEFAULT, attempts, delay); if (!ok) { free(ip); return NULL; } adb_port = SC_ADB_PORT_DEFAULT; LOGI("TCP/IP mode enabled on port " SC_STR(SC_ADB_PORT_DEFAULT)); } char *ip_port = append_port(ip, adb_port); free(ip); return ip_port; } static bool sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { struct sc_intr *intr = &server->intr; // Error expected if not connected, do not report any error sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); LOGI("Connecting to %s...", ip_port); bool ok = sc_adb_connect(intr, ip_port, 0); if (!ok) { LOGE("Could not connect to %s", ip_port); return false; } LOGI("Connected to %s", ip_port); return true; } static bool sc_server_configure_tcpip_known_address(struct sc_server *server, const char *addr) { // Append ":5555" if no port is present bool contains_port = strchr(addr, ':'); char *ip_port = contains_port ? strdup(addr) : append_port(addr, SC_ADB_PORT_DEFAULT); if (!ip_port) { LOG_OOM(); return false; } server->serial = ip_port; return sc_server_connect_to_tcpip(server, ip_port); } static bool sc_server_configure_tcpip_unknown_address(struct sc_server *server, const char *serial) { bool is_already_tcpip = sc_adb_device_get_type(serial) == SC_ADB_DEVICE_TYPE_TCPIP; if (is_already_tcpip) { // Nothing to do LOGI("Device already connected via TCP/IP: %s", serial); return true; } char *ip_port = sc_server_switch_to_tcpip(server, serial); if (!ip_port) { return false; } server->serial = ip_port; return sc_server_connect_to_tcpip(server, ip_port); } static int run_server(void *data) { struct sc_server *server = data; const struct sc_server_params *params = &server->params; // Execute "adb start-server" before "adb devices" so that daemon starting // output/errors is correctly printed in the console ("adb devices" output // is parsed, so it is not output) bool ok = sc_adb_start_server(&server->intr, 0); if (!ok) { LOGE("Could not start adb daemon"); goto error_connection_failed; } // params->tcpip_dst implies params->tcpip assert(!params->tcpip_dst || params->tcpip); // If tcpip_dst parameter is given, then it must connect to this address. // Therefore, the device is unknown, so serial is meaningless at this point. assert(!params->req_serial || !params->tcpip_dst); // A device must be selected via a serial in all cases except when --tcpip= // is called with a parameter (in that case, the device may initially not // exist, and scrcpy will execute "adb connect"). bool need_initial_serial = !params->tcpip_dst; if (need_initial_serial) { // At most one of the 3 following parameters may be set assert(!!params->req_serial + params->select_usb + params->select_tcpip <= 1); struct sc_adb_device_selector selector; if (params->req_serial) { selector.type = SC_ADB_DEVICE_SELECT_SERIAL; selector.serial = params->req_serial; } else if (params->select_usb) { selector.type = SC_ADB_DEVICE_SELECT_USB; } else if (params->select_tcpip) { selector.type = SC_ADB_DEVICE_SELECT_TCPIP; } else { // No explicit selection, check $ANDROID_SERIAL const char *env_serial = getenv("ANDROID_SERIAL"); if (env_serial) { LOGI("Using ANDROID_SERIAL: %s", env_serial); selector.type = SC_ADB_DEVICE_SELECT_SERIAL; selector.serial = env_serial; } else { selector.type = SC_ADB_DEVICE_SELECT_ALL; } } struct sc_adb_device device; ok = sc_adb_select_device(&server->intr, &selector, 0, &device); if (!ok) { goto error_connection_failed; } if (params->tcpip) { assert(!params->tcpip_dst); ok = sc_server_configure_tcpip_unknown_address(server, device.serial); sc_adb_device_destroy(&device); if (!ok) { goto error_connection_failed; } assert(server->serial); } else { // "move" the device.serial without copy server->serial = device.serial; // the serial must not be freed by the destructor device.serial = NULL; sc_adb_device_destroy(&device); } } else { ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); if (!ok) { goto error_connection_failed; } } const char *serial = server->serial; assert(serial); LOGD("Device serial: %s", serial); ok = push_server(&server->intr, serial); if (!ok) { goto error_connection_failed; } ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, 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, 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, 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); // Interrupt sockets to wake up socket blocking calls on the server assert(server->video_socket != SC_SOCKET_NONE); net_interrupt(server->video_socket); if (server->control_socket != SC_SOCKET_NONE) { // There is no control_socket if --no-control is set net_interrupt(server->control_socket); } // Give some delay for the server to terminate properly #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, "scrcpy-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) { if (server->video_socket != SC_SOCKET_NONE) { net_close(server->video_socket); } if (server->control_socket != SC_SOCKET_NONE) { net_close(server->control_socket); } free(server->serial); sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); } scrcpy-1.25/app/src/server.h000066400000000000000000000050731435104021100157660ustar00rootroot00000000000000#ifndef SERVER_H #define SERVER_H #include "common.h" #include #include #include #include "adb/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 *req_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 downsize_on_error; bool tcpip; const char *tcpip_dst; bool select_usb; bool select_tcpip; bool cleanup; bool power_on; }; struct sc_server { // The internal allocated strings are copies owned by the server struct sc_server_params params; char *serial; 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.25/app/src/sys/000077500000000000000000000000001435104021100151205ustar00rootroot00000000000000scrcpy-1.25/app/src/sys/unix/000077500000000000000000000000001435104021100161035ustar00rootroot00000000000000scrcpy-1.25/app/src/sys/unix/file.c000066400000000000000000000036131435104021100171710ustar00rootroot00000000000000#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.25/app/src/sys/unix/process.c000066400000000000000000000133611435104021100177310ustar00rootroot00000000000000#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]); } else { int devnull = open("/dev/null", O_RDONLY | O_CREAT, 0666); if (devnull != -1) { dup2(devnull, STDIN_FILENO); } else { LOGE("Could not open /dev/null for stdin"); } } if (pout) { if (out[1] != STDOUT_FILENO) { dup2(out[1], STDOUT_FILENO); close(out[1]); } close(out[0]); } else if (!inherit_stdout) { int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); if (devnull != -1) { dup2(devnull, STDOUT_FILENO); } else { LOGE("Could not open /dev/null for stdout"); } } if (perr) { if (err[1] != STDERR_FILENO) { dup2(err[1], STDERR_FILENO); close(err[1]); } close(err[0]); } else if (!inherit_stderr) { int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666); if (devnull != -1) { dup2(devnull, STDERR_FILENO); } else { LOGE("Could not open /dev/null for stderr"); } } close(internal[0]); enum sc_process_result err; // Somehow SDL masks many signals - undo them for other processes // https://github.com/libsdl-org/SDL/blob/release-2.0.18/src/thread/pthread/SDL_systhread.c#L167 sigset_t mask; sigemptyset(&mask); sigprocmask(SIG_SETMASK, &mask, NULL); if (fcntl(internal[1], F_SETFD, FD_CLOEXEC) == 0) { execvp(argv[0], (char *const *) argv); perror("exec"); err = errno == ENOENT ? SC_PROCESS_ERROR_MISSING_BINARY : SC_PROCESS_ERROR_GENERIC; } else { perror("fcntl"); 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) { LOGE("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.25/app/src/sys/win/000077500000000000000000000000001435104021100157155ustar00rootroot00000000000000scrcpy-1.25/app/src/sys/win/file.c000066400000000000000000000014731435104021100170050ustar00rootroot00000000000000#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.25/app/src/sys/win/process.c000066400000000000000000000160121435104021100175370ustar00rootroot00000000000000#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 || !!perr; enum sc_process_result ret = SC_PROCESS_ERROR_GENERIC; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; HANDLE stdin_read_handle; HANDLE stdout_write_handle; HANDLE stderr_write_handle; if (pin) { if (!CreatePipe(&stdin_read_handle, pin, &sa, 0)) { perror("pipe"); return SC_PROCESS_ERROR_GENERIC; } if (!SetHandleInformation(*pin, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation stdin failed"); goto error_close_stdin; } } if (pout) { if (!CreatePipe(pout, &stdout_write_handle, &sa, 0)) { perror("pipe"); goto error_close_stdin; } if (!SetHandleInformation(*pout, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation stdout failed"); goto error_close_stdout; } } if (perr) { if (!CreatePipe(perr, &stderr_write_handle, &sa, 0)) { perror("pipe"); goto error_close_stdout; } if (!SetHandleInformation(*perr, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation stderr failed"); goto error_close_stderr; } } STARTUPINFOEXW si; PROCESS_INFORMATION pi; memset(&si, 0, sizeof(si)); si.StartupInfo.cb = sizeof(si); HANDLE handles[3]; si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; if (inherit_stdout) { si.StartupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); } if (inherit_stderr) { si.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); } LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL; if (handle_count) { unsigned i = 0; if (pin) { si.StartupInfo.hStdInput = stdin_read_handle; handles[i++] = si.StartupInfo.hStdInput; } if (pout) { assert(!inherit_stdout); si.StartupInfo.hStdOutput = stdout_write_handle; handles[i++] = si.StartupInfo.hStdOutput; } if (perr) { assert(!inherit_stderr); si.StartupInfo.hStdError = stderr_write_handle; handles[i++] = si.StartupInfo.hStdError; } SIZE_T size; // Call it once to know the required buffer size BOOL ok = InitializeProcThreadAttributeList(NULL, 1, 0, &size) || GetLastError() == ERROR_INSUFFICIENT_BUFFER; if (!ok) { goto error_close_stderr; } lpAttributeList = malloc(size); if (!lpAttributeList) { LOG_OOM(); goto error_close_stderr; } ok = InitializeProcThreadAttributeList(lpAttributeList, 1, 0, &size); if (!ok) { free(lpAttributeList); goto error_close_stderr; } ok = UpdateProcThreadAttribute(lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles, handle_count * sizeof(HANDLE), NULL, NULL); if (!ok) { goto error_free_attribute_list; } si.lpAttributeList = lpAttributeList; } char *cmd = malloc(CMD_MAX_LEN); if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { 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 || inherit_stdout || inherit_stderr; DWORD dwCreationFlags = 0; if (handle_count > 0) { dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT; } if (!inherit_stdout && !inherit_stderr) { // DETACHED_PROCESS to disable stdin, stdout and stderr dwCreationFlags |= DETACHED_PROCESS; } BOOL ok = CreateProcessW(NULL, wide, NULL, NULL, bInheritHandles, dwCreationFlags, NULL, NULL, &si.StartupInfo, &pi); free(wide); if (!ok) { int err = GetLastError(); LOGE("CreateProcessW() error %d", err); if (err == ERROR_FILE_NOT_FOUND) { ret = SC_PROCESS_ERROR_MISSING_BINARY; } 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.25/app/src/trait/000077500000000000000000000000001435104021100154255ustar00rootroot00000000000000scrcpy-1.25/app/src/trait/frame_sink.h000066400000000000000000000007771435104021100177270ustar00rootroot00000000000000#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.25/app/src/trait/key_processor.h000066400000000000000000000026421435104021100204710ustar00rootroot00000000000000#ifndef SC_KEY_PROCESSOR_H #define SC_KEY_PROCESSOR_H #include "common.h" #include #include #include "input_events.h" /** * Key processor trait. * * Component able to process and inject keys should implement this trait. */ struct sc_key_processor { /** * Set by the implementation to indicate that it must explicitly wait for * the clipboard to be set on the device before injecting Ctrl+v to avoid * race conditions. If it is set, the input_manager will pass a valid * ack_to_wait to process_key() in case of clipboard synchronization * resulting of the key event. */ bool async_paste; const struct sc_key_processor_ops *ops; }; struct sc_key_processor_ops { /** * Process a keyboard event * * The `sequence` number (if different from `SC_SEQUENCE_INVALID`) indicates * the acknowledgement number to wait for before injecting this event. * This allows to ensure that the device clipboard is set before injecting * Ctrl+v on the device. * * This function is mandatory. */ void (*process_key)(struct sc_key_processor *kp, const struct sc_key_event *event, uint64_t ack_to_wait); /** * Process an input text * * This function is optional. */ void (*process_text)(struct sc_key_processor *kp, const struct sc_text_event *event); }; #endif scrcpy-1.25/app/src/trait/mouse_processor.h000066400000000000000000000027721435104021100210350ustar00rootroot00000000000000#ifndef SC_MOUSE_PROCESSOR_H #define SC_MOUSE_PROCESSOR_H #include "common.h" #include #include #include "input_events.h" /** * Mouse processor trait. * * Component able to process and inject mouse events should implement this * trait. */ struct sc_mouse_processor { const struct sc_mouse_processor_ops *ops; /** * If set, the mouse processor works in relative mode (the absolute * position is irrelevant). In particular, it indicates that the mouse * pointer must be "captured" by the UI. */ bool relative_mode; }; struct sc_mouse_processor_ops { /** * Process a mouse motion event * * This function is mandatory. */ void (*process_mouse_motion)(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event); /** * Process a mouse click event * * This function is mandatory. */ void (*process_mouse_click)(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event); /** * Process a mouse scroll event * * This function is optional. */ void (*process_mouse_scroll)(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event); /** * Process a touch event * * This function is optional. */ void (*process_touch)(struct sc_mouse_processor *mp, const struct sc_touch_event *event); }; #endif scrcpy-1.25/app/src/trait/packet_sink.h000066400000000000000000000011031435104021100200640ustar00rootroot00000000000000#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.25/app/src/usb/000077500000000000000000000000001435104021100150735ustar00rootroot00000000000000scrcpy-1.25/app/src/usb/aoa_hid.c000066400000000000000000000231011435104021100166200ustar00rootroot00000000000000#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); } bool sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb, struct sc_acksync *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; } aoa->stopped = false; aoa->acksync = acksync; aoa->usb = usb; 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); } 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) { LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); sc_usb_check_disconnected(aoa->usb, result); return false; } return true; } static bool sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, const 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) { LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); sc_usb_check_disconnected(aoa->usb, result); return false; } return true; } bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, const 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) { LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); sc_usb_check_disconnected(aoa->usb, result); return false; } return true; } bool sc_aoa_unregister_hid(struct sc_aoa *aoa, 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) { LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); sc_usb_check_disconnected(aoa->usb, result); return false; } return true; } bool sc_aoa_push_hid_event(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); // If some events have ack_to_wait set, then sc_aoa must have been // initialized with a non NULL acksync assert(aoa->acksync); // Do not block the loop indefinitely if the ack never comes (it // should never happen) sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500); enum sc_acksync_wait_result result = sc_acksync_wait(aoa->acksync, ack_to_wait, deadline); if (result == SC_ACKSYNC_WAIT_TIMEOUT) { LOGW("Ack not received after 500ms, discarding HID event"); 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, "scrcpy-aoa", aoa); if (!ok) { LOGE("Could not start AOA thread"); return false; } return true; } void sc_aoa_stop(struct sc_aoa *aoa) { sc_mutex_lock(&aoa->mutex); aoa->stopped = true; sc_cond_signal(&aoa->event_cond); sc_mutex_unlock(&aoa->mutex); if (aoa->acksync) { sc_acksync_interrupt(aoa->acksync); } } void sc_aoa_join(struct sc_aoa *aoa) { sc_thread_join(&aoa->thread, NULL); } scrcpy-1.25/app/src/usb/aoa_hid.h000066400000000000000000000025641435104021100166370ustar00rootroot00000000000000#ifndef SC_AOA_HID_H #define SC_AOA_HID_H #include #include #include #include "usb.h" #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 { struct sc_usb *usb; 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, struct sc_usb *usb, struct sc_acksync *acksync); void sc_aoa_destroy(struct sc_aoa *aoa); bool sc_aoa_start(struct sc_aoa *aoa); void sc_aoa_stop(struct sc_aoa *aoa); void sc_aoa_join(struct sc_aoa *aoa); bool sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, const 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.25/app/src/usb/hid_keyboard.c000066400000000000000000000321751435104021100176730ustar00rootroot00000000000000#include "hid_keyboard.h" #include #include "input_events.h" #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 }; /** * A keyboard HID event is 8 bytes long: * * - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys) * - byte 1: reserved (always 0) * - bytes 2 to 7: pressed keys (6 at most) * * 7 6 5 4 3 2 1 0 * +---------------+ * byte 0: |. . . . . . . .| modifiers * +---------------+ * ^ ^ ^ ^ ^ ^ ^ ^ * | | | | | | | `- left Ctrl * | | | | | | `--- left Shift * | | | | | `----- left Alt * | | | | `------- left Gui * | | | `--------- right Ctrl * | | `----------- right Shift * | `------------- right Alt * `--------------- right Gui * * +---------------+ * byte 1: |0 0 0 0 0 0 0 0| reserved * +---------------+ * * +---------------+ * bytes 2 to 7: |. . . . . . . .| scancode of 1st key pressed * +---------------+ * |. . . . . . . .| scancode of 2nd key pressed * +---------------+ * |. . . . . . . .| scancode of 3rd key pressed * +---------------+ * |. . . . . . . .| scancode of 4th key pressed * +---------------+ * |. . . . . . . .| scancode of 5th key pressed * +---------------+ * |. . . . . . . .| scancode of 6th key pressed * +---------------+ * * If there are less than 6 keys pressed, the last items are set to 0. * For example, if A and W are pressed: * * +---------------+ * bytes 2 to 7: |0 0 0 0 0 1 0 0| A is pressed (scancode = 4) * +---------------+ * |0 0 0 1 1 0 1 0| W is pressed (scancode = 26) * +---------------+ * |0 0 0 0 0 0 0 0| ^ * +---------------+ | only 2 keys are pressed, the * |0 0 0 0 0 0 0 0| | remaining items are set to 0 * +---------------+ | * |0 0 0 0 0 0 0 0| | * +---------------+ | * |0 0 0 0 0 0 0 0| v * +---------------+ * * Pressing more than 6 keys is not supported. If this happens (typically, * never in practice), report a "phantom state": * * +---------------+ * bytes 2 to 7: |0 0 0 0 0 0 0 1| ^ * +---------------+ | * |0 0 0 0 0 0 0 1| | more than 6 keys pressed: * +---------------+ | the list is filled with a special * |0 0 0 0 0 0 0 1| | rollover error code (0x01) * +---------------+ | * |0 0 0 0 0 0 0 1| | * +---------------+ | * |0 0 0 0 0 0 0 1| | * +---------------+ | * |0 0 0 0 0 0 0 1| v * +---------------+ */ static unsigned char sdl_keymod_to_hid_modifiers(uint16_t mod) { unsigned char modifiers = HID_MODIFIER_NONE; if (mod & SC_MOD_LCTRL) { modifiers |= HID_MODIFIER_LEFT_CONTROL; } if (mod & SC_MOD_LSHIFT) { modifiers |= HID_MODIFIER_LEFT_SHIFT; } if (mod & SC_MOD_LALT) { modifiers |= HID_MODIFIER_LEFT_ALT; } if (mod & SC_MOD_LGUI) { modifiers |= HID_MODIFIER_LEFT_GUI; } if (mod & SC_MOD_RCTRL) { modifiers |= HID_MODIFIER_RIGHT_CONTROL; } if (mod & SC_MOD_RSHIFT) { modifiers |= HID_MODIFIER_RIGHT_SHIFT; } if (mod & SC_MOD_RALT) { modifiers |= HID_MODIFIER_RIGHT_ALT; } if (mod & SC_MOD_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(enum sc_scancode scancode) { return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI; } static bool convert_hid_keyboard_event(struct sc_hid_keyboard *kb, struct sc_hid_event *hid_event, const struct sc_key_event *event) { enum sc_scancode scancode = event->scancode; assert(scancode >= 0); // SDL also generates events when only modifiers are pressed, we cannot // ignore them totally, for example press 'a' first then press 'Control', // if we ignore 'Control' event, only 'a' is sent. if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) { // Scancode to ignore return false; } 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->mods_state); if (scancode < SC_HID_KEYBOARD_KEYS) { // Pressed is true and released is false kb->keys[scancode] = (event->action == SC_ACTION_DOWN); 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) { // Phantom 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->action == SC_ACTION_DOWN ? "down" : "up", event->scancode, event->scancode, modifiers); return true; } static bool push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { bool capslock = mods_state & SC_MOD_CAPS; bool numlock = mods_state & SC_MOD_NUM; if (!capslock && !numlock) { // Nothing to do return true; } struct sc_hid_event hid_event; if (!sc_hid_keyboard_event_init(&hid_event)) { LOGW("Could not initialize HID keyboard event"); return false; } 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 not request HID event (mod lock state)"); return false; } LOGD("HID keyboard state synchronized"); return true; } static void sc_key_processor_process_key(struct sc_key_processor *kp, const struct sc_key_event *event, uint64_t ack_to_wait) { if (event->repeat) { // In USB HID protocol, key repeat is handled by the host (Android), so // just ignore key repeat here. return; } struct sc_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->mods_state)) { 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 not request HID event (key)"); } } } 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, // Never forward text input via HID (all the keys are injected // separately) .process_text = NULL, }; // Clipboard synchronization is requested over the control socket, while HID // events are sent over AOA, so it must wait for clipboard synchronization // to be acknowledged by the device before injecting Ctrl+v. kb->key_processor.async_paste = true; kb->key_processor.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.25/app/src/usb/hid_keyboard.h000066400000000000000000000026231435104021100176730ustar00rootroot00000000000000#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.25/app/src/usb/hid_mouse.c000066400000000000000000000167361435104021100172300ustar00rootroot00000000000000#include "hid_mouse.h" #include #include "input_events.h" #include "util/log.h" /** Downcast mouse processor to hid_mouse */ #define DOWNCAST(MP) container_of(MP, struct sc_hid_mouse, mouse_processor) #define HID_MOUSE_ACCESSORY_ID 2 // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position #define HID_MOUSE_EVENT_SIZE 4 /** * Mouse descriptor from the specification: * * * Appendix E (p71): §E.10 Report Descriptor (Mouse) * * The usage tags (like Wheel) are listed in "HID Usage Tables": * * §4 Generic Desktop Page (0x01) (p26) */ static const unsigned char mouse_report_desc[] = { // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (Mouse) 0x09, 0x02, // Collection (Application) 0xA1, 0x01, // Usage (Pointer) 0x09, 0x01, // Collection (Physical) 0xA1, 0x00, // Usage Page (Buttons) 0x05, 0x09, // Usage Minimum (1) 0x19, 0x01, // Usage Maximum (5) 0x29, 0x05, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (1) 0x25, 0x01, // Report Count (5) 0x95, 0x05, // Report Size (1) 0x75, 0x01, // Input (Data, Variable, Absolute): 5 buttons bits 0x81, 0x02, // Report Count (1) 0x95, 0x01, // Report Size (3) 0x75, 0x03, // Input (Constant): 3 bits padding 0x81, 0x01, // Usage Page (Generic Desktop) 0x05, 0x01, // Usage (X) 0x09, 0x30, // Usage (Y) 0x09, 0x31, // Usage (Wheel) 0x09, 0x38, // Local Minimum (-127) 0x15, 0x81, // Local Maximum (127) 0x25, 0x7F, // Report Size (8) 0x75, 0x08, // Report Count (3) 0x95, 0x03, // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) 0x81, 0x06, // End Collection 0xC0, // End Collection 0xC0, }; /** * A mouse HID event is 3 bytes long: * * - byte 0: buttons state * - byte 1: relative x motion (signed byte from -127 to 127) * - byte 2: relative y motion (signed byte from -127 to 127) * * 7 6 5 4 3 2 1 0 * +---------------+ * byte 0: |0 0 0 . . . . .| buttons state * +---------------+ * ^ ^ ^ ^ ^ * | | | | `- left button * | | | `--- right button * | | `----- middle button * | `------- button 4 * `--------- button 5 * * +---------------+ * byte 1: |. . . . . . . .| relative x motion * +---------------+ * byte 2: |. . . . . . . .| relative y motion * +---------------+ * byte 3: |. . . . . . . .| wheel motion (-1, 0 or 1) * +---------------+ * * As an example, here is the report for a motion of (x=5, y=-4) with left * button pressed: * * +---------------+ * |0 0 0 0 0 0 0 1| left button pressed * +---------------+ * |0 0 0 0 0 1 0 1| horizontal motion (x = 5) * +---------------+ * |1 1 1 1 1 1 0 0| relative y motion (y = -4) * +---------------+ * |0 0 0 0 0 0 0 0| wheel motion * +---------------+ */ static bool sc_hid_mouse_event_init(struct sc_hid_event *hid_event) { unsigned char *buffer = calloc(1, HID_MOUSE_EVENT_SIZE); if (!buffer) { LOG_OOM(); return false; } sc_hid_event_init(hid_event, HID_MOUSE_ACCESSORY_ID, buffer, HID_MOUSE_EVENT_SIZE); return true; } static unsigned char buttons_state_to_hid_buttons(uint8_t buttons_state) { unsigned char c = 0; if (buttons_state & SC_MOUSE_BUTTON_LEFT) { c |= 1 << 0; } if (buttons_state & SC_MOUSE_BUTTON_RIGHT) { c |= 1 << 1; } if (buttons_state & SC_MOUSE_BUTTON_MIDDLE) { c |= 1 << 2; } if (buttons_state & SC_MOUSE_BUTTON_X1) { c |= 1 << 3; } if (buttons_state & SC_MOUSE_BUTTON_X2) { c |= 1 << 4; } return c; } static void sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp, const struct sc_mouse_motion_event *event) { struct sc_hid_mouse *mouse = DOWNCAST(mp); struct sc_hid_event hid_event; if (!sc_hid_mouse_event_init(&hid_event)) { return; } unsigned char *buffer = hid_event.buffer; buffer[0] = buttons_state_to_hid_buttons(event->buttons_state); buffer[1] = CLAMP(event->xrel, -127, 127); buffer[2] = CLAMP(event->yrel, -127, 127); buffer[3] = 0; // wheel coordinates only used for scrolling if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mouse motion)"); } } static void sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, const struct sc_mouse_click_event *event) { struct sc_hid_mouse *mouse = DOWNCAST(mp); struct sc_hid_event hid_event; if (!sc_hid_mouse_event_init(&hid_event)) { return; } unsigned char *buffer = hid_event.buffer; buffer[0] = buttons_state_to_hid_buttons(event->buttons_state); buffer[1] = 0; // no x motion buffer[2] = 0; // no y motion buffer[3] = 0; // wheel coordinates only used for scrolling if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mouse click)"); } } static void sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, const struct sc_mouse_scroll_event *event) { struct sc_hid_mouse *mouse = DOWNCAST(mp); struct sc_hid_event hid_event; if (!sc_hid_mouse_event_init(&hid_event)) { return; } unsigned char *buffer = hid_event.buffer; buffer[0] = 0; // buttons state irrelevant (and unknown) buffer[1] = 0; // no x motion buffer[2] = 0; // no y motion // In practice, vscroll is always -1, 0 or 1, but in theory other values // are possible buffer[3] = CLAMP(event->vscroll, -127, 127); // Horizontal scrolling ignored if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) { sc_hid_event_destroy(&hid_event); LOGW("Could not request HID event (mouse scroll)"); } } bool sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa) { mouse->aoa = aoa; bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, mouse_report_desc, ARRAY_LEN(mouse_report_desc)); if (!ok) { LOGW("Register HID mouse failed"); return false; } static const struct sc_mouse_processor_ops ops = { .process_mouse_motion = sc_mouse_processor_process_mouse_motion, .process_mouse_click = sc_mouse_processor_process_mouse_click, .process_mouse_scroll = sc_mouse_processor_process_mouse_scroll, // Touch events not supported (coordinates are not relative) .process_touch = NULL, }; mouse->mouse_processor.ops = &ops; mouse->mouse_processor.relative_mode = true; return true; } void sc_hid_mouse_destroy(struct sc_hid_mouse *mouse) { bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID); if (!ok) { LOGW("Could not unregister HID mouse"); } } scrcpy-1.25/app/src/usb/hid_mouse.h000066400000000000000000000006261435104021100172240ustar00rootroot00000000000000#ifndef SC_HID_MOUSE_H #define SC_HID_MOUSE_H #include "common.h" #include #include "aoa_hid.h" #include "trait/mouse_processor.h" struct sc_hid_mouse { struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_aoa *aoa; }; bool sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa); void sc_hid_mouse_destroy(struct sc_hid_mouse *mouse); #endif scrcpy-1.25/app/src/usb/scrcpy_otg.c000066400000000000000000000125411435104021100174160ustar00rootroot00000000000000#include "scrcpy_otg.h" #include #include "adb/adb.h" #include "events.h" #include "screen_otg.h" #include "util/log.h" struct scrcpy_otg { struct sc_usb usb; struct sc_aoa aoa; struct sc_hid_keyboard keyboard; struct sc_hid_mouse mouse; struct sc_screen_otg screen_otg; }; static void sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) { (void) usb; (void) userdata; SDL_Event event; event.type = EVENT_USB_DEVICE_DISCONNECTED; int ret = SDL_PushEvent(&event); if (ret < 0) { LOGE("Could not post USB disconnection event: %s", SDL_GetError()); } } static enum scrcpy_exit_code event_loop(struct scrcpy_otg *s) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { case EVENT_USB_DEVICE_DISCONNECTED: LOGW("Device disconnected"); return SCRCPY_EXIT_DISCONNECTED; case SDL_QUIT: LOGD("User requested to quit"); return SCRCPY_EXIT_SUCCESS; default: sc_screen_otg_handle_event(&s->screen_otg, &event); break; } } return SCRCPY_EXIT_FAILURE; } enum scrcpy_exit_code scrcpy_otg(struct scrcpy_options *options) { static struct scrcpy_otg scrcpy_otg; struct scrcpy_otg *s = &scrcpy_otg; const char *serial = options->serial; if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); } // Minimal SDL initialization if (SDL_Init(SDL_INIT_EVENTS)) { LOGE("Could not initialize SDL: %s", SDL_GetError()); return false; } atexit(SDL_Quit); if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) { LOGW("Could not enable mouse focus clickthrough"); } enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; struct sc_hid_keyboard *keyboard = NULL; struct sc_hid_mouse *mouse = NULL; bool usb_device_initialized = false; bool usb_connected = false; bool aoa_started = false; bool aoa_initialized = false; #ifdef _WIN32 // On Windows, only one process could open a USB device // LOGI("Killing adb daemon (if any)..."); unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; // uninterruptible (intr == NULL), but in practice it's very quick sc_adb_kill_server(NULL, flags); #endif static const struct sc_usb_callbacks cbs = { .on_disconnected = sc_usb_on_disconnected, }; bool ok = sc_usb_init(&s->usb); if (!ok) { return SCRCPY_EXIT_FAILURE; } struct sc_usb_device usb_device; ok = sc_usb_select_device(&s->usb, serial, &usb_device); if (!ok) { goto end; } usb_device_initialized = true; LOGI("USB device: %s (%04x:%04x) %s %s", usb_device.serial, (unsigned) usb_device.vid, (unsigned) usb_device.pid, usb_device.manufacturer, usb_device.product); ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); if (!ok) { goto end; } usb_connected = true; ok = sc_aoa_init(&s->aoa, &s->usb, NULL); if (!ok) { goto end; } aoa_initialized = true; bool enable_keyboard = options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; bool enable_mouse = options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; // If neither --hid-keyboard or --hid-mouse is passed, enable both if (!enable_keyboard && !enable_mouse) { enable_keyboard = true; enable_mouse = true; } if (enable_keyboard) { ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); if (!ok) { goto end; } keyboard = &s->keyboard; } if (enable_mouse) { ok = sc_hid_mouse_init(&s->mouse, &s->aoa); if (!ok) { goto end; } mouse = &s->mouse; } ok = sc_aoa_start(&s->aoa); if (!ok) { goto end; } aoa_started = true; const char *window_title = options->window_title; if (!window_title) { window_title = usb_device.product ? usb_device.product : "scrcpy"; } struct sc_screen_otg_params params = { .keyboard = keyboard, .mouse = mouse, .window_title = window_title, .always_on_top = options->always_on_top, .window_x = options->window_x, .window_y = options->window_y, .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, }; ok = sc_screen_otg_init(&s->screen_otg, ¶ms); if (!ok) { goto end; } // usb_device not needed anymore sc_usb_device_destroy(&usb_device); usb_device_initialized = false; ret = event_loop(s); LOGD("quit..."); end: if (aoa_started) { sc_aoa_stop(&s->aoa); } sc_usb_stop(&s->usb); if (mouse) { sc_hid_mouse_destroy(&s->mouse); } if (keyboard) { sc_hid_keyboard_destroy(&s->keyboard); } if (aoa_initialized) { sc_aoa_join(&s->aoa); sc_aoa_destroy(&s->aoa); } sc_usb_join(&s->usb); if (usb_connected) { sc_usb_disconnect(&s->usb); } if (usb_device_initialized) { sc_usb_device_destroy(&usb_device); } sc_usb_destroy(&s->usb); return ret; } scrcpy-1.25/app/src/usb/scrcpy_otg.h000066400000000000000000000002641435104021100174220ustar00rootroot00000000000000#ifndef SCRCPY_OTG_H #define SCRCPY_OTG_H #include "common.h" #include "options.h" #include "scrcpy.h" enum scrcpy_exit_code scrcpy_otg(struct scrcpy_options *options); #endif scrcpy-1.25/app/src/usb/screen_otg.c000066400000000000000000000233161435104021100173740ustar00rootroot00000000000000#include "screen_otg.h" #include "icon.h" #include "options.h" #include "util/log.h" static void sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { #ifdef __APPLE__ // Workaround for SDL bug on macOS: // if (capture) { int mouse_x, mouse_y; SDL_GetGlobalMouseState(&mouse_x, &mouse_y); int x, y, w, h; SDL_GetWindowPosition(screen->window, &x, &y); SDL_GetWindowSize(screen->window, &w, &h); bool outside_window = mouse_x < x || mouse_x >= x + w || mouse_y < y || mouse_y >= y + h; if (outside_window) { SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); } } #else (void) screen; #endif if (SDL_SetRelativeMouseMode(capture)) { LOGE("Could not set relative mouse mode to %s: %s", capture ? "true" : "false", SDL_GetError()); } } static inline bool sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) { (void) screen; return SDL_GetRelativeMouseMode(); } static inline void sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) { (void) screen; bool new_value = !sc_screen_otg_get_mouse_capture(screen); sc_screen_otg_set_mouse_capture(screen, new_value); } static void sc_screen_otg_render(struct sc_screen_otg *screen) { SDL_RenderClear(screen->renderer); if (screen->texture) { SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL); } SDL_RenderPresent(screen->renderer); } bool sc_screen_otg_init(struct sc_screen_otg *screen, const struct sc_screen_otg_params *params) { screen->keyboard = params->keyboard; screen->mouse = params->mouse; screen->mouse_capture_key_pressed = 0; const char *title = params->window_title; assert(title); int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; int width = params->window_width ? params->window_width : 256; int height = params->window_height ? params->window_height : 256; uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI; if (params->always_on_top) { window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; } if (params->window_borderless) { window_flags |= SDL_WINDOW_BORDERLESS; } screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags); if (!screen->window) { LOGE("Could not create window: %s", SDL_GetError()); return false; } screen->renderer = SDL_CreateRenderer(screen->window, -1, 0); if (!screen->renderer) { LOGE("Could not create renderer: %s", SDL_GetError()); goto error_destroy_window; } SDL_Surface *icon = scrcpy_icon_load(); if (icon) { SDL_SetWindowIcon(screen->window, icon); if (SDL_RenderSetLogicalSize(screen->renderer, icon->w, icon->h)) { LOGW("Could not set renderer logical size: %s", SDL_GetError()); // don't fail } screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon); scrcpy_icon_destroy(icon); if (!screen->texture) { goto error_destroy_renderer; } } else { screen->texture = NULL; LOGW("Could not load icon"); } if (screen->mouse) { // Capture mouse on start sc_screen_otg_set_mouse_capture(screen, true); } return true; error_destroy_window: SDL_DestroyWindow(screen->window); error_destroy_renderer: SDL_DestroyRenderer(screen->renderer); return false; } void sc_screen_otg_destroy(struct sc_screen_otg *screen) { if (screen->texture) { SDL_DestroyTexture(screen->texture); } SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); } static inline bool sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) { return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; } static void sc_screen_otg_process_key(struct sc_screen_otg *screen, const SDL_KeyboardEvent *event) { assert(screen->keyboard); struct sc_key_processor *kp = &screen->keyboard->key_processor; struct sc_key_event evt = { .action = sc_action_from_sdl_keyboard_type(event->type), .keycode = sc_keycode_from_sdl(event->keysym.sym), .scancode = sc_scancode_from_sdl(event->keysym.scancode), .repeat = event->repeat, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), }; assert(kp->ops->process_key); kp->ops->process_key(kp, &evt, SC_SEQUENCE_INVALID); } static void sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen, const SDL_MouseMotionEvent *event) { assert(screen->mouse); struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; struct sc_mouse_motion_event evt = { // .position not used for HID events .xrel = event->xrel, .yrel = event->yrel, .buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true), }; assert(mp->ops->process_mouse_motion); mp->ops->process_mouse_motion(mp, &evt); } static void sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen, const SDL_MouseButtonEvent *event) { assert(screen->mouse); struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); struct sc_mouse_click_event evt = { // .position not used for HID events .action = sc_action_from_sdl_mousebutton_type(event->type), .button = sc_mouse_button_from_sdl(event->button), .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), }; assert(mp->ops->process_mouse_click); mp->ops->process_mouse_click(mp, &evt); } static void sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, const SDL_MouseWheelEvent *event) { assert(screen->mouse); struct sc_mouse_processor *mp = &screen->mouse->mouse_processor; uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); struct sc_mouse_scroll_event evt = { // .position not used for HID events .hscroll = event->x, .vscroll = event->y, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true), }; assert(mp->ops->process_mouse_scroll); mp->ops->process_mouse_scroll(mp, &evt); } void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { switch (event->type) { case SDL_WINDOWEVENT: switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: sc_screen_otg_render(screen); break; case SDL_WINDOWEVENT_FOCUS_LOST: if (screen->mouse) { sc_screen_otg_set_mouse_capture(screen, false); } break; } return; case SDL_KEYDOWN: if (screen->mouse) { SDL_Keycode key = event->key.keysym.sym; if (sc_screen_otg_is_mouse_capture_key(key)) { if (!screen->mouse_capture_key_pressed) { screen->mouse_capture_key_pressed = key; } else { // Another mouse capture key has been pressed, cancel // mouse (un)capture screen->mouse_capture_key_pressed = 0; } // Mouse capture keys are never forwarded to the device return; } } if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_KEYUP: if (screen->mouse) { SDL_Keycode key = event->key.keysym.sym; SDL_Keycode cap = screen->mouse_capture_key_pressed; screen->mouse_capture_key_pressed = 0; if (sc_screen_otg_is_mouse_capture_key(key)) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode sc_screen_otg_toggle_mouse_capture(screen); } // Mouse capture keys are never forwarded to the device return; } } if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_MOUSEMOTION: if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: if (screen->mouse) { if (sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_button(screen, &event->button); } else { sc_screen_otg_set_mouse_capture(screen, true); } } break; case SDL_MOUSEWHEEL: if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; } } scrcpy-1.25/app/src/usb/screen_otg.h000066400000000000000000000020441435104021100173740ustar00rootroot00000000000000#ifndef SC_SCREEN_OTG_H #define SC_SCREEN_OTG_H #include "common.h" #include #include #include "hid_keyboard.h" #include "hid_mouse.h" struct sc_screen_otg { struct sc_hid_keyboard *keyboard; struct sc_hid_mouse *mouse; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; // See equivalent mechanism in screen.h SDL_Keycode mouse_capture_key_pressed; }; struct sc_screen_otg_params { struct sc_hid_keyboard *keyboard; struct sc_hid_mouse *mouse; const char *window_title; bool always_on_top; int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED uint16_t window_width; uint16_t window_height; bool window_borderless; }; bool sc_screen_otg_init(struct sc_screen_otg *screen, const struct sc_screen_otg_params *params); void sc_screen_otg_destroy(struct sc_screen_otg *screen); void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event); #endif scrcpy-1.25/app/src/usb/usb.c000066400000000000000000000261341435104021100160360ustar00rootroot00000000000000#include "usb.h" #include #include "util/log.h" #include "util/vector.h" struct sc_vec_usb_devices SC_VECTOR(struct sc_usb_device); static char * read_string(libusb_device_handle *handle, uint8_t desc_index) { char buffer[128]; int result = libusb_get_string_descriptor_ascii(handle, desc_index, (unsigned char *) buffer, sizeof(buffer)); if (result < 0) { LOGD("Read string: libusb error: %s", libusb_strerror(result)); return NULL; } assert((size_t) result <= sizeof(buffer)); // When non-negative, 'result' contains the number of bytes written char *s = malloc(result + 1); if (!s) { LOG_OOM(); return NULL; } memcpy(s, buffer, result); s[result] = '\0'; return s; } static bool sc_usb_read_device(libusb_device *device, struct sc_usb_device *out) { // Do not log any USB error in this function, it is expected that many USB // devices available on the computer have permission restrictions struct libusb_device_descriptor desc; int result = libusb_get_device_descriptor(device, &desc); if (result < 0 || !desc.iSerialNumber) { return false; } libusb_device_handle *handle; result = libusb_open(device, &handle); if (result < 0) { // Log at debug level because it is expected that some non-Android USB // devices present on the computer require special permissions LOGD("Open USB device %04x:%04x: libusb error: %s", (unsigned) desc.idVendor, (unsigned) desc.idProduct, libusb_strerror(result)); return false; } char *device_serial = read_string(handle, desc.iSerialNumber); if (!device_serial) { libusb_close(handle); return false; } out->device = libusb_ref_device(device); out->serial = device_serial; out->vid = desc.idVendor; out->pid = desc.idProduct; out->manufacturer = read_string(handle, desc.iManufacturer); out->product = read_string(handle, desc.iProduct); out->selected = false; libusb_close(handle); return true; } void sc_usb_device_destroy(struct sc_usb_device *usb_device) { if (usb_device->device) { libusb_unref_device(usb_device->device); } free(usb_device->serial); free(usb_device->manufacturer); free(usb_device->product); } void sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) { *dst = *src; src->device = NULL; src->serial = NULL; src->manufacturer = NULL; src->product = NULL; } void sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) { for (size_t i = 0; i < usb_devices->size; ++i) { sc_usb_device_destroy(&usb_devices->data[i]); } sc_vector_destroy(usb_devices); } static bool sc_usb_list_devices(struct sc_usb *usb, struct sc_vec_usb_devices *out_vec) { libusb_device **list; ssize_t count = libusb_get_device_list(usb->context, &list); if (count < 0) { LOGE("List USB devices: libusb error: %s", libusb_strerror(count)); return false; } for (size_t i = 0; i < (size_t) count; ++i) { libusb_device *device = list[i]; struct sc_usb_device usb_device; if (sc_usb_read_device(device, &usb_device)) { bool ok = sc_vector_push(out_vec, usb_device); if (!ok) { LOG_OOM(); LOGE("Could not push usb_device to vector"); sc_usb_device_destroy(&usb_device); // continue anyway } } } libusb_free_device_list(list, 1); return true; } static bool sc_usb_accept_device(const struct sc_usb_device *device, const char *serial) { if (!serial) { return true; } return !strcmp(serial, device->serial); } static size_t sc_usb_devices_select(struct sc_usb_device *devices, size_t len, const char *serial, size_t *idx_out) { size_t count = 0; for (size_t i = 0; i < len; ++i) { struct sc_usb_device *device = &devices[i]; device->selected = sc_usb_accept_device(device, serial); if (device->selected) { if (idx_out && !count) { *idx_out = i; } ++count; } } return count; } static void sc_usb_devices_log(enum sc_log_level level, struct sc_usb_device *devices, size_t count) { for (size_t i = 0; i < count; ++i) { struct sc_usb_device *d = &devices[i]; const char *selection = d->selected ? "-->" : " "; // Convert uint16_t to unsigned because PRIx16 may not exist on Windows LOG(level, " %s %-18s (%04x:%04x) %s %s", selection, d->serial, (unsigned) d->vid, (unsigned) d->pid, d->manufacturer, d->product); } } bool sc_usb_select_device(struct sc_usb *usb, const char *serial, struct sc_usb_device *out_device) { struct sc_vec_usb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_usb_list_devices(usb, &vec); if (!ok) { LOGE("Could not list USB devices"); return false; } if (vec.size == 0) { LOGE("Could not find any USB device"); return false; } size_t sel_idx; // index of the single matching device if sel_count == 1 size_t sel_count = sc_usb_devices_select(vec.data, vec.size, serial, &sel_idx); if (sel_count == 0) { // if count > 0 && sel_count == 0, then necessarily a serial is provided assert(serial); LOGE("Could not find USB device %s", serial); sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); sc_usb_devices_destroy(&vec); return false; } if (sel_count > 1) { if (serial) { LOGE("Multiple (%" SC_PRIsizet ") USB devices with serial %s:", sel_count, serial); } else { LOGE("Multiple (%" SC_PRIsizet ") USB devices:", sel_count); } sc_usb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size); LOGE("Select a device via -s (--serial)"); sc_usb_devices_destroy(&vec); return false; } assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 struct sc_usb_device *device = &vec.data[sel_idx]; LOGD("USB device found:"); sc_usb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); // Move device into out_device (do not destroy device) sc_usb_device_move(out_device, device); sc_usb_devices_destroy(&vec); return true; } bool sc_usb_init(struct sc_usb *usb) { usb->handle = NULL; return libusb_init(&usb->context) == LIBUSB_SUCCESS; } void sc_usb_destroy(struct sc_usb *usb) { libusb_exit(usb->context); } static void sc_usb_report_disconnected(struct sc_usb *usb) { if (usb->cbs && !atomic_flag_test_and_set(&usb->disconnection_notified)) { assert(usb->cbs && usb->cbs->on_disconnected); usb->cbs->on_disconnected(usb, usb->cbs_userdata); } } bool sc_usb_check_disconnected(struct sc_usb *usb, int result) { if (result == LIBUSB_ERROR_NO_DEVICE || result == LIBUSB_ERROR_NOT_FOUND) { sc_usb_report_disconnected(usb); return false; } return true; } static LIBUSB_CALL int sc_usb_libusb_callback(libusb_context *ctx, libusb_device *device, libusb_hotplug_event event, void *userdata) { (void) ctx; (void) device; (void) event; struct sc_usb *usb = userdata; libusb_device *dev = libusb_get_device(usb->handle); assert(dev); if (dev != device) { // Not the connected device return 0; } sc_usb_report_disconnected(usb); // Do not automatically deregister the callback by returning 1. Instead, // manually deregister to interrupt libusb_handle_events() from the libusb // event thread: return 0; } static int run_libusb_event_handler(void *data) { struct sc_usb *usb = data; while (!atomic_load(&usb->stopped)) { // Interrupted by events or by libusb_hotplug_deregister_callback() libusb_handle_events(usb->context); } return 0; } static bool sc_usb_register_callback(struct sc_usb *usb) { if (!libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) { LOGW("On this platform, libusb does not have hotplug capability; " "device disconnection will not be detected properly"); return false; } libusb_device *device = libusb_get_device(usb->handle); assert(device); struct libusb_device_descriptor desc; int result = libusb_get_device_descriptor(device, &desc); if (result < 0) { LOGE("Device descriptor: libusb error: %s", libusb_strerror(result)); return false; } int events = LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; int flags = LIBUSB_HOTPLUG_NO_FLAGS; int vendor_id = desc.idVendor; int product_id = desc.idProduct; int dev_class = LIBUSB_HOTPLUG_MATCH_ANY; result = libusb_hotplug_register_callback(usb->context, events, flags, vendor_id, product_id, dev_class, sc_usb_libusb_callback, usb, &usb->callback_handle); if (result < 0) { LOGE("Register hotplog callback: libusb error: %s", libusb_strerror(result)); return false; } usb->has_callback_handle = true; return true; } bool sc_usb_connect(struct sc_usb *usb, libusb_device *device, const struct sc_usb_callbacks *cbs, void *cbs_userdata) { int result = libusb_open(device, &usb->handle); if (result < 0) { LOGE("Open USB device: libusb error: %s", libusb_strerror(result)); return false; } usb->has_callback_handle = false; usb->has_libusb_event_thread = false; // If cbs is set, then cbs->on_disconnected must be set assert(!cbs || cbs->on_disconnected); usb->cbs = cbs; usb->cbs_userdata = cbs_userdata; if (cbs) { atomic_init(&usb->stopped, false); usb->disconnection_notified = (atomic_flag) ATOMIC_FLAG_INIT; if (sc_usb_register_callback(usb)) { // Create a thread to process libusb events, so that device // disconnection could be detected immediately usb->has_libusb_event_thread = sc_thread_create(&usb->libusb_event_thread, run_libusb_event_handler, "scrcpy-usbev", usb); if (!usb->has_libusb_event_thread) { LOGW("Libusb event thread handler could not be created, USB " "device disconnection might not be detected immediately"); } } } return true; } void sc_usb_disconnect(struct sc_usb *usb) { libusb_close(usb->handle); } void sc_usb_stop(struct sc_usb *usb) { if (usb->has_callback_handle) { atomic_store(&usb->stopped, true); libusb_hotplug_deregister_callback(usb->context, usb->callback_handle); } } void sc_usb_join(struct sc_usb *usb) { if (usb->has_libusb_event_thread) { sc_thread_join(&usb->libusb_event_thread, NULL); } } scrcpy-1.25/app/src/usb/usb.h000066400000000000000000000036011435104021100160350ustar00rootroot00000000000000#ifndef SC_USB_H #define SC_USB_H #include "common.h" #include #include #include "util/thread.h" struct sc_usb { libusb_context *context; libusb_device_handle *handle; const struct sc_usb_callbacks *cbs; void *cbs_userdata; bool has_callback_handle; libusb_hotplug_callback_handle callback_handle; bool has_libusb_event_thread; sc_thread libusb_event_thread; atomic_bool stopped; // only used if cbs != NULL atomic_flag disconnection_notified; }; struct sc_usb_callbacks { void (*on_disconnected)(struct sc_usb *usb, void *userdata); }; struct sc_usb_device { libusb_device *device; char *serial; char *manufacturer; char *product; uint16_t vid; uint16_t pid; bool selected; }; void sc_usb_device_destroy(struct sc_usb_device *usb_device); /** * Move src to dst * * After this call, the content of src is undefined, except that * sc_usb_device_destroy() can be called. * * This is useful to take a device from a list that will be destroyed, without * making unnecessary copies. */ void sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src); void sc_usb_devices_destroy_all(struct sc_usb_device *usb_devices, size_t count); bool sc_usb_init(struct sc_usb *usb); void sc_usb_destroy(struct sc_usb *usb); bool sc_usb_select_device(struct sc_usb *usb, const char *serial, struct sc_usb_device *out_device); bool sc_usb_connect(struct sc_usb *usb, libusb_device *device, const struct sc_usb_callbacks *cbs, void *cbs_userdata); void sc_usb_disconnect(struct sc_usb *usb); // A client should call this function with the return value of a libusb call // to detect disconnection immediately bool sc_usb_check_disconnected(struct sc_usb *usb, int result); void sc_usb_stop(struct sc_usb *usb); void sc_usb_join(struct sc_usb *usb); #endif scrcpy-1.25/app/src/util/000077500000000000000000000000001435104021100152575ustar00rootroot00000000000000scrcpy-1.25/app/src/util/acksync.c000066400000000000000000000030701435104021100170560ustar00rootroot00000000000000#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.25/app/src/util/acksync.h000066400000000000000000000025511435104021100170660ustar00rootroot00000000000000#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.25/app/src/util/binary.h000066400000000000000000000031701435104021100167150ustar00rootroot00000000000000#ifndef SC_BINARY_H #define SC_BINARY_H #include "common.h" #include #include #include static inline void sc_write16be(uint8_t *buf, uint16_t value) { buf[0] = value >> 8; buf[1] = value; } static inline void sc_write32be(uint8_t *buf, uint32_t value) { buf[0] = value >> 24; buf[1] = value >> 16; buf[2] = value >> 8; buf[3] = value; } static inline void sc_write64be(uint8_t *buf, uint64_t value) { sc_write32be(buf, value >> 32); sc_write32be(&buf[4], (uint32_t) value); } static inline uint16_t sc_read16be(const uint8_t *buf) { return (buf[0] << 8) | buf[1]; } static inline uint32_t sc_read32be(const uint8_t *buf) { return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; } static inline uint64_t sc_read64be(const uint8_t *buf) { uint32_t msb = sc_read32be(buf); uint32_t lsb = sc_read32be(&buf[4]); return ((uint64_t) msb << 32) | lsb; } /** * Convert a float between 0 and 1 to an unsigned 16-bit fixed-point value */ static inline uint16_t sc_float_to_u16fp(float f) { assert(f >= 0.0f && f <= 1.0f); uint32_t u = f * 0x1p16f; // 2^16 if (u >= 0xffff) { assert(u == 0x10000); // for f == 1.0f u = 0xffff; } return (uint16_t) u; } /** * Convert a float between -1 and 1 to a signed 16-bit fixed-point value */ static inline int16_t sc_float_to_i16fp(float f) { assert(f >= -1.0f && f <= 1.0f); int32_t i = f * 0x1p15f; // 2^15 assert(i >= -0x8000); if (i >= 0x7fff) { assert(i == 0x8000); // for f == 1.0f i = 0x7fff; } return (int16_t) i; } #endif scrcpy-1.25/app/src/util/cbuf.h000066400000000000000000000023501435104021100163470ustar00rootroot00000000000000// generic circular buffer (bounded queue) implementation #ifndef SC_CBUF_H #define SC_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.25/app/src/util/file.c000066400000000000000000000023341435104021100163440ustar00rootroot00000000000000#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.25/app/src/util/file.h000066400000000000000000000017201435104021100163470ustar00rootroot00000000000000#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.25/app/src/util/intmap.c000066400000000000000000000005411435104021100167130ustar00rootroot00000000000000#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.25/app/src/util/intmap.h000066400000000000000000000010021435104021100167110ustar00rootroot00000000000000#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.25/app/src/util/intr.c000066400000000000000000000036341435104021100164050ustar00rootroot00000000000000#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.25/app/src/util/intr.h000066400000000000000000000030541435104021100164060ustar00rootroot00000000000000#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.25/app/src/util/log.c000066400000000000000000000045321435104021100162100ustar00rootroot00000000000000#include "log.h" #if _WIN32 # include #endif #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); } void sc_log(enum sc_log_level level, const char *fmt, ...) { SDL_LogPriority sdl_level = log_level_sc_to_sdl(level); va_list ap; va_start(ap, fmt); SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, sdl_level, fmt, ap); va_end(ap); } #ifdef _WIN32 bool sc_log_windows_error(const char *prefix, int error) { assert(prefix); char *message; DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM; DWORD lang_id = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); int ret = FormatMessage(flags, NULL, error, lang_id, (char *) &message, 0, NULL); if (ret <= 0) { return false; } // Note: message already contains a trailing '\n' LOGE("%s: [%d] %s", prefix, error, message); LocalFree(message); return true; } #endif scrcpy-1.25/app/src/util/log.h000066400000000000000000000017301435104021100162120ustar00rootroot00000000000000#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 LOG_OOM() \ LOGE("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); void sc_log(enum sc_log_level level, const char *fmt, ...); #define LOG(LEVEL, ...) sc_log((LEVEL), __VA_ARGS__) #ifdef _WIN32 // Log system error (typically returned by GetLastError() or similar) bool sc_log_windows_error(const char *prefix, int error); #endif #endif scrcpy-1.25/app/src/util/net.c000066400000000000000000000141731435104021100162170ustar00rootroot00000000000000#include "net.h" #include #include #include #include "log.h" #ifdef _WIN32 # include typedef int socklen_t; typedef SOCKET sc_raw_socket; # define SC_RAW_SOCKET_NONE INVALID_SOCKET #else # include # include # include # include # include # include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; typedef int sc_raw_socket; # define SC_RAW_SOCKET_NONE -1 #endif bool net_init(void) { #ifdef _WIN32 WSADATA wsa; int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0; if (res < 0) { LOGE("WSAStartup failed with error %d", res); return false; } #endif return true; } void net_cleanup(void) { #ifdef _WIN32 WSACleanup(); #endif } static inline sc_socket wrap(sc_raw_socket sock) { #ifdef _WIN32 if (sock == INVALID_SOCKET) { return SC_SOCKET_NONE; } struct sc_socket_windows *socket = malloc(sizeof(*socket)); if (!socket) { LOG_OOM(); closesocket(sock); return SC_SOCKET_NONE; } socket->socket = sock; socket->closed = (atomic_flag) ATOMIC_FLAG_INIT; return socket; #else return sock; #endif } static inline sc_raw_socket unwrap(sc_socket socket) { #ifdef _WIN32 if (socket == SC_SOCKET_NONE) { return INVALID_SOCKET; } return socket->socket; #else return socket; #endif } #ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning static inline bool sc_raw_socket_close(sc_raw_socket raw_sock) { #ifndef _WIN32 return !close(raw_sock); #else return !closesocket(raw_sock); #endif } #endif #ifndef HAVE_SOCK_CLOEXEC // If SOCK_CLOEXEC does not exist, the flag must be set manually once the // socket is created static bool set_cloexec_flag(sc_raw_socket raw_sock) { #ifndef _WIN32 if (fcntl(raw_sock, F_SETFD, FD_CLOEXEC) == -1) { perror("fcntl F_SETFD"); return false; } #else if (!SetHandleInformation((HANDLE) raw_sock, HANDLE_FLAG_INHERIT, 0)) { LOGE("SetHandleInformation socket failed"); return false; } #endif return true; } #endif static void net_perror(const char *s) { #ifdef _WIN32 sc_log_windows_error(s, WSAGetLastError()); #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 server_socket, uint32_t addr, uint16_t port, int backlog) { sc_raw_socket raw_sock = unwrap(server_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 _WIN32 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 _WIN32 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.25/app/src/util/net.h000066400000000000000000000026461435104021100162260ustar00rootroot00000000000000#ifndef SC_NET_H #define SC_NET_H #include "common.h" #include #include #ifdef _WIN32 # include # include # define SC_SOCKET_NONE NULL typedef struct sc_socket_windows { SOCKET socket; atomic_flag closed; } *sc_socket; #else // not _WIN32 # 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 server_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.25/app/src/util/net_intr.c000066400000000000000000000043771435104021100172600ustar00rootroot00000000000000#include "net_intr.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return false; } bool ret = net_connect(socket, addr, port); sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; } bool net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, uint16_t port, int backlog) { if (!sc_intr_set_socket(intr, server_socket)) { // Already interrupted return false; } bool ret = net_listen(server_socket, addr, port, backlog); sc_intr_set_socket(intr, SC_SOCKET_NONE); return ret; } sc_socket net_accept_intr(struct sc_intr *intr, sc_socket server_socket) { if (!sc_intr_set_socket(intr, server_socket)) { // Already interrupted return SC_SOCKET_NONE; } sc_socket socket = net_accept(server_socket); sc_intr_set_socket(intr, SC_SOCKET_NONE); return socket; } ssize_t net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t r = net_recv(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return r; } ssize_t net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t r = net_recv_all(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return r; } ssize_t net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t w = net_send(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return w; } ssize_t net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len) { if (!sc_intr_set_socket(intr, socket)) { // Already interrupted return -1; } ssize_t w = net_send_all(socket, buf, len); sc_intr_set_socket(intr, SC_SOCKET_NONE); return w; } scrcpy-1.25/app/src/util/net_intr.h000066400000000000000000000015121435104021100172510ustar00rootroot00000000000000#ifndef SC_NET_INTR_H #define SC_NET_INTR_H #include "common.h" #include "intr.h" #include "net.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, uint16_t port); bool net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr, uint16_t port, int backlog); sc_socket net_accept_intr(struct sc_intr *intr, sc_socket server_socket); ssize_t net_recv_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); ssize_t net_recv_all_intr(struct sc_intr *intr, sc_socket socket, void *buf, size_t len); ssize_t net_send_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len); ssize_t net_send_all_intr(struct sc_intr *intr, sc_socket socket, const void *buf, size_t len); #endif scrcpy-1.25/app/src/util/process.c000066400000000000000000000052711435104021100171060ustar00rootroot00000000000000#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, "scrcpy-proc", observer); if (!ok) { sc_cond_destroy(&observer->cond_terminated); sc_mutex_destroy(&observer->mutex); return false; } return true; } bool sc_process_observer_timedwait(struct sc_process_observer *observer, sc_tick deadline) { sc_mutex_lock(&observer->mutex); bool timed_out = false; while (!observer->terminated && !timed_out) { timed_out = !sc_cond_timedwait(&observer->cond_terminated, &observer->mutex, deadline); } bool terminated = observer->terminated; sc_mutex_unlock(&observer->mutex); return terminated; } void sc_process_observer_join(struct sc_process_observer *observer) { sc_thread_join(&observer->thread, NULL); } void sc_process_observer_destroy(struct sc_process_observer *observer) { sc_cond_destroy(&observer->cond_terminated); sc_mutex_destroy(&observer->mutex); } scrcpy-1.25/app/src/util/process.h000066400000000000000000000103001435104021100171000ustar00rootroot00000000000000#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_PROCESS_NONE NULL # define SC_EXIT_CODE_NONE -1UL // max value as unsigned long typedef HANDLE sc_pid; typedef DWORD sc_exit_code; typedef HANDLE sc_pipe; #else # include # define 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.25/app/src/util/process_intr.c000066400000000000000000000014201435104021100201320ustar00rootroot00000000000000#include "process_intr.h" ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted return false; } ssize_t ret = sc_pipe_read(pipe, data, len); if (intr) { sc_intr_set_process(intr, SC_PROCESS_NONE); } return ret; } ssize_t sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted return false; } ssize_t ret = sc_pipe_read_all(pipe, data, len); if (intr) { sc_intr_set_process(intr, SC_PROCESS_NONE); } return ret; } scrcpy-1.25/app/src/util/process_intr.h000066400000000000000000000005551435104021100201470ustar00rootroot00000000000000#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.25/app/src/util/queue.h000066400000000000000000000037421435104021100165620ustar00rootroot00000000000000// 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.25/app/src/util/str.c000066400000000000000000000164561435104021100162470ustar00rootroot00000000000000#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 conditionally, // depending on the next token char pending = 0; // col tracks the current column in the current line size_t col = indent; while (*input) { size_t sep_idx = strcspn(input, "\n "); size_t new_col = col + sep_idx; if (pending == ' ') { // The pending space counts ++new_col; } bool wrap = new_col > columns; char sep = input[sep_idx]; if (sep == ' ') sep = ' '; if (wrap) { APPEND_CHAR('\n'); APPEND_INDENT(); col = indent; } else if (pending) { APPEND_CHAR(pending); ++col; if (pending == '\n') { APPEND_INDENT(); col = indent; } } if (sep_idx) { APPEND(input, sep_idx); col += sep_idx; } pending = sep; input += sep_idx; if (*input != '\0') { // Skip the separator ++input; } } if (pending) APPEND_CHAR(pending); return buf.s; error: free(buf.s); return NULL; } ssize_t sc_str_index_of_column(const char *s, unsigned col, const char *seps) { size_t colidx = 0; size_t idx = 0; while (s[idx] != '\0' && colidx != col) { size_t r = strcspn(&s[idx], seps); idx += r; if (s[idx] == '\0') { // Not found return -1; } size_t consecutive_seps = strspn(&s[idx], seps); assert(consecutive_seps); // At least one idx += consecutive_seps; if (s[idx] != '\0') { ++colidx; } } return col == colidx ? (ssize_t) idx : -1; } size_t sc_str_remove_trailing_cr(char *s, size_t len) { while (len) { if (s[len - 1] != '\r') { break; } s[--len] = '\0'; } return len; } scrcpy-1.25/app/src/util/str.h000066400000000000000000000066401435104021100162460ustar00rootroot00000000000000#ifndef SC_STR_H #define SC_STR_H #include "common.h" #include #include /* Stringify a numeric value */ #define SC_STR(s) SC_XSTR(s) #define SC_XSTR(s) #s /** * Like strncpy(), except: * - it copies at most n-1 chars * - the dest string is nul-terminated * - it does not write useless bytes if strlen(src) < n * - it returns the number of chars actually written (max n-1) if src has * been copied completely, or n if src has been truncated */ size_t sc_strncpy(char *dest, const char *src, size_t n); /** * Join tokens by separator `sep` into `dst` * * Return the number of chars actually written (max n-1) if no truncation * occurred, or n if truncated. */ size_t sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); /** * Quote a string * * Return a new allocated string, surrounded with quotes (`"`). */ char * sc_str_quote(const char *src); /** * Parse `s` as an integer into `out` * * Return true if the conversion succeeded, false otherwise. */ bool sc_str_parse_integer(const char *s, long *out); /** * Parse `s` as integers separated by `sep` (for example `1234:2000`) into `out` * * Returns the number of integers on success, 0 on failure. */ size_t sc_str_parse_integers(const char *s, const char sep, size_t max_items, long *out); /** * Parse `s` as an integer into `out` * * Like `sc_str_parse_integer()`, but accept 'k'/'K' (x1000) and 'm'/'M' * (x1000000) as suffixes. * * Return true if the conversion succeeded, false otherwise. */ bool sc_str_parse_integer_with_suffix(const char *s, long *out); /** * Search `s` in the list separated by `sep` * * For example, sc_str_list_contains("a,bc,def", ',', "bc") returns true. */ bool sc_str_list_contains(const char *list, char sep, const char *s); /** * Return the index to truncate a UTF-8 string at a valid position */ size_t sc_str_utf8_truncation_index(const char *utf8, size_t max_len); #ifdef _WIN32 /** * Convert a UTF-8 string to a wchar_t string * * Return the new allocated string, to be freed by the caller. */ wchar_t * sc_str_to_wchars(const char *utf8); /** * Convert a wchar_t string to a UTF-8 string * * Return the new allocated string, to be freed by the caller. */ char * sc_str_from_wchars(const wchar_t *s); #endif /** * Wrap input lines to fit in `columns` columns * * Break input lines at word boundaries (spaces) so that they fit in `columns` * columns, left-indented by `indent` spaces. */ char * sc_str_wrap_lines(const char *input, unsigned columns, unsigned indent); /** * Find the start of a column in a string * * A string may represent several columns, separated by some "spaces" * (separators). This function aims to find the start of the column number * `col`. * * For example, to find the 4th column (column number 3): * * // here * // v * const char *s = "abc def ghi jk"; * ssize_t index = sc_str_index_of_column(s, 3, " "); * assert(index == 16); // points to "jk" * * Return -1 if no such column exists. */ ssize_t sc_str_index_of_column(const char *s, unsigned col, const char *seps); /** * Remove all `\r` at the end of the line * * The line length is provided by `len` (this avoids a call to `strlen()` when * the caller already knows the length). * * Return the new length. */ size_t sc_str_remove_trailing_cr(char *s, size_t len); #endif scrcpy-1.25/app/src/util/strbuf.c000066400000000000000000000034571435104021100167410ustar00rootroot00000000000000#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.25/app/src/util/strbuf.h000066400000000000000000000025161435104021100167410ustar00rootroot00000000000000#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.25/app/src/util/term.c000066400000000000000000000016501435104021100163740ustar00rootroot00000000000000#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.25/app/src/util/term.h000066400000000000000000000007001435104021100163740ustar00rootroot00000000000000#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.25/app/src/util/thread.c000066400000000000000000000076761435104021100167120ustar00rootroot00000000000000#include "thread.h" #include #include #include #include "log.h" bool sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name, void *userdata) { // The thread name length is limited on some systems. Never use a name // longer than 16 bytes (including the final '\0') assert(strlen(name) <= 15); SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata); if (!sdl_thread) { LOG_OOM(); return false; } 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) { LOGE("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) { LOGE("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) { LOGE("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 } // Round up to the next millisecond to guarantee that the deadline is // reached when returning due to timeout uint32_t ms = SC_TICK_TO_MS(deadline - now + SC_TICK_FROM_MS(1) - 1); int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { LOGE("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); // The deadline is reached on timeout assert(r != SDL_MUTEX_TIMEDOUT || sc_tick_now() >= deadline); return r == 0; } void sc_cond_signal(sc_cond *cond) { int r = SDL_CondSignal(cond->cond); #ifndef NDEBUG if (r) { LOGE("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) { LOGE("Could not broadcast a condition: %s", SDL_GetError()); abort(); } #else (void) r; #endif } scrcpy-1.25/app/src/util/thread.h000066400000000000000000000026651435104021100167100ustar00rootroot00000000000000#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.25/app/src/util/tick.c000066400000000000000000000027731435104021100163660ustar00rootroot00000000000000#include "tick.h" #include #include #ifdef _WIN32 # include #endif sc_tick sc_tick_now(void) { #ifndef _WIN32 // Maximum sc_tick precision (microsecond) struct timespec ts; int ret = clock_gettime(CLOCK_MONOTONIC, &ts); if (ret) { abort(); } return SC_TICK_FROM_SEC(ts.tv_sec) + SC_TICK_FROM_NS(ts.tv_nsec); #else LARGE_INTEGER c; // On systems that run Windows XP or later, the function will always // succeed and will thus never return zero. // // BOOL ok = QueryPerformanceCounter(&c); assert(ok); (void) ok; LONGLONG counter = c.QuadPart; static LONGLONG frequency; if (!frequency) { // Initialize on first call LARGE_INTEGER f; ok = QueryPerformanceFrequency(&f); assert(ok); frequency = f.QuadPart; assert(frequency); } if (frequency % SC_TICK_FREQ == 0) { // Expected case (typically frequency = 10000000, i.e. 100ns precision) sc_tick div = frequency / SC_TICK_FREQ; return SC_TICK_FROM_US(counter / div); } // Split the division to avoid overflow sc_tick secs = SC_TICK_FROM_SEC(counter / frequency); sc_tick subsec = SC_TICK_FREQ * (counter % frequency) / frequency; return secs + subsec; #endif } scrcpy-1.25/app/src/util/tick.h000066400000000000000000000011111435104021100163540ustar00rootroot00000000000000#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_NS(tick) ((tick) * 1000) #define SC_TICK_TO_US(tick) (tick) #define SC_TICK_TO_MS(tick) ((tick) / 1000) #define SC_TICK_TO_SEC(tick) ((tick) / 1000000) #define SC_TICK_FROM_NS(ns) ((ns) / 1000) #define SC_TICK_FROM_US(us) (us) #define SC_TICK_FROM_MS(ms) ((ms) * 1000) #define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) sc_tick sc_tick_now(void); #endif scrcpy-1.25/app/src/util/vector.h000066400000000000000000000337611435104021100167440ustar00rootroot00000000000000#ifndef SC_VECTOR_H #define SC_VECTOR_H #include "common.h" #include #include #include #include // Adapted from vlc_vector: // /** * Vector struct body * * A vector is a dynamic array, managed by the sc_vector_* helpers. * * It is generic over the type of its items, so it is implemented as macros. * * To use a vector, a new type must be defined: * * struct vec_int SC_VECTOR(int); * * The struct may be anonymous: * * struct SC_VECTOR(const char *) names; * * Vector size is accessible via `vec.size`, and items are intended to be * accessed directly, via `vec.data[i]`. * * Functions and macros having name ending with '_' are private. */ #define SC_VECTOR(type) \ { \ size_t cap; \ size_t size; \ type *data; \ } /** * Static initializer for a vector */ #define SC_VECTOR_INITIALIZER { 0, 0, NULL } /** * Initialize an empty vector */ #define sc_vector_init(pv) \ ({ \ (pv)->cap = 0; \ (pv)->size = 0; \ (pv)->data = NULL; \ }) /** * Destroy a vector * * The vector may not be used anymore unless sc_vector_init() is called. */ #define sc_vector_destroy(pv) \ free((pv)->data) /** * Clear a vector * * Remove all items from the vector. */ #define sc_vector_clear(pv) \ ({ \ sc_vector_destroy(pv); \ sc_vector_init(pv);\ }) /** * The minimal allocation size, in number of items * * Private. */ #define SC_VECTOR_MINCAP_ ((size_t) 10) static inline size_t sc_vector_min_(size_t a, size_t b) { return a < b ? a : b; } static inline size_t sc_vector_max_(size_t a, size_t b) { return a > b ? a : b; } static inline size_t sc_vector_clamp_(size_t x, size_t min, size_t max) { return sc_vector_max_(min, sc_vector_min_(max, x)); } /** * Realloc data and update vector fields * * On reallocation success, update the vector capacity (*pcap) and size * (*psize), and return the reallocated data. * * On reallocation failure, return NULL without any change. * * Private. * * \param ptr the current `data` field of the vector to realloc * \param count the requested capacity, in number of items * \param size the size of one item * \param pcap a pointer to the `cap` field of the vector [IN/OUT] * \param psize a pointer to the `size` field of the vector [IN/OUT] * \return the new ptr on success, NULL on error */ static inline void * sc_vector_reallocdata_(void *ptr, size_t count, size_t size, size_t *restrict pcap, size_t *restrict psize) { void *p = realloc(ptr, count * size); if (!p) { return NULL; } *pcap = count; *psize = sc_vector_min_(*psize, count); return p; } #define sc_vector_realloc_(pv, newcap) \ ({ \ void *p = sc_vector_reallocdata_((pv)->data, newcap, sizeof(*(pv)->data), \ &(pv)->cap, &(pv)->size); \ if (p) { \ (pv)->data = p; \ } \ (bool) p; \ }); #define sc_vector_resize_(pv, newcap) \ ({ \ bool ok; \ if ((pv)->cap == (newcap)) { \ ok = true; \ } else if ((newcap) > 0) { \ ok = sc_vector_realloc_(pv, (newcap)); \ } else { \ sc_vector_clear(pv); \ ok = true; \ } \ ok; \ }) static inline size_t sc_vector_growsize_(size_t value) { /* integer multiplication by 1.5 */ return value + (value >> 1); } /* SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow. */ #define sc_vector_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data)) /** * Increase the capacity of the vector to at least `mincap` * * \param pv a pointer to the vector * \param mincap (size_t) the requested capacity * \retval true if no allocation failed * \retval false on allocation failure (the vector is left untouched) */ #define sc_vector_reserve(pv, mincap) \ ({ \ bool ok; \ /* avoid to allocate tiny arrays (< SC_VECTOR_MINCAP_) */ \ size_t mincap_ = sc_vector_max_(mincap, SC_VECTOR_MINCAP_); \ if (mincap_ <= (pv)->cap) { \ /* nothing to do */ \ ok = true; \ } else if (mincap_ <= sc_vector_max_cap_(pv)) { \ /* not too big */ \ size_t newsize = sc_vector_growsize_((pv)->cap); \ newsize = sc_vector_clamp_(newsize, mincap_, sc_vector_max_cap_(pv)); \ ok = sc_vector_realloc_(pv, newsize); \ } else { \ ok = false; \ } \ ok; \ }) #define sc_vector_shrink_to_fit(pv) \ /* decreasing the size may not fail */ \ (void) sc_vector_resize_(pv, (pv)->size) /** * Resize the vector down automatically * * Shrink only when necessary (in practice when cap > (size+5)*1.5) * * \param pv a pointer to the vector */ #define sc_vector_autoshrink(pv) \ ({ \ bool must_shrink = \ /* do not shrink to tiny size */ \ (pv)->cap > SC_VECTOR_MINCAP_ && \ /* no need to shrink */ \ (pv)->cap >= sc_vector_growsize_((pv)->size + 5); \ if (must_shrink) { \ size_t newsize = sc_vector_max_((pv)->size + 5, SC_VECTOR_MINCAP_); \ sc_vector_resize_(pv, newsize); \ } \ }) #define sc_vector_check_same_ptr_type_(a, b) \ (void) ((a) == (b)) /* warn on type mismatch */ /** * Push an item at the end of the vector * * The amortized complexity is O(1). * * \param pv a pointer to the vector * \param item the item to append * \retval true if no allocation failed * \retval false on allocation failure (the vector is left untouched) */ #define sc_vector_push(pv, item) \ ({ \ bool ok = sc_vector_reserve(pv, (pv)->size + 1); \ if (ok) { \ (pv)->data[(pv)->size++] = (item); \ } \ ok; \ }) /** * Append `count` items at the end of the vector * * \param pv a pointer to the vector * \param items the items array to append * \param count the number of items in the array * \retval true if no allocation failed * \retval false on allocation failure (the vector is left untouched) */ #define sc_vector_push_all(pv, items, count) \ sc_vector_push_all_(pv, items, (size_t) count) #define sc_vector_push_all_(pv, items, count) \ ({ \ sc_vector_check_same_ptr_type_((pv)->data, items); \ bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ if (ok) { \ memcpy(&(pv)->data[(pv)->size], items, (count) * sizeof(*(pv)->data)); \ (pv)->size += count; \ } \ ok; \ }) /** * Insert an hole of size `count` to the given index * * The items in range [index; size-1] will be moved. The items in the hole are * left uninitialized. * * \param pv a pointer to the vector * \param index the index where the hole is to be inserted * \param count the number of items in the hole * \retval true if no allocation failed * \retval false on allocation failure (the vector is left untouched) */ #define sc_vector_insert_hole(pv, index, count) \ sc_vector_insert_hole_(pv, (size_t) index, (size_t) count); #define sc_vector_insert_hole_(pv, index, count) \ ({ \ bool ok = sc_vector_reserve(pv, (pv)->size + (count)); \ if (ok) { \ if ((index) < (pv)->size) { \ memmove(&(pv)->data[(index) + (count)], \ &(pv)->data[(index)], \ ((pv)->size - (index)) * sizeof(*(pv)->data)); \ } \ (pv)->size += count; \ } \ ok; \ }) /** * Insert an item at the given index * * The items in range [index; size-1] will be moved. * * \param pv a pointer to the vector * \param index the index where the item is to be inserted * \param item the item to append * \retval true if no allocation failed * \retval false on allocation failure (the vector is left untouched) */ #define sc_vector_insert(pv, index, item) \ sc_vector_insert_(pv, (size_t) index, (size_t) item); #define sc_vector_insert_(pv, index, item) \ ({ \ bool ok = sc_vector_insert_hole_(pv, index, 1); \ if (ok) { \ (pv)->data[index] = (item); \ } \ ok; \ }) /** * Insert `count` items at the given index * * The items in range [index; size-1] will be moved. * * \param pv a pointer to the vector * \param index the index where the items are to be inserted * \param items the items array to append * \param count the number of items in the array * \retval true if no allocation failed * \retval false on allocation failure (the vector is left untouched) */ #define sc_vector_insert_all(pv, index, items, count) \ sc_vector_insert_all_(pv, (size_t) index, items, (size_t) count) #define sc_vector_insert_all_(pv, index, items, count) \ ({ \ sc_vector_check_same_ptr_type_((pv)->data, items); \ bool ok = sc_vector_insert_hole_(pv, index, count); \ if (ok) { \ memcpy(&(pv)->data[index], items, count * sizeof(*(pv)->data)); \ } \ ok; \ }) /** Reverse a char array in place */ static inline void sc_char_array_reverse(char *array, size_t len) { for (size_t i = 0; i < len / 2; ++i) { char c = array[i]; array[i] = array[len - i - 1]; array[len - i - 1] = c; } } /** * Right-rotate a (char) array in place * * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with * distance 4 will result in {5, 6, 1, 2, 3, 4}. * * Private. */ static inline void sc_char_array_rotate_left(char *array, size_t len, size_t distance) { sc_char_array_reverse(array, distance); sc_char_array_reverse(&array[distance], len - distance); sc_char_array_reverse(array, len); } /** * Right-rotate a (char) array in place * * For example, left-rotating a char array containing {1, 2, 3, 4, 5, 6} with * distance 2 will result in {5, 6, 1, 2, 3, 4}. * * Private. */ static inline void sc_char_array_rotate_right(char *array, size_t len, size_t distance) { sc_char_array_rotate_left(array, len, len - distance); } /** * Move items in a (char) array in place * * Move slice [index, count] to target. */ static inline void sc_char_array_move(char *array, size_t idx, size_t count, size_t target) { if (idx < target) { sc_char_array_rotate_left(&array[idx], target - idx + count, count); } else { sc_char_array_rotate_right(&array[target], idx - target + count, count); } } /** * Move a slice of items to a given target index * * The items in range [index; count] will be moved so that the *new* position * of the first item is `target`. * * \param pv a pointer to the vector * \param index the index of the first item to move * \param count the number of items to move * \param target the new index of the moved slice */ #define sc_vector_move_slice(pv, index, count, target) \ sc_vector_move_slice_(pv, (size_t) index, count, (size_t) target); #define sc_vector_move_slice_(pv, index, count, target) \ ({ \ sc_char_array_move((char *) (pv)->data, \ (index) * sizeof(*(pv)->data), \ (count) * sizeof(*(pv)->data), \ (target) * sizeof(*(pv)->data)); \ }) /** * Move an item to a given target index * * The items will be moved so that its *new* position is `target`. * * \param pv a pointer to the vector * \param index the index of the item to move * \param target the new index of the moved item */ #define sc_vector_move(pv, index, target) \ sc_vector_move_slice(pv, index, 1, target) /** * Remove a slice of items, without shrinking the array * * If you have no good reason to use the _noshrink() version, use * sc_vector_remove_slice() instead. * * The items in range [index+count; size-1] will be moved. * * \param pv a pointer to the vector * \param index the index of the first item to remove * \param count the number of items to remove */ #define sc_vector_remove_slice_noshrink(pv, index, count) \ sc_vector_remove_slice_noshrink_(pv, (size_t) index, (size_t) count) #define sc_vector_remove_slice_noshrink_(pv, index, count) \ ({ \ if ((index) + (count) < (pv)->size) { \ memmove(&(pv)->data[index], \ &(pv)->data[(index) + (count)], \ ((pv)->size - (index) - (count)) * sizeof(*(pv)->data)); \ } \ (pv)->size -= count; \ }) /** * Remove a slice of items * * The items in range [index+count; size-1] will be moved. * * \param pv a pointer to the vector * \param index the index of the first item to remove * \param count the number of items to remove */ #define sc_vector_remove_slice(pv, index, count) \ ({ \ sc_vector_remove_slice_noshrink(pv, index, count); \ sc_vector_autoshrink(pv); \ }) /** * Remove an item, without shrinking the array * * If you have no good reason to use the _noshrink() version, use * sc_vector_remove() instead. * * The items in range [index+1; size-1] will be moved. * * \param pv a pointer to the vector * \param index the index of item to remove */ #define sc_vector_remove_noshrink(pv, index) \ sc_vector_remove_slice_noshrink(pv, index, 1) /** * Remove an item * * The items in range [index+1; size-1] will be moved. * * \param pv a pointer to the vector * \param index the index of item to remove */ #define sc_vector_remove(pv, index) \ ({ \ sc_vector_remove_noshrink(pv, index); \ sc_vector_autoshrink(pv); \ }) /** * Remove an item * * The removed item is replaced by the last item of the vector. * * This does not preserve ordering, but is O(1). This is useful when the order * of items is not meaningful. * * \param pv a pointer to the vector * \param index the index of item to remove */ #define sc_vector_swap_remove(pv, index) \ sc_vector_swap_remove_(pv, (size_t) index); #define sc_vector_swap_remove_(pv, index) \ ({ \ (pv)->data[index] = (pv)->data[(pv)->size-1]; \ (pv)->size--; \ }); /** * Return the index of an item * * Iterate over all items to find a given item. * * Use only for vectors of primitive types or pointers. * * Return the index, or -1 if not found. * * \param pv a pointer to the vector * \param item the item to find (compared with ==) */ #define sc_vector_index_of(pv, item) \ ({ \ ssize_t idx = -1; \ for (size_t i = 0; i < (pv)->size; ++i) { \ if ((pv)->data[i] == (item)) { \ idx = (ssize_t) i; \ break; \ } \ } \ idx; \ }) #endif scrcpy-1.25/app/src/v4l2_sink.c000066400000000000000000000240101435104021100162560ustar00rootroot00000000000000#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, "scrcpy-v4l2", vs); if (!ok) { LOGE("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.25/app/src/v4l2_sink.h000066400000000000000000000015461435104021100162740ustar00rootroot00000000000000#ifndef SC_V4L2_SINK_H #define SC_V4L2_SINK_H #include "common.h" #include #include #include "coords.h" #include "trait/frame_sink.h" #include "video_buffer.h" #include "util/tick.h" 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.25/app/src/version.c000066400000000000000000000041111435104021100161300ustar00rootroot00000000000000#include "version.h" #include #include #include #ifdef HAVE_V4L2 # include #endif #ifdef HAVE_USB # include #endif void scrcpy_print_version(void) { printf("\nDependencies (compiled / linked):\n"); SDL_version sdl; SDL_GetVersion(&sdl); printf(" - SDL: %u.%u.%u / %u.%u.%u\n", SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL, (unsigned) sdl.major, (unsigned) sdl.minor, (unsigned) sdl.patch); unsigned avcodec = avcodec_version(); printf(" - libavcodec: %u.%u.%u / %u.%u.%u\n", LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, LIBAVCODEC_VERSION_MICRO, AV_VERSION_MAJOR(avcodec), AV_VERSION_MINOR(avcodec), AV_VERSION_MICRO(avcodec)); unsigned avformat = avformat_version(); printf(" - libavformat: %u.%u.%u / %u.%u.%u\n", LIBAVFORMAT_VERSION_MAJOR, LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO, AV_VERSION_MAJOR(avformat), AV_VERSION_MINOR(avformat), AV_VERSION_MICRO(avformat)); unsigned avutil = avutil_version(); printf(" - libavutil: %u.%u.%u / %u.%u.%u\n", LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO, AV_VERSION_MAJOR(avutil), AV_VERSION_MINOR(avutil), AV_VERSION_MICRO(avutil)); #ifdef HAVE_V4L2 unsigned avdevice = avdevice_version(); printf(" - libavdevice: %u.%u.%u / %u.%u.%u\n", LIBAVDEVICE_VERSION_MAJOR, LIBAVDEVICE_VERSION_MINOR, LIBAVDEVICE_VERSION_MICRO, AV_VERSION_MAJOR(avdevice), AV_VERSION_MINOR(avdevice), AV_VERSION_MICRO(avdevice)); #endif #ifdef HAVE_USB const struct libusb_version *usb = libusb_get_version(); // The compiled version may not be known printf(" - libusb: - / %u.%u.%u\n", (unsigned) usb->major, (unsigned) usb->minor, (unsigned) usb->micro); #endif } scrcpy-1.25/app/src/version.h000066400000000000000000000001511435104021100161350ustar00rootroot00000000000000#ifndef SC_VERSION_H #define SC_VERSION_H #include "common.h" void scrcpy_print_version(void); #endif scrcpy-1.25/app/src/video_buffer.c000066400000000000000000000146171435104021100171160ustar00rootroot00000000000000#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, "scrcpy-vbuf", 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.25/app/src/video_buffer.h000066400000000000000000000031731435104021100171160ustar00rootroot00000000000000#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.25/app/tests/000077500000000000000000000000001435104021100146555ustar00rootroot00000000000000scrcpy-1.25/app/tests/test_adb_parser.c000066400000000000000000000204231435104021100201630ustar00rootroot00000000000000#include "common.h" #include #include "adb/adb_device.h" #include "adb/adb_parser.h" static void test_adb_devices(void) { char output[] = "List of devices attached\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\n" "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " "device:MyWifiDevice trandport_id:2\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 2); struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); device = &vec.data[1]; assert(!strcmp("192.168.1.1:5555", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyWifiModel", device->model)); sc_adb_devices_destroy(&vec); } static void test_adb_devices_cr(void) { char output[] = "List of devices attached\r\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\r\n" "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " "device:MyWifiDevice trandport_id:2\r\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 2); struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); device = &vec.data[1]; assert(!strcmp("192.168.1.1:5555", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyWifiModel", device->model)); sc_adb_devices_destroy(&vec); } static void test_adb_devices_daemon_start(void) { char output[] = "* daemon not running; starting now at tcp:5037\n" "* daemon started successfully\n" "List of devices attached\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 1); struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); sc_adb_devices_destroy(&vec); } static void test_adb_devices_daemon_start_mixed(void) { char output[] = "List of devices attached\n" "adb server version (41) doesn't match this client (39); killing...\n" "* daemon started successfully *\n" "0123456789abcdef unauthorized usb:1-1\n" "87654321 device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 2); struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("unauthorized", device->state)); assert(!device->model); device = &vec.data[1]; assert(!strcmp("87654321", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); sc_adb_devices_destroy(&vec); } static void test_adb_devices_without_eol(void) { char output[] = "List of devices attached\n" "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 1); struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("device", device->state)); assert(!strcmp("MyModel", device->model)); sc_adb_devices_destroy(&vec); } static void test_adb_devices_without_header(void) { char output[] = "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " "device:MyDevice transport_id:1\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(!ok); } static void test_adb_devices_corrupted(void) { char output[] = "List of devices attached\n" "corrupted_garbage\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 0); } static void test_adb_devices_spaces(void) { char output[] = "List of devices attached\n" "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; bool ok = sc_adb_parse_devices(output, &vec); assert(ok); assert(vec.size == 1); struct sc_adb_device *device = &vec.data[0]; assert(!strcmp("0123456789abcdef", device->serial)); assert(!strcmp("unauthorized", device->state)); assert(!device->model); sc_adb_devices_destroy(&vec); } static void test_get_ip_single_line(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); } static void test_get_ip_single_line_without_eol(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34"; char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); } static void test_get_ip_single_line_with_trailing_space(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34 \n"; char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.12.34")); free(ip); } static void test_get_ip_multiline_first_ok(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.1.2\r\n" "10.0.0.0/24 dev rmnet proto kernel scope link src " "10.0.0.2\r\n"; char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.1.2")); free(ip); } static void test_get_ip_multiline_second_ok(void) { char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src " "10.0.0.3\r\n" "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.1.3\r\n"; char *ip = sc_adb_parse_device_ip(ip_route); assert(ip); assert(!strcmp(ip, "192.168.1.3")); free(ip); } static void test_get_ip_no_wlan(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34\r\r\n"; char *ip = sc_adb_parse_device_ip(ip_route); assert(!ip); } static void test_get_ip_no_wlan_without_eol(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "192.168.12.34"; char *ip = sc_adb_parse_device_ip(ip_route); assert(!ip); } static void test_get_ip_truncated(void) { char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " "\n"; char *ip = sc_adb_parse_device_ip(ip_route); assert(!ip); } int main(int argc, char *argv[]) { (void) argc; (void) argv; test_adb_devices(); test_adb_devices_cr(); test_adb_devices_daemon_start(); test_adb_devices_daemon_start_mixed(); test_adb_devices_without_eol(); test_adb_devices_without_header(); test_adb_devices_corrupted(); test_adb_devices_spaces(); test_get_ip_single_line(); test_get_ip_single_line_without_eol(); test_get_ip_single_line_with_trailing_space(); test_get_ip_multiline_first_ok(); test_get_ip_multiline_second_ok(); test_get_ip_no_wlan(); test_get_ip_no_wlan_without_eol(); test_get_ip_truncated(); return 0; } scrcpy-1.25/app/tests/test_binary.c000066400000000000000000000053471435104021100173550ustar00rootroot00000000000000#include "common.h" #include #include "util/binary.h" static void test_write16be(void) { uint16_t val = 0xABCD; uint8_t buf[2]; sc_write16be(buf, val); assert(buf[0] == 0xAB); assert(buf[1] == 0xCD); } static void test_write32be(void) { uint32_t val = 0xABCD1234; uint8_t buf[4]; sc_write32be(buf, val); assert(buf[0] == 0xAB); assert(buf[1] == 0xCD); assert(buf[2] == 0x12); assert(buf[3] == 0x34); } static void test_write64be(void) { uint64_t val = 0xABCD1234567890EF; uint8_t buf[8]; sc_write64be(buf, val); assert(buf[0] == 0xAB); assert(buf[1] == 0xCD); assert(buf[2] == 0x12); assert(buf[3] == 0x34); assert(buf[4] == 0x56); assert(buf[5] == 0x78); assert(buf[6] == 0x90); assert(buf[7] == 0xEF); } static void test_read16be(void) { uint8_t buf[2] = {0xAB, 0xCD}; uint16_t val = sc_read16be(buf); assert(val == 0xABCD); } static void test_read32be(void) { uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34}; uint32_t val = sc_read32be(buf); assert(val == 0xABCD1234); } static void test_read64be(void) { uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34, 0x56, 0x78, 0x90, 0xEF}; uint64_t val = sc_read64be(buf); assert(val == 0xABCD1234567890EF); } static void test_float_to_u16fp(void) { assert(sc_float_to_u16fp(0.0f) == 0); assert(sc_float_to_u16fp(0.03125f) == 0x800); assert(sc_float_to_u16fp(0.0625f) == 0x1000); assert(sc_float_to_u16fp(0.125f) == 0x2000); assert(sc_float_to_u16fp(0.25f) == 0x4000); assert(sc_float_to_u16fp(0.5f) == 0x8000); assert(sc_float_to_u16fp(0.75f) == 0xc000); assert(sc_float_to_u16fp(1.0f) == 0xffff); } static void test_float_to_i16fp(void) { assert(sc_float_to_i16fp(0.0f) == 0); assert(sc_float_to_i16fp(0.03125f) == 0x400); assert(sc_float_to_i16fp(0.0625f) == 0x800); assert(sc_float_to_i16fp(0.125f) == 0x1000); assert(sc_float_to_i16fp(0.25f) == 0x2000); assert(sc_float_to_i16fp(0.5f) == 0x4000); assert(sc_float_to_i16fp(0.75f) == 0x6000); assert(sc_float_to_i16fp(1.0f) == 0x7fff); assert(sc_float_to_i16fp(-0.03125f) == -0x400); assert(sc_float_to_i16fp(-0.0625f) == -0x800); assert(sc_float_to_i16fp(-0.125f) == -0x1000); assert(sc_float_to_i16fp(-0.25f) == -0x2000); assert(sc_float_to_i16fp(-0.5f) == -0x4000); assert(sc_float_to_i16fp(-0.75f) == -0x6000); assert(sc_float_to_i16fp(-1.0f) == -0x8000); } int main(int argc, char *argv[]) { (void) argc; (void) argv; test_write16be(); test_write32be(); test_write64be(); test_read16be(); test_read32be(); test_read64be(); test_float_to_u16fp(); test_float_to_i16fp(); return 0; } scrcpy-1.25/app/tests/test_cbuf.c000066400000000000000000000030641435104021100170020ustar00rootroot00000000000000#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.25/app/tests/test_cli.c000066400000000000000000000114701435104021100166320ustar00rootroot00000000000000#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_SHORTCUT_MOD_LCTRL); ok = sc_parse_shortcut_mods("lctrl+lalt", &mods); assert(ok); assert(mods.count == 1); assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_LALT)); ok = sc_parse_shortcut_mods("rctrl,lalt", &mods); assert(ok); assert(mods.count == 2); assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL); assert(mods.data[1] == SC_SHORTCUT_MOD_LALT); ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods); assert(ok); assert(mods.count == 3); assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER); assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT)); assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL | SC_SHORTCUT_MOD_RALT)); ok = sc_parse_shortcut_mods("", &mods); assert(!ok); 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.25/app/tests/test_clock.c000066400000000000000000000043651435104021100171630ustar00rootroot00000000000000#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.25/app/tests/test_control_msg_serialize.c000066400000000000000000000250511435104021100224600ustar00rootroot00000000000000#include "common.h" #include #include #include "control_msg.h" static void test_serialize_inject_keycode(void) { struct sc_control_msg msg = { .type = SC_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[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 14); const unsigned char expected[] = { SC_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 sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_INJECT_TEXT, .inject_text = { .text = "hello, world!", }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 18); const unsigned char expected[] = { SC_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 sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; char text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH + 1]; memset(text, 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; msg.inject_text.text = text; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); unsigned char expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; expected[1] = 0x00; expected[2] = 0x00; expected[3] = 0x01; expected[4] = 0x2c; // text length (32 bits) memset(&expected[5], 'a', SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_touch_event(void) { struct sc_control_msg msg = { .type = SC_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[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 28); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, 0x00, // AKEY_EVENT_ACTION_DOWN 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200 0x04, 0x38, 0x07, 0x80, // 1080 1920 0xff, 0xff, // pressure 0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_inject_scroll_event(void) { struct sc_control_msg msg = { .type = SC_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, .buttons = 1, }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 21); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 0x7F, 0xFF, // 1 (float encoded as i16) 0x80, 0x00, // -1 (float encoded as i16) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_back_or_screen_on(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .back_or_screen_on = { .action = AKEY_EVENT_ACTION_UP, }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, 0x01, // AKEY_EVENT_ACTION_UP }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_notification_panel(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_expand_settings_panel(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_collapse_panels(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_get_clipboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, .get_clipboard = { .copy_key = SC_COPY_KEY_COPY, }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_COPY_KEY_COPY, }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_clipboard(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), .paste = true, .text = "hello, world!", }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 27); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste 0x00, 0x00, 0x00, 0x0d, // text length 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_clipboard_long(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, .set_clipboard = { .sequence = UINT64_C(0x0102030405060708), .paste = true, .text = NULL, }, }; char text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH + 1]; memset(text, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; msg.set_clipboard.text = text; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == SC_CONTROL_MSG_MAX_SIZE); unsigned char expected[SC_CONTROL_MSG_MAX_SIZE] = { SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 1, // paste // text length SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 24, (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 16) & 0xff, (SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH >> 8) & 0xff, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH & 0xff, }; memset(expected + 14, 'a', SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_set_screen_power_mode(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, .set_screen_power_mode = { .mode = SC_SCREEN_POWER_MODE_NORMAL, }, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 2); const unsigned char expected[] = { SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, 0x02, // SC_SCREEN_POWER_MODE_NORMAL }; assert(!memcmp(buf, expected, sizeof(expected))); } static void test_serialize_rotate_device(void) { struct sc_control_msg msg = { .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, }; unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); assert(size == 1); const unsigned char expected[] = { SC_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_clipboard_long(); test_serialize_set_screen_power_mode(); test_serialize_rotate_device(); return 0; } scrcpy-1.25/app/tests/test_device_msg_deserialize.c000066400000000000000000000040141435104021100225440ustar00rootroot00000000000000#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.25/app/tests/test_queue.c000066400000000000000000000014501435104021100172040ustar00rootroot00000000000000#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.25/app/tests/test_str.c000066400000000000000000000261041435104021100166730ustar00rootroot00000000000000#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_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_index_of_column(); test_remove_trailing_cr(); return 0; } scrcpy-1.25/app/tests/test_strbuf.c000066400000000000000000000017141435104021100173700ustar00rootroot00000000000000#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.25/app/tests/test_vector.c000066400000000000000000000233461435104021100173720ustar00rootroot00000000000000#include "common.h" #include #include "util/vector.h" static void test_vector_insert_remove(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; ok = sc_vector_push(&vec, 42); assert(ok); assert(vec.data[0] == 42); assert(vec.size == 1); ok = sc_vector_push(&vec, 37); assert(ok); assert(vec.size == 2); assert(vec.data[0] == 42); assert(vec.data[1] == 37); ok = sc_vector_insert(&vec, 1, 100); assert(ok); assert(vec.size == 3); assert(vec.data[0] == 42); assert(vec.data[1] == 100); assert(vec.data[2] == 37); ok = sc_vector_push(&vec, 77); assert(ok); assert(vec.size == 4); assert(vec.data[0] == 42); assert(vec.data[1] == 100); assert(vec.data[2] == 37); assert(vec.data[3] == 77); sc_vector_remove(&vec, 1); assert(vec.size == 3); assert(vec.data[0] == 42); assert(vec.data[1] == 37); assert(vec.data[2] == 77); sc_vector_clear(&vec); assert(vec.size == 0); sc_vector_destroy(&vec); } static void test_vector_push_array(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; ok = sc_vector_push(&vec, 3); assert(ok); ok = sc_vector_push(&vec, 14); assert(ok); ok = sc_vector_push(&vec, 15); assert(ok); ok = sc_vector_push(&vec, 92); assert(ok); ok = sc_vector_push(&vec, 65); assert(ok); assert(vec.size == 5); int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; ok = sc_vector_push_all(&vec, items, 8); assert(ok); assert(vec.size == 13); assert(vec.data[0] == 3); assert(vec.data[1] == 14); assert(vec.data[2] == 15); assert(vec.data[3] == 92); assert(vec.data[4] == 65); assert(vec.data[5] == 1); assert(vec.data[6] == 2); assert(vec.data[7] == 3); assert(vec.data[8] == 4); assert(vec.data[9] == 5); assert(vec.data[10] == 6); assert(vec.data[11] == 7); assert(vec.data[12] == 8); sc_vector_destroy(&vec); } static void test_vector_insert_array(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; ok = sc_vector_push(&vec, 3); assert(ok); ok = sc_vector_push(&vec, 14); assert(ok); ok = sc_vector_push(&vec, 15); assert(ok); ok = sc_vector_push(&vec, 92); assert(ok); ok = sc_vector_push(&vec, 65); assert(ok); assert(vec.size == 5); int items[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; ok = sc_vector_insert_all(&vec, 3, items, 8); assert(ok); assert(vec.size == 13); assert(vec.data[0] == 3); assert(vec.data[1] == 14); assert(vec.data[2] == 15); assert(vec.data[3] == 1); assert(vec.data[4] == 2); assert(vec.data[5] == 3); assert(vec.data[6] == 4); assert(vec.data[7] == 5); assert(vec.data[8] == 6); assert(vec.data[9] == 7); assert(vec.data[10] == 8); assert(vec.data[11] == 92); assert(vec.data[12] == 65); sc_vector_destroy(&vec); } static void test_vector_remove_slice(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; for (int i = 0; i < 100; ++i) { ok = sc_vector_push(&vec, i); assert(ok); } assert(vec.size == 100); sc_vector_remove_slice(&vec, 32, 60); assert(vec.size == 40); assert(vec.data[31] == 31); assert(vec.data[32] == 92); sc_vector_destroy(&vec); } static void test_vector_swap_remove(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; ok = sc_vector_push(&vec, 3); assert(ok); ok = sc_vector_push(&vec, 14); assert(ok); ok = sc_vector_push(&vec, 15); assert(ok); ok = sc_vector_push(&vec, 92); assert(ok); ok = sc_vector_push(&vec, 65); assert(ok); assert(vec.size == 5); sc_vector_swap_remove(&vec, 1); assert(vec.size == 4); assert(vec.data[0] == 3); assert(vec.data[1] == 65); assert(vec.data[2] == 15); assert(vec.data[3] == 92); sc_vector_destroy(&vec); } static void test_vector_index_of(void) { struct SC_VECTOR(int) vec; sc_vector_init(&vec); bool ok; for (int i = 0; i < 10; ++i) { ok = sc_vector_push(&vec, i); assert(ok); } ssize_t idx; idx = sc_vector_index_of(&vec, 0); assert(idx == 0); idx = sc_vector_index_of(&vec, 1); assert(idx == 1); idx = sc_vector_index_of(&vec, 4); assert(idx == 4); idx = sc_vector_index_of(&vec, 9); assert(idx == 9); idx = sc_vector_index_of(&vec, 12); assert(idx == -1); sc_vector_destroy(&vec); } static void test_vector_grow() { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; for (int i = 0; i < 50; ++i) { ok = sc_vector_push(&vec, i); /* append */ assert(ok); } assert(vec.cap >= 50); assert(vec.size == 50); for (int i = 0; i < 25; ++i) { ok = sc_vector_insert(&vec, 20, i); /* insert in the middle */ assert(ok); } assert(vec.cap >= 75); assert(vec.size == 75); for (int i = 0; i < 25; ++i) { ok = sc_vector_insert(&vec, 0, i); /* prepend */ assert(ok); } assert(vec.cap >= 100); assert(vec.size == 100); for (int i = 0; i < 50; ++i) sc_vector_remove(&vec, 20); /* remove from the middle */ assert(vec.cap >= 50); assert(vec.size == 50); for (int i = 0; i < 25; ++i) sc_vector_remove(&vec, 0); /* remove from the head */ assert(vec.cap >= 25); assert(vec.size == 25); for (int i = 24; i >=0; --i) sc_vector_remove(&vec, i); /* remove from the tail */ assert(vec.size == 0); sc_vector_destroy(&vec); } static void test_vector_exp_growth(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; size_t oldcap = vec.cap; int realloc_count = 0; bool ok; for (int i = 0; i < 10000; ++i) { ok = sc_vector_push(&vec, i); assert(ok); if (vec.cap != oldcap) { realloc_count++; oldcap = vec.cap; } } /* Test speciically for an expected growth factor of 1.5. In practice, the * result is even lower (19) due to the first alloc of size 10 */ assert(realloc_count <= 23); /* ln(10000) / ln(1.5) ~= 23 */ realloc_count = 0; for (int i = 9999; i >= 0; --i) { sc_vector_remove(&vec, i); if (vec.cap != oldcap) { realloc_count++; oldcap = vec.cap; } } assert(realloc_count <= 23); /* same expectations for removals */ assert(realloc_count > 0); /* sc_vector_remove() must autoshrink */ sc_vector_destroy(&vec); } static void test_vector_reserve(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; ok = sc_vector_reserve(&vec, 800); assert(ok); assert(vec.cap >= 800); assert(vec.size == 0); size_t initial_cap = vec.cap; for (int i = 0; i < 800; ++i) { ok = sc_vector_push(&vec, i); assert(ok); assert(vec.cap == initial_cap); /* no realloc */ } sc_vector_destroy(&vec); } static void test_vector_shrink_to_fit(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; bool ok; ok = sc_vector_reserve(&vec, 800); assert(ok); for (int i = 0; i < 250; ++i) { ok = sc_vector_push(&vec, i); assert(ok); } assert(vec.cap >= 800); assert(vec.size == 250); sc_vector_shrink_to_fit(&vec); assert(vec.cap == 250); assert(vec.size == 250); sc_vector_destroy(&vec); } static void test_vector_move(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; for (int i = 0; i < 7; ++i) { bool ok = sc_vector_push(&vec, i); assert(ok); } /* move item at 1 so that its new position is 4 */ sc_vector_move(&vec, 1, 4); assert(vec.size == 7); assert(vec.data[0] == 0); assert(vec.data[1] == 2); assert(vec.data[2] == 3); assert(vec.data[3] == 4); assert(vec.data[4] == 1); assert(vec.data[5] == 5); assert(vec.data[6] == 6); sc_vector_destroy(&vec); } static void test_vector_move_slice_forward(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; for (int i = 0; i < 10; ++i) { bool ok = sc_vector_push(&vec, i); assert(ok); } /* move slice {2, 3, 4, 5} so that its new position is 5 */ sc_vector_move_slice(&vec, 2, 4, 5); assert(vec.size == 10); assert(vec.data[0] == 0); assert(vec.data[1] == 1); assert(vec.data[2] == 6); assert(vec.data[3] == 7); assert(vec.data[4] == 8); assert(vec.data[5] == 2); assert(vec.data[6] == 3); assert(vec.data[7] == 4); assert(vec.data[8] == 5); assert(vec.data[9] == 9); sc_vector_destroy(&vec); } static void test_vector_move_slice_backward(void) { struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; for (int i = 0; i < 10; ++i) { bool ok = sc_vector_push(&vec, i); assert(ok); } /* move slice {5, 6, 7} so that its new position is 2 */ sc_vector_move_slice(&vec, 5, 3, 2); assert(vec.size == 10); assert(vec.data[0] == 0); assert(vec.data[1] == 1); assert(vec.data[2] == 5); assert(vec.data[3] == 6); assert(vec.data[4] == 7); assert(vec.data[5] == 2); assert(vec.data[6] == 3); assert(vec.data[7] == 4); assert(vec.data[8] == 8); assert(vec.data[9] == 9); sc_vector_destroy(&vec); } int main(int argc, char *argv[]) { (void) argc; (void) argv; test_vector_insert_remove(); test_vector_push_array(); test_vector_insert_array(); test_vector_remove_slice(); test_vector_swap_remove(); test_vector_move(); test_vector_move_slice_forward(); test_vector_move_slice_backward(); test_vector_index_of(); test_vector_grow(); test_vector_exp_growth(); test_vector_reserve(); test_vector_shrink_to_fit(); return 0; } scrcpy-1.25/assets/000077500000000000000000000000001435104021100142355ustar00rootroot00000000000000scrcpy-1.25/assets/screenshot-debian-600.jpg000066400000000000000000001276201435104021100206470ustar00rootroot00000000000000JFIFC     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.25/build.gradle000066400000000000000000000011741435104021100152150ustar00rootroot00000000000000// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.2.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() mavenCentral() } tasks.withType(JavaCompile) { options.compilerArgs << "-Xlint:deprecation" } } task clean(type: Delete) { delete rootProject.buildDir } scrcpy-1.25/bump_version000077500000000000000000000016471435104021100154010ustar00rootroot00000000000000#!/usr/bin/env bash # # This script bump scrcpy version by editing all the necessary files. # # Usage: # # ./bump_version 1.23.4 # # Then check the diff manually to confirm that everything is ok. set -e if [[ $# != 1 ]] then echo "Syntax: $0 " >&2 exit 1 fi VERSION="$1" a=( ${VERSION//./ } ) MAJOR="${a[0]:-0}" MINOR="${a[1]:-0}" PATCH="${a[2]:-0}" # If VERSION is 1.23.4, then VERSION_CODE is 12304 VERSION_CODE="$(( $MAJOR * 10000 + $MINOR * 100 + "$PATCH" ))" echo "$VERSION: major=$MAJOR minor=$MINOR patch=$PATCH [versionCode=$VERSION_CODE]" sed -i "s/^\(\s*version: \)'[^']*'/\1'$VERSION'/" meson.build sed -i "s/^\(\s*versionCode \).*/\1$VERSION_CODE/;s/^\(\s*versionName \).*/\1\"$VERSION\"/" server/build.gradle sed -i "s/^\(SCRCPY_VERSION_NAME=\).*/\1$VERSION/" server/build_without_gradle.sh sed -i "s/^\(\s*VALUE \"ProductVersion\", \)\"[^\"]*\"/\1\"$VERSION\"/" app/scrcpy-windows.rc echo done scrcpy-1.25/config/000077500000000000000000000000001435104021100142005ustar00rootroot00000000000000scrcpy-1.25/config/android-checkstyle.gradle000066400000000000000000000012031435104021100211300ustar00rootroot00000000000000apply 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.25/config/checkstyle/000077500000000000000000000000001435104021100163365ustar00rootroot00000000000000scrcpy-1.25/config/checkstyle/checkstyle.xml000066400000000000000000000134701435104021100212230ustar00rootroot00000000000000 scrcpy-1.25/cross_win32.txt000066400000000000000000000011561435104021100156520ustar00rootroot00000000000000# 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' windres = 'i686-w64-mingw32-windres' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'i686' endian = 'little' [properties] ffmpeg_avcodec = 'avcodec-58' ffmpeg_avformat = 'avformat-58' ffmpeg_avutil = 'avutil-56' prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1' prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32' scrcpy-1.25/cross_win64.txt000066400000000000000000000011741435104021100156570ustar00rootroot00000000000000# 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' windres = 'x86_64-w64-mingw32-windres' [host_machine] system = 'windows' cpu_family = 'x86' cpu = 'x86_64' endian = 'little' [properties] ffmpeg_avcodec = 'avcodec-59' ffmpeg_avformat = 'avformat-59' ffmpeg_avutil = 'avutil-57' prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2' prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32' prebuilt_libusb_root = 'libusb-1.0.26' prebuilt_libusb = 'libusb-1.0.26/MinGW-x64' scrcpy-1.25/gradle.properties000066400000000000000000000013331435104021100163070ustar00rootroot00000000000000# 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.25/gradle/000077500000000000000000000000001435104021100141715ustar00rootroot00000000000000scrcpy-1.25/gradle/wrapper/000077500000000000000000000000001435104021100156515ustar00rootroot00000000000000scrcpy-1.25/gradle/wrapper/gradle-wrapper.jar000066400000000000000000001625061435104021100212750ustar00rootroot00000000000000PK 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.25/gradle/wrapper/gradle-wrapper.properties000066400000000000000000000003121435104021100226770ustar00rootroot00000000000000distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists scrcpy-1.25/gradlew000077500000000000000000000132041435104021100143060ustar00rootroot00000000000000#!/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.25/gradlew.bat000066400000000000000000000057601435104021100150600ustar00rootroot00000000000000@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.25/install_release.sh000077500000000000000000000012371435104021100164430ustar00rootroot00000000000000#!/usr/bin/env bash set -e BUILDDIR=build-auto PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24 PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056 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.25/meson.build000066400000000000000000000005741435104021100151030ustar00rootroot00000000000000project('scrcpy', 'c', version: '1.25', 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.25/meson_options.txt000066400000000000000000000015731435104021100163760ustar00rootroot00000000000000option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') scrcpy-1.25/release.mk000066400000000000000000000135221435104021100147070ustar00rootroot00000000000000# 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 VERSION := $(shell git describe --tags --always) DIST := dist WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) WIN32_TARGET := $(WIN32_TARGET_DIR).zip WIN64_TARGET := $(WIN64_TARGET_DIR).zip RELEASE_DIR := release-$(VERSION) 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 setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) ninja -C "$(TEST_BUILD_DIR)" $(GRADLE) -p server check build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) ninja -C "$(SERVER_BUILD_DIR)" prepare-deps-win32: @app/prebuilt-deps/prepare-adb.sh @app/prebuilt-deps/prepare-sdl.sh @app/prebuilt-deps/prepare-ffmpeg-win32.sh @app/prebuilt-deps/prepare-libusb.sh prepare-deps-win64: @app/prebuilt-deps/prepare-adb.sh @app/prebuilt-deps/prepare-sdl.sh @app/prebuilt-deps/prepare-ffmpeg-win64.sh @app/prebuilt-deps/prepare-libusb.sh build-win32: prepare-deps-win32 [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ meson setup "$(WIN32_BUILD_DIR)" \ --cross-file cross_win32.txt \ --buildtype release --strip -Db_lto=true \ -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN32_BUILD_DIR)" build-win64: prepare-deps-win64 [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ meson setup "$(WIN64_BUILD_DIR)" \ --cross-file cross_win64.txt \ --buildtype release --strip -Db_lto=true \ -Dcompile_server=false \ -Dportable=true ) ninja -C "$(WIN64_BUILD_DIR)" dist-win32: build-server build-win32 mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.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 app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" zip-win64: dist-win64 cd "$(DIST)"; \ zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" scrcpy-1.25/release.sh000077500000000000000000000000371435104021100147120ustar00rootroot00000000000000#!/bin/bash make -f release.mk scrcpy-1.25/run000077500000000000000000000010621435104021100134640ustar00rootroot00000000000000#!/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="app/data/icon.png" \ SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \ "$BUILDDIR/app/scrcpy" "$@" scrcpy-1.25/scripts/000077500000000000000000000000001435104021100144225ustar00rootroot00000000000000scrcpy-1.25/scripts/run-scrcpy.sh000077500000000000000000000001571435104021100170710ustar00rootroot00000000000000#!/usr/bin/env bash SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy" scrcpy-1.25/server/000077500000000000000000000000001435104021100142415ustar00rootroot00000000000000scrcpy-1.25/server/.gitignore000066400000000000000000000001301435104021100162230ustar00rootroot00000000000000*.iml .gradle /local.properties /.idea/ .DS_Store /build /captures .externalNativeBuild scrcpy-1.25/server/build.gradle000066400000000000000000000012041435104021100165150ustar00rootroot00000000000000apply plugin: 'com.android.application' android { compileSdkVersion 33 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 33 versionCode 12500 versionName "1.25" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { testImplementation 'junit:junit:4.13.2' } apply from: "$project.rootDir/config/android-checkstyle.gradle" scrcpy-1.25/server/build_without_gradle.sh000077500000000000000000000046131435104021100210040ustar00rootroot00000000000000#!/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.25 PLATFORM=${ANDROID_PLATFORM:-33} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" 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" "$BUILD_TOOLS_DIR/aidl" -o"$CLASSES_DIR" android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/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 -lt 31 ]] then # use dx "$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/wrappers/*.class echo "Archiving..." cd "$BUILD_DIR" jar cvf "$SERVER_BINARY" classes.dex rm -rf classes.dex classes else # use d8 "$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \ --output "$BUILD_DIR/classes.zip" \ android/view/*.class \ android/content/*.class \ com/genymobile/scrcpy/*.class \ com/genymobile/scrcpy/wrappers/*.class cd "$BUILD_DIR" mv classes.zip "$SERVER_BINARY" rm -rf classes fi echo "Server generated in $BUILD_DIR/$SERVER_BINARY" scrcpy-1.25/server/meson.build000066400000000000000000000022211435104021100164000ustar00rootroot00000000000000# 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('/') # prebuilt server path is relative to the root scrcpy directory prebuilt_server = '../' + 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.25/server/proguard-rules.pro000066400000000000000000000013571435104021100177440ustar00rootroot00000000000000# 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.25/server/scripts/000077500000000000000000000000001435104021100157305ustar00rootroot00000000000000scrcpy-1.25/server/scripts/build-wrapper.sh000077500000000000000000000014571435104021100210530ustar00rootroot00000000000000#!/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.25/server/src/000077500000000000000000000000001435104021100150305ustar00rootroot00000000000000scrcpy-1.25/server/src/main/000077500000000000000000000000001435104021100157545ustar00rootroot00000000000000scrcpy-1.25/server/src/main/AndroidManifest.xml000066400000000000000000000001671435104021100215510ustar00rootroot00000000000000 scrcpy-1.25/server/src/main/aidl/000077500000000000000000000000001435104021100166655ustar00rootroot00000000000000scrcpy-1.25/server/src/main/aidl/android/000077500000000000000000000000001435104021100203055ustar00rootroot00000000000000scrcpy-1.25/server/src/main/aidl/android/content/000077500000000000000000000000001435104021100217575ustar00rootroot00000000000000scrcpy-1.25/server/src/main/aidl/android/content/IOnPrimaryClipChangedListener.aidl000066400000000000000000000013651435104021100304400ustar00rootroot00000000000000/** * 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.25/server/src/main/aidl/android/view/000077500000000000000000000000001435104021100212575ustar00rootroot00000000000000scrcpy-1.25/server/src/main/aidl/android/view/IRotationWatcher.aidl000066400000000000000000000014411435104021100253400ustar00rootroot00000000000000/* //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.25/server/src/main/java/000077500000000000000000000000001435104021100166755ustar00rootroot00000000000000scrcpy-1.25/server/src/main/java/com/000077500000000000000000000000001435104021100174535ustar00rootroot00000000000000scrcpy-1.25/server/src/main/java/com/genymobile/000077500000000000000000000000001435104021100216055ustar00rootroot00000000000000scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/000077500000000000000000000000001435104021100231105ustar00rootroot00000000000000scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/Binary.java000066400000000000000000000017721435104021100252060ustar00rootroot00000000000000package com.genymobile.scrcpy; public final class Binary { private Binary() { // not instantiable } public static int toUnsigned(short value) { return value & 0xffff; } public static int toUnsigned(byte value) { return value & 0xff; } /** * Convert unsigned 16-bit fixed-point to a float between 0 and 1 * * @param value encoded value * @return Float value between 0 and 1 */ public static float u16FixedPointToFloat(short value) { int unsignedShort = Binary.toUnsigned(value); // 0x1p16f is 2^16 as float return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f); } /** * Convert signed 16-bit fixed-point to a float between -1 and 1 * * @param value encoded value * @return Float value between -1 and 1 */ public static float i16FixedPointToFloat(short value) { // 0x1p15f is 2^15 as float return value == 0x7fff ? 1f : (value / 0x1p15f); } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/CleanUp.java000066400000000000000000000144751435104021100253150ustar00rootroot00000000000000package com.genymobile.scrcpy; 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) { 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.25/server/src/main/java/com/genymobile/scrcpy/CodecOption.java000066400000000000000000000062461435104021100261710ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Command.java000066400000000000000000000027741435104021100253430ustar00rootroot00000000000000package com.genymobile.scrcpy; import java.io.IOException; import java.util.Arrays; import java.util.Scanner; public final class Command { private Command() { // not instantiable } public static void exec(String... cmd) throws IOException, InterruptedException { Process process = Runtime.getRuntime().exec(cmd); int exitCode = process.waitFor(); if (exitCode != 0) { throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); } } public static String execReadLine(String... cmd) throws IOException, InterruptedException { String result = null; Process process = Runtime.getRuntime().exec(cmd); Scanner scanner = new Scanner(process.getInputStream()); if (scanner.hasNextLine()) { result = scanner.nextLine(); } int exitCode = process.waitFor(); if (exitCode != 0) { throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); } return result; } public static String execReadOutput(String... cmd) throws IOException, InterruptedException { Process process = Runtime.getRuntime().exec(cmd); String output = IO.toString(process.getInputStream()); int exitCode = process.waitFor(); if (exitCode != 0) { throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode); } return output; } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/ControlMessage.java000066400000000000000000000116671435104021100267130ustar00rootroot00000000000000package 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 float hScroll; private float 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, float hScroll, float vScroll, int buttons) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_INJECT_SCROLL_EVENT; msg.position = position; msg.hScroll = hScroll; msg.vScroll = vScroll; msg.buttons = buttons; return msg; } 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 float getHScroll() { return hScroll; } public float 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.25/server/src/main/java/com/genymobile/scrcpy/ControlMessageReader.java000066400000000000000000000160241435104021100300260ustar00rootroot00000000000000package 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 = Binary.toUnsigned(buffer.get()); int keycode = buffer.getInt(); int repeat = buffer.getInt(); int metaState = buffer.getInt(); return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState); } private String parseString() { 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 = Binary.toUnsigned(buffer.get()); long pointerId = buffer.getLong(); Position position = readPosition(buffer); float pressure = Binary.u16FixedPointToFloat(buffer.getShort()); 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); float hScroll = Binary.i16FixedPointToFloat(buffer.getShort()); float vScroll = Binary.i16FixedPointToFloat(buffer.getShort()); int buttons = buffer.getInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } private ControlMessage parseBackOrScreenOnEvent() { if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) { return null; } int action = Binary.toUnsigned(buffer.get()); return ControlMessage.createBackOrScreenOn(action); } private ControlMessage parseGetClipboard() { if (buffer.remaining() < GET_CLIPBOARD_LENGTH) { return null; } int copyKey = Binary.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 = Binary.toUnsigned(buffer.getShort()); int screenHeight = Binary.toUnsigned(buffer.getShort()); return new Position(x, y, screenWidth, screenHeight); } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/Controller.java000066400000000000000000000316631435104021100261070ustar00rootroot00000000000000package 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; // control_msg.h values of the pointerId field in inject_touch_event message private static final int POINTER_ID_MOUSE = -1; private static final int POINTER_ID_VIRTUAL_MOUSE = -3; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private final Device device; private final DesktopConnection connection; private final DeviceMessageSender sender; private final boolean clipboardAutosync; private final boolean powerOn; 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, boolean powerOn) { this.device = device; this.connection = connection; this.clipboardAutosync = clipboardAutosync; this.powerOn = powerOn; 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 (powerOn && !Device.isScreenOn()) { device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); // dirty hack // After POWER is injected, the device is powered on asynchronously. // To turn the device screen off while mirroring, the client will send a message that // would be handled before the device is actually powered on, so its effect would // be "canceled" once the device is turned back on. // Adding this delay prevents to handle the message before the device is actually // powered on. SystemClock.sleep(500); } 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(), msg.getButtons()); } 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 source; int pointerCount = pointersState.update(pointerProperties, pointerCoords); if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) { // real mouse event (forced by the client when --forward-on-click) pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE; source = InputDevice.SOURCE_MOUSE; } else { // POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER; source = InputDevice.SOURCE_TOUCHSCREEN; // Buttons must not be set for touch events buttons = 0; } 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); } } 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, float hScroll, float vScroll, int buttons) { 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, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); return device.injectEvent(event, Device.INJECT_MODE_ASYNC); } /** * Schedule a call to set power mode to off after a small delay. */ private static void schedulePowerModeOff() { EXECUTOR.schedule(new Runnable() { @Override public void run() { Ln.i("Forcing screen off"); Device.setScreenPowerMode(Device.POWER_MODE_OFF); } }, 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.25/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java000066400000000000000000000110521435104021100274030ustar00rootroot00000000000000package 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; if (controlSocket != null) { controlInputStream = controlSocket.getInputStream(); controlOutputStream = controlSocket.getOutputStream(); } else { controlInputStream = null; controlOutputStream = null; } 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(boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException { LocalSocket videoSocket; LocalSocket controlSocket = null; if (tunnelForward) { LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME); try { videoSocket = localServerSocket.accept(); if (sendDummyByte) { // send one byte so the client may read() to detect a connection error videoSocket.getOutputStream().write(0); } if (control) { try { controlSocket = localServerSocket.accept(); } catch (IOException | RuntimeException e) { videoSocket.close(); throw e; } } } finally { localServerSocket.close(); } } else { videoSocket = connect(SOCKET_NAME); if (control) { try { controlSocket = connect(SOCKET_NAME); } catch (IOException | RuntimeException e) { videoSocket.close(); throw e; } } } return new DesktopConnection(videoSocket, controlSocket); } public void close() throws IOException { videoSocket.shutdownInput(); videoSocket.shutdownOutput(); videoSocket.close(); if (controlSocket != null) { controlSocket.shutdownInput(); controlSocket.shutdownOutput(); controlSocket.close(); } } public void sendDeviceMeta(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.25/server/src/main/java/com/genymobile/scrcpy/Device.java000066400000000000000000000302301435104021100251500ustar00rootroot00000000000000package 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; public interface RotationListener { void onRotationChanged(int rotation); } public interface ClipboardListener { void onClipboardTextChanged(String text); } private final Size deviceSize; private final Rect crop; private int maxSize; private final int lockVideoOrientation; 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 = ServiceManager.getDisplayManager().getDisplayInfo(displayId); if (displayInfo == null) { int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds(); throw new InvalidDisplayIdException(displayId, displayIds); } int displayInfoFlags = displayInfo.getFlags(); deviceSize = displayInfo.getSize(); crop = options.getCrop(); maxSize = options.getMaxSize(); lockVideoOrientation = options.getLockVideoOrientation(); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); layerStack = displayInfo.getLayerStack(); ServiceManager.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 = ServiceManager.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 void setMaxSize(int newMaxSize) { maxSize = newMaxSize; screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); } 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 ServiceManager.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 ServiceManager.getPowerManager().isScreenOn(); } public synchronized void setRotationListener(RotationListener rotationListener) { this.rotationListener = rotationListener; } public synchronized void setClipboardListener(ClipboardListener clipboardListener) { this.clipboardListener = clipboardListener; } public static void expandNotificationPanel() { ServiceManager.getStatusBarManager().expandNotificationsPanel(); } public static void expandSettingsPanel() { ServiceManager.getStatusBarManager().expandSettingsPanel(); } public static void collapsePanels() { ServiceManager.getStatusBarManager().collapsePanels(); } public static String getClipboardText() { ClipboardManager clipboardManager = ServiceManager.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 = ServiceManager.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 = ServiceManager.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(); } } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/DeviceMessage.java000066400000000000000000000017141435104021100264620ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java000066400000000000000000000026361435104021100276270ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/DeviceMessageWriter.java000066400000000000000000000026161435104021100276610ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/DisplayInfo.java000066400000000000000000000015451435104021100262010ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/IO.java000066400000000000000000000033441435104021100242660ustar00rootroot00000000000000package 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.io.InputStream; import java.nio.ByteBuffer; import java.util.Scanner; 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)); } public static String toString(InputStream inputStream) { StringBuilder builder = new StringBuilder(); Scanner scanner = new Scanner(inputStream); while (scanner.hasNextLine()) { builder.append(scanner.nextLine()).append('\n'); } return builder.toString(); } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/InvalidDisplayIdException.java000066400000000000000000000010741435104021100310250ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/InvalidEncoderException.java000066400000000000000000000011421435104021100305160ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/KeyComposition.java000066400000000000000000000133311435104021100267300ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Ln.java000066400000000000000000000043061435104021100243270ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Options.java000066400000000000000000000111751435104021100254130ustar00rootroot00000000000000package 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 control = true; private int displayId; private boolean showTouches; private boolean stayAwake; private List codecOptions; private String encoderName; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; private boolean downsizeOnError = true; private boolean cleanup = true; private boolean powerOn = true; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size private boolean sendFrameMeta = true; // send PTS so that the client may record properly private boolean sendDummyByte = true; // write a byte on start to detect connection issues 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 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; } public boolean getDownsizeOnError() { return downsizeOnError; } public void setDownsizeOnError(boolean downsizeOnError) { this.downsizeOnError = downsizeOnError; } public boolean getCleanup() { return cleanup; } public void setCleanup(boolean cleanup) { this.cleanup = cleanup; } public boolean getPowerOn() { return powerOn; } public void setPowerOn(boolean powerOn) { this.powerOn = powerOn; } public boolean getSendDeviceMeta() { return sendDeviceMeta; } public void setSendDeviceMeta(boolean sendDeviceMeta) { this.sendDeviceMeta = sendDeviceMeta; } public boolean getSendFrameMeta() { return sendFrameMeta; } public void setSendFrameMeta(boolean sendFrameMeta) { this.sendFrameMeta = sendFrameMeta; } public boolean getSendDummyByte() { return sendDummyByte; } public void setSendDummyByte(boolean sendDummyByte) { this.sendDummyByte = sendDummyByte; } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/Point.java000066400000000000000000000014251435104021100250460ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Pointer.java000066400000000000000000000020141435104021100253700ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/PointersState.java000066400000000000000000000055041435104021100265630ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Position.java000066400000000000000000000032521435104021100255610ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java000066400000000000000000000303171435104021100264760ustar00rootroot00000000000000package 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"; // Keep the values in descending order private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final ByteBuffer headerBuffer = ByteBuffer.allocate(12); private final String encoderName; private final List codecOptions; private final int bitRate; private final int maxFps; private final boolean sendFrameMeta; private final boolean downsizeOnError; private long ptsOrigin; private boolean firstFrameSent; public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; this.encoderName = encoderName; this.downsizeOnError = downsizeOnError; } @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()); Surface surface = null; try { configure(codec, format); surface = codec.createInputSurface(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); codec.start(); alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!downsizeOnError || firstFrameSent) { // Fail immediately throw e; } int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); if (newMaxSize == 0) { // Definitively fail throw e; } // Retry with a smaller device size Ln.i("Retrying with -m" + newMaxSize + "..."); device.setMaxSize(newMaxSize); alive = true; } finally { destroyDisplay(display); codec.release(); if (surface != null) { surface.release(); } } } while (alive); } finally { device.setRotationListener(null); } } private static int chooseMaxSizeFallback(Size failedSize) { int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); for (int value : MAX_SIZE_FALLBACK) { if (value < currentMaxSize) { // We found a smaller value to reduce the video size return value; } } // No fallback, fail definitively return 0; } private boolean encode(MediaCodec codec, 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); if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) { // If this is not a config packet, then it contains a frame firstFrameSent = true; } } } 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 = PACKET_FLAG_CONFIG; // non-media data packet } else { if (ptsOrigin == 0) { ptsOrigin = bufferInfo.presentationTimeUs; } pts = bufferInfo.presentationTimeUs - ptsOrigin; if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { pts |= PACKET_FLAG_KEY_FRAME; } } 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.25/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java000066400000000000000000000135141435104021100260120ustar00rootroot00000000000000package 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(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } 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.25/server/src/main/java/com/genymobile/scrcpy/Server.java000066400000000000000000000327141435104021100252300ustar00rootroot00000000000000package com.genymobile.scrcpy; import android.graphics.Rect; 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; boolean restoreNormalPowerMode = options.getControl(); // only restore power mode if control is enabled if (options.getShowTouches() || options.getStayAwake()) { 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); } } } if (options.getCleanup()) { try { CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, restoreNormalPowerMode, 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(); boolean control = options.getControl(); boolean sendDummyByte = options.getSendDummyByte(); try (DesktopConnection connection = DesktopConnection.open(tunnelForward, control, sendDummyByte)) { if (options.getSendDeviceMeta()) { Size videoSize = device.getScreenInfo().getVideoSize(); connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight()); } ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, options.getEncoderName(), options.getDownsizeOnError()); Thread controllerThread = null; Thread deviceMessageSenderThread = null; if (control) { final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn()); // 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 "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; case "downsize_on_error": boolean downsizeOnError = Boolean.parseBoolean(value); options.setDownsizeOnError(downsizeOnError); break; case "cleanup": boolean cleanup = Boolean.parseBoolean(value); options.setCleanup(cleanup); break; case "power_on": boolean powerOn = Boolean.parseBoolean(value); options.setPowerOn(powerOn); break; case "send_device_meta": boolean sendDeviceMeta = Boolean.parseBoolean(value); options.setSendDeviceMeta(sendDeviceMeta); break; case "send_frame_meta": boolean sendFrameMeta = Boolean.parseBoolean(value); options.setSendFrameMeta(sendFrameMeta); break; case "send_dummy_byte": boolean sendDummyByte = Boolean.parseBoolean(value); options.setSendDummyByte(sendDummyByte); break; case "raw_video_stream": boolean rawVideoStream = Boolean.parseBoolean(value); if (rawVideoStream) { options.setSendDeviceMeta(false); options.setSendFrameMeta(false); options.setSendDummyByte(false); } 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 (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.25/server/src/main/java/com/genymobile/scrcpy/Settings.java000066400000000000000000000065321435104021100255610ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import java.io.IOException; public final class Settings { public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM; public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE; public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL; private Settings() { /* not instantiable */ } private static void execSettingsPut(String table, String key, String value) throws SettingsException { try { Command.exec("settings", "put", table, key, value); } catch (IOException | InterruptedException e) { throw new SettingsException("put", table, key, value, e); } } private static String execSettingsGet(String table, String key) throws SettingsException { try { return Command.execReadLine("settings", "get", table, key); } catch (IOException | InterruptedException e) { throw new SettingsException("get", table, key, null, e); } } public static String getValue(String table, String key) throws SettingsException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); } catch (SettingsException e) { Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e); } } return execSettingsGet(table, key); } public static void putValue(String table, String key, String value) throws SettingsException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); } catch (SettingsException e) { Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e); } } execSettingsPut(table, key, value); } public static String getAndPutValue(String table, String key, String value) throws SettingsException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); if (!value.equals(oldValue)) { provider.putValue(table, key, value); } return oldValue; } catch (SettingsException e) { Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e); } } String oldValue = getValue(table, key); if (!value.equals(oldValue)) { putValue(table, key, value); } return oldValue; } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/SettingsException.java000066400000000000000000000007401435104021100274330ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Size.java000066400000000000000000000020571435104021100246710ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/StringUtils.java000066400000000000000000000012471435104021100262460ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/Workarounds.java000066400000000000000000000073761435104021100263060ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/000077500000000000000000000000001435104021100247535ustar00rootroot00000000000000scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java000066400000000000000000000067661435104021100307240ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java000066400000000000000000000150621435104021100310140ustar00rootroot00000000000000package 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; private boolean alternativeGetMethod; private boolean alternativeSetMethod; private boolean alternativeAddListenerMethod; 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 { try { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); } catch (NoSuchMethodException e) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); alternativeGetMethod = true; } } } 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 { try { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); } catch (NoSuchMethodException e) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); alternativeSetMethod = true; } } } return setPrimaryClipMethod; } private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME); } if (alternativeMethod) { return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); } return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME); } else if (alternativeMethod) { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); } else { method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID); } } public CharSequence getText() { try { Method method = getGetPrimaryClipMethod(); ClipData clipData = getPrimaryClip(method, alternativeGetMethod, 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, alternativeSetMethod, manager, clipData); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager, IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME); } else if (alternativeMethod) { method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID); } 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 { try { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); } catch (NoSuchMethodException e) { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class); alternativeAddListenerMethod = true; } } } return addPrimaryClipChangedListener; } public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { try { Method method = getAddPrimaryClipChangedListener(); addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener); return true; } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { Ln.e("Could not invoke method", e); return false; } } } scrcpy-1.25/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java000066400000000000000000000154661435104021100307570ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java000066400000000000000000000073661435104021100305320ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Command; import com.genymobile.scrcpy.DisplayInfo; import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Size; import android.view.Display; import java.lang.reflect.Field; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal public DisplayManager(Object manager) { this.manager = manager; } // public to call it from unit tests public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { Pattern regex = Pattern.compile( "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " + "rotation ([0-9]+).*?, layerStack ([0-9]+)", Pattern.MULTILINE); Matcher m = regex.matcher(dumpsysDisplayOutput); if (!m.find()) { return null; } int flags = parseDisplayFlags(m.group(1)); int width = Integer.parseInt(m.group(2)); int height = Integer.parseInt(m.group(3)); int rotation = Integer.parseInt(m.group(4)); int layerStack = Integer.parseInt(m.group(5)); return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { try { String dumpsysDisplayOutput = Command.execReadOutput("dumpsys", "display"); return parseDisplayInfo(dumpsysDisplayOutput, displayId); } catch (Exception e) { Ln.e("Could not get display info from \"dumpsys display\" output", e); return null; } } private static int parseDisplayFlags(String text) { Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); if (text == null) { return 0; } int flags = 0; Matcher m = regex.matcher(text); while (m.find()) { String flagString = m.group(); try { Field filed = Display.class.getDeclaredField(flagString); flags |= filed.getInt(null); } catch (NoSuchFieldException | IllegalAccessException e) { // Silently ignore, some flags reported by "dumpsys display" are @TestApi } } return flags; } public DisplayInfo getDisplayInfo(int displayId) { try { Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); } 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java000066400000000000000000000040621435104021100302120ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.Ln; 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 android.hardware.input.InputManager manager; private Method injectInputEventMethod; private static Method setDisplayIdMethod; public InputManager(android.hardware.input.InputManager 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java000066400000000000000000000023401435104021100302040ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java000066400000000000000000000113701435104021100305130ustar00rootroot00000000000000package com.genymobile.scrcpy.wrappers; import android.annotation.SuppressLint; import android.os.IBinder; import android.os.IInterface; import java.lang.reflect.InvocationTargetException; 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 static final Method GET_SERVICE_METHOD; static { try { GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); } catch (Exception e) { throw new AssertionError(e); } } private static WindowManager windowManager; private static DisplayManager displayManager; private static InputManager inputManager; private static PowerManager powerManager; private static StatusBarManager statusBarManager; private static ClipboardManager clipboardManager; private static ActivityManager activityManager; private ServiceManager() { /* not instantiable */ } private static IInterface getService(String service, String type) { try { IBinder binder = (IBinder) GET_SERVICE_METHOD.invoke(null, service); Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); return (IInterface) asInterfaceMethod.invoke(null, binder); } catch (Exception e) { throw new AssertionError(e); } } public static WindowManager getWindowManager() { if (windowManager == null) { windowManager = new WindowManager(getService("window", "android.view.IWindowManager")); } return windowManager; } public static DisplayManager getDisplayManager() { if (displayManager == null) { try { Class clazz = Class.forName("android.hardware.display.DisplayManagerGlobal"); Method getInstanceMethod = clazz.getDeclaredMethod("getInstance"); Object dmg = getInstanceMethod.invoke(null); displayManager = new DisplayManager(dmg); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new AssertionError(e); } } return displayManager; } public static InputManager getInputManager() { if (inputManager == null) { try { Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance"); android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null); inputManager = new InputManager(im); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { throw new AssertionError(e); } } return inputManager; } public static PowerManager getPowerManager() { if (powerManager == null) { powerManager = new PowerManager(getService("power", "android.os.IPowerManager")); } return powerManager; } public static StatusBarManager getStatusBarManager() { if (statusBarManager == null) { statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService")); } return statusBarManager; } public static ClipboardManager getClipboardManager() { if (clipboardManager == null) { IInterface clipboard = getService("clipboard", "android.content.IClipboard"); if (clipboard == null) { // Some devices have no clipboard manager // // return null; } clipboardManager = new ClipboardManager(clipboard); } return clipboardManager; } public static 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/StatusBarManager.java000066400000000000000000000067451435104021100310350ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java000066400000000000000000000114761435104021100305600ustar00rootroot00000000000000package 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.25/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java000066400000000000000000000077431435104021100303730ustar00rootroot00000000000000package 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.25/server/src/test/000077500000000000000000000000001435104021100160075ustar00rootroot00000000000000scrcpy-1.25/server/src/test/java/000077500000000000000000000000001435104021100167305ustar00rootroot00000000000000scrcpy-1.25/server/src/test/java/com/000077500000000000000000000000001435104021100175065ustar00rootroot00000000000000scrcpy-1.25/server/src/test/java/com/genymobile/000077500000000000000000000000001435104021100216405ustar00rootroot00000000000000scrcpy-1.25/server/src/test/java/com/genymobile/scrcpy/000077500000000000000000000000001435104021100231435ustar00rootroot00000000000000scrcpy-1.25/server/src/test/java/com/genymobile/scrcpy/BinaryTest.java000066400000000000000000000046101435104021100260730ustar00rootroot00000000000000package com.genymobile.scrcpy; import org.junit.Assert; import org.junit.Test; public class BinaryTest { @Test public void testU16FixedPointToFloat() { final float delta = 0.0f; // on these values, there MUST be no rounding error Assert.assertEquals(0.0f, Binary.u16FixedPointToFloat((short) 0), delta); Assert.assertEquals(0.03125f, Binary.u16FixedPointToFloat((short) 0x800), delta); Assert.assertEquals(0.0625f, Binary.u16FixedPointToFloat((short) 0x1000), delta); Assert.assertEquals(0.125f, Binary.u16FixedPointToFloat((short) 0x2000), delta); Assert.assertEquals(0.25f, Binary.u16FixedPointToFloat((short) 0x4000), delta); Assert.assertEquals(0.5f, Binary.u16FixedPointToFloat((short) 0x8000), delta); Assert.assertEquals(0.75f, Binary.u16FixedPointToFloat((short) 0xc000), delta); Assert.assertEquals(1.0f, Binary.u16FixedPointToFloat((short) 0xffff), delta); } @Test public void testI16FixedPointToFloat() { final float delta = 0.0f; // on these values, there MUST be no rounding error Assert.assertEquals(0.0f, Binary.i16FixedPointToFloat((short) 0), delta); Assert.assertEquals(0.03125f, Binary.i16FixedPointToFloat((short) 0x400), delta); Assert.assertEquals(0.0625f, Binary.i16FixedPointToFloat((short) 0x800), delta); Assert.assertEquals(0.125f, Binary.i16FixedPointToFloat((short) 0x1000), delta); Assert.assertEquals(0.25f, Binary.i16FixedPointToFloat((short) 0x2000), delta); Assert.assertEquals(0.5f, Binary.i16FixedPointToFloat((short) 0x4000), delta); Assert.assertEquals(0.75f, Binary.i16FixedPointToFloat((short) 0x6000), delta); Assert.assertEquals(1.0f, Binary.i16FixedPointToFloat((short) 0x7fff), delta); Assert.assertEquals(-0.03125f, Binary.i16FixedPointToFloat((short) -0x400), delta); Assert.assertEquals(-0.0625f, Binary.i16FixedPointToFloat((short) -0x800), delta); Assert.assertEquals(-0.125f, Binary.i16FixedPointToFloat((short) -0x1000), delta); Assert.assertEquals(-0.25f, Binary.i16FixedPointToFloat((short) -0x2000), delta); Assert.assertEquals(-0.5f, Binary.i16FixedPointToFloat((short) -0x4000), delta); Assert.assertEquals(-0.75f, Binary.i16FixedPointToFloat((short) -0x6000), delta); Assert.assertEquals(-1.0f, Binary.i16FixedPointToFloat((short) -0x8000), delta); } } scrcpy-1.25/server/src/test/java/com/genymobile/scrcpy/CodecOptionsTest.java000066400000000000000000000074461435104021100272520ustar00rootroot00000000000000package 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.25/server/src/test/java/com/genymobile/scrcpy/CommandParserTest.java000066400000000000000000000470351435104021100274120ustar00rootroot00000000000000package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.DisplayManager; import android.view.Display; import org.junit.Assert; import org.junit.Test; public class CommandParserTest { @Test public void testParseDisplayInfoFromDumpsysDisplay() { /* @formatter:off */ String partialOutput = "Logical Displays: size=1\n" + " Display 0:\n" + "mDisplayId=0\n" + " mLayerStack=0\n" + " mHasContent=true\n" + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + " mRequestedColorMode=0\n" + " mDisplayOffset=(0, 0)\n" + " mDisplayScalingDisabled=false\n" + " mPrimaryDisplayDevice=Built-in Screen\n" + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state OFF, " + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + "FLAG_TRUSTED, real 1440 x 3120, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + "minimalPostProcessingSupported false, rotation 0, state ON, type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 " + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + "relativeAddress=null}, removeMode 0}\n" + " mRequestedMinimalPostProcessing=false\n"; DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); Assert.assertNotNull(displayInfo); Assert.assertEquals(0, displayInfo.getDisplayId()); Assert.assertEquals(0, displayInfo.getRotation()); Assert.assertEquals(0, displayInfo.getLayerStack()); // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); Assert.assertEquals(1440, displayInfo.getSize().getWidth()); Assert.assertEquals(3120, displayInfo.getSize().getHeight()); } @Test public void testParseDisplayInfoFromDumpsysDisplayWithRotation() { /* @formatter:off */ String partialOutput = "Logical Displays: size=1\n" + " Display 0:\n" + "mDisplayId=0\n" + " mLayerStack=0\n" + " mHasContent=true\n" + " mDesiredDisplayModeSpecs={baseModeId=2 primaryRefreshRateRange=[90 90] appRequestRefreshRateRange=[90 90]}\n" + " mRequestedColorMode=0\n" + " mDisplayOffset=(0, 0)\n" + " mDisplayScalingDisabled=false\n" + " mPrimaryDisplayDevice=Built-in Screen\n" + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, " + "real 1440 x 3120, largest app 1440 x 3120, smallest app 1440 x 3120, appVsyncOff 2000000, presDeadline 11111111, mode 2, " + "defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, width=1080, " + "height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], " + "mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, minimalPostProcessingSupported false, rotation 0, state ON, " + "type INTERNAL, uniqueId \"local:0\", app 1440 x 3120, density 600 (515.154 x 514.597) dpi, layerStack 0, colorMode 0, " + "supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo DeviceProductInfo{name=, manufacturerPnpId=QCM, " + "productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, relativeAddress=null}, removeMode 0}\n" + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, " + "FLAG_TRUSTED, real 3120 x 1440, largest app 3120 x 2983, smallest app 1440 x 1303, appVsyncOff 2000000, presDeadline 11111111, " + "mode 2, defaultMode 1, modes [{id=1, width=1440, height=3120, fps=60.0}, {id=2, width=1440, height=3120, fps=90.0}, {id=3, " + "width=1080, height=2340, fps=90.0}, {id=4, width=1080, height=2340, fps=60.0}], hdrCapabilities " + "HdrCapabilities{mSupportedHdrTypes=[2, 3, 4], mMaxLuminance=540.0, mMaxAverageLuminance=270.1, mMinLuminance=0.2}, " + "minimalPostProcessingSupported false, rotation 3, state ON, type INTERNAL, uniqueId \"local:0\", app 3120 x 1440, density 600 " + "(515.154 x 514.597) dpi, layerStack 0, colorMode 0, supportedColorModes [0, 7, 9], address {port=129, model=0}, deviceProductInfo " + "DeviceProductInfo{name=, manufacturerPnpId=QCM, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, " + "relativeAddress=null}, removeMode 0}\n" + " mRequestedMinimalPostProcessing=false"; DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); Assert.assertNotNull(displayInfo); Assert.assertEquals(0, displayInfo.getDisplayId()); Assert.assertEquals(3, displayInfo.getRotation()); Assert.assertEquals(0, displayInfo.getLayerStack()); // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); Assert.assertEquals(3120, displayInfo.getSize().getWidth()); Assert.assertEquals(1440, displayInfo.getSize().getHeight()); } @Test public void testParseDisplayInfoFromDumpsysDisplayAPI31() { /* @formatter:off */ String partialOutput = "Logical Displays: size=1\n" + " Display 0:\n" + " mDisplayId=0\n" + " mPhase=1\n" + " mLayerStack=0\n" + " mHasContent=true\n" + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 " + "Infinity]}\n" + " mRequestedColorMode=0\n" + " mDisplayOffset=(0, 0)\n" + " mDisplayScalingDisabled=false\n" + " mPrimaryDisplayDevice=Built-in Screen\n" + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, " + "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff " + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, FLAG_SECURE, " + "FLAG_SUPPORTS_PROTECTED_BUFFERS, FLAG_TRUSTED, real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff " + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + " mRequestedMinimalPostProcessing=false\n" + " mFrameRateOverrides=[]\n" + " mPendingFrameRateOverrideUids={}\n"; DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); Assert.assertNotNull(displayInfo); Assert.assertEquals(0, displayInfo.getDisplayId()); Assert.assertEquals(0, displayInfo.getRotation()); Assert.assertEquals(0, displayInfo.getLayerStack()); // FLAG_TRUSTED does not exist in Display (@TestApi), so it won't be reported Assert.assertEquals(Display.FLAG_SECURE | Display.FLAG_SUPPORTS_PROTECTED_BUFFERS, displayInfo.getFlags()); Assert.assertEquals(1080, displayInfo.getSize().getWidth()); Assert.assertEquals(2280, displayInfo.getSize().getHeight()); } @Test public void testParseDisplayInfoFromDumpsysDisplayAPI31NoFlags() { /* @formatter:off */ String partialOutput = "Logical Displays: size=1\n" + " Display 0:\n" + " mDisplayId=0\n" + " mPhase=1\n" + " mLayerStack=0\n" + " mHasContent=true\n" + " mDesiredDisplayModeSpecs={baseModeId=1 allowGroupSwitching=false primaryRefreshRateRange=[0 60] appRequestRefreshRateRange=[0 " + "Infinity]}\n" + " mRequestedColorMode=0\n" + " mDisplayOffset=(0, 0)\n" + " mDisplayScalingDisabled=false\n" + " mPrimaryDisplayDevice=Built-in Screen\n" + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + "real 1080 x 2280, largest app 1080 x 2280, smallest app 1080 x 2280, appVsyncOff " + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2280, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen\", displayId 0\", displayGroupId 0, " + "real 1080 x 2280, largest app 2148 x 2065, smallest app 1080 x 997, appVsyncOff " + "1000000, presDeadline 16666666, mode 1, defaultMode 1, modes [{id=1, width=1080, height=2280, fps=60.000004, " + "alternativeRefreshRates=[]}], hdrCapabilities HdrCapabilities{mSupportedHdrTypes=[], mMaxLuminance=500.0, " + "mMaxAverageLuminance=500.0, mMinLuminance=0.0}, userDisabledHdrTypes [], minimalPostProcessingSupported false, rotation 0, state " + "ON, type INTERNAL, uniqueId \"local:0\", app 1080 x 2148, density 440 (440.0 x 440.0) dpi, layerStack 0, colorMode 0, " + "supportedColorModes [0], address {port=0, model=0}, deviceProductInfo DeviceProductInfo{name=EMU_display_0, " + "manufacturerPnpId=GGL, productId=1, modelYear=null, manufactureDate=ManufactureDate{week=27, year=2006}, connectionToSinkType=0}, " + "removeMode 0, refreshRateOverride 0.0, brightnessMinimum 0.0, brightnessMaximum 1.0, brightnessDefault 0.39763778}\n" + " mRequestedMinimalPostProcessing=false\n" + " mFrameRateOverrides=[]\n" + " mPendingFrameRateOverrideUids={}\n"; DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 0); Assert.assertNotNull(displayInfo); Assert.assertEquals(0, displayInfo.getDisplayId()); Assert.assertEquals(0, displayInfo.getRotation()); Assert.assertEquals(0, displayInfo.getLayerStack()); Assert.assertEquals(0, displayInfo.getFlags()); Assert.assertEquals(1080, displayInfo.getSize().getWidth()); Assert.assertEquals(2280, displayInfo.getSize().getHeight()); } @Test public void testParseDisplayInfoFromDumpsysDisplayAPI29WithNoFlags() { /* @formatter:off */ String partialOutput = "Logical Displays: size=2\n" + " Display 0:\n" + " mDisplayId=0\n" + " mLayerStack=0\n" + " mHasContent=true\n" + " mAllowedDisplayModes=[1]\n" + " mRequestedColorMode=0\n" + " mDisplayOffset=(0, 0)\n" + " mDisplayScalingDisabled=false\n" + " mPrimaryDisplayDevice=Built-in Screen\n" + " mBaseDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + "real 3664 x 1920, largest app 3664 x 1920, smallest app 3664 x 1920, mode 61, defaultMode 61, modes [" + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + " mOverrideDisplayInfo=DisplayInfo{\"Built-in Screen, displayId 0\", uniqueId \"local:0\", app 3664 x 1920, " + "real 3664 x 1920, largest app 3664 x 3620, smallest app 1920 x 1876, mode 61, defaultMode 61, modes [" + "{id=1, width=3664, height=1920, fps=60.000004}, {id=2, width=3664, height=1920, fps=61.000004}, " + "{id=61, width=3664, height=1920, fps=120.00001}], colorMode 0, supportedColorModes [0], " + "hdrCapabilities android.view.Display$HdrCapabilities@4a41fe79, rotation 0, density 290 (320.842 x 319.813) dpi, " + "layerStack 0, appVsyncOff 1000000, presDeadline 8333333, type BUILT_IN, address {port=129, model=0}, " + "state ON, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, removeMode 0}\n" + " Display 31:\n" + " mDisplayId=31\n" + " mLayerStack=31\n" + " mHasContent=true\n" + " mAllowedDisplayModes=[92]\n" + " mRequestedColorMode=0\n" + " mDisplayOffset=(0, 0)\n" + " mDisplayScalingDisabled=false\n" + " mPrimaryDisplayDevice=PanelLayer-#main\n" + " mBaseDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId " + "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 110, smallest app 800 x " + "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + "type VIRTUAL, state ON, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n" + " mOverrideDisplayInfo=DisplayInfo{\"PanelLayer-#main, displayId 31\", uniqueId " + "\"virtual:com.test.system,10040,PanelLayer-#main,0\", app 800 x 110, real 800 x 110, largest app 800 x 800, smallest app 110 x " + "110, mode 92, defaultMode 92, modes [{id=92, width=800, height=110, fps=60.0}], colorMode 0, supportedColorModes [0], " + "hdrCapabilities null, rotation 0, density 200 (200.0 x 200.0) dpi, layerStack 31, appVsyncOff 0, presDeadline 16666666, " + "type VIRTUAL, state OFF, owner com.test.system (uid 10040), FLAG_PRIVATE, removeMode 1}\n"; DisplayInfo displayInfo = DisplayManager.parseDisplayInfo(partialOutput, 31); Assert.assertNotNull(displayInfo); Assert.assertEquals(31, displayInfo.getDisplayId()); Assert.assertEquals(0, displayInfo.getRotation()); Assert.assertEquals(31, displayInfo.getLayerStack()); Assert.assertEquals(0, displayInfo.getFlags()); Assert.assertEquals(800, displayInfo.getSize().getWidth()); Assert.assertEquals(110, displayInfo.getSize().getHeight()); } } scrcpy-1.25/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java000066400000000000000000000375411435104021100307300ustar00rootroot00000000000000package 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.writeShort(0); // 0.0f encoded as i16 dos.writeShort(0x8000); // -1.0f encoded as i16 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(0f, event.getHScroll(), 0f); Assert.assertEquals(-1f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); } @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.25/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java000066400000000000000000000032471435104021100305550ustar00rootroot00000000000000package 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.25/server/src/test/java/com/genymobile/scrcpy/StringUtilsTest.java000066400000000000000000000023211435104021100271330ustar00rootroot00000000000000package 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.25/settings.gradle000066400000000000000000000000221435104021100157450ustar00rootroot00000000000000include ':server'